Working Error Logging
All checks were successful
Run Tests on Code / run-tests (push) Successful in 0s

This commit is contained in:
Neshura 2023-12-17 22:29:18 +01:00
parent bba7fae8b9
commit 47e6cc59c0
Signed by: Neshura
GPG key ID: B6983AAA6B9A7A6C
7 changed files with 253 additions and 123 deletions

View file

@ -1,10 +1,9 @@
use std::collections::HashMap;
use std::error::Error;
use std::sync::{Arc};
use chrono::{DateTime, Duration, Timelike, Utc};
use lemmy_db_schema::newtypes::{CommunityId, LanguageId};
use lemmy_db_schema::PostFeatureType;
use tokio::sync::{RwLock, RwLockWriteGuard};
use tokio::sync::{RwLock};
use crate::{jnovel, lemmy, Message, SharedData};
use crate::config::{Config, PostBody, SeriesConfig};
use crate::jnovel::PostInfo;
@ -23,17 +22,20 @@ pub(crate) async fn run(data: Arc<RwLock<SharedData>>){
let mut login_error: bool;
let mut communities;
{
let mut shared_data = data.write().await;
let mut write = data.write().await;
// Errors during bot init are likely unrecoverable and therefore should panic the bot
// Does not really matter since the bot will get restarted anyway but this way the uptime url logs a downtime
shared_data.config = match Config::load() {
write.config = match Config::load() {
Ok(data) => data,
Err(e) => panic!("{}", e),
};
last_reload = Utc::now();
}
lemmy = match lemmy::login(&credentials, shared_data.config.instance.as_str()).await {
{
let read = data.read().await;
lemmy = match lemmy::login(&credentials, read.config.instance.as_str()).await {
Ok(data) => data,
Err(e) => panic!("{}", e),
};
@ -49,64 +51,101 @@ pub(crate) async fn run(data: Arc<RwLock<SharedData>>){
sleep(Duration::milliseconds(100).to_std().unwrap()).await;
}
{
let mut write = data.write().await;
write.start = Utc::now();
}
loop {
idle(&data).await;
let mut shared_data = data.write().await;
{
let mut write = data.write().await;
shared_data.start = Utc::now();
write.start = Utc::now();
if shared_data.start - last_reload > Duration::seconds(shared_data.config.config_reload_seconds as i64) {
shared_data.config = match Config::load() {
if write.start - last_reload > Duration::seconds(write.config.config_reload_seconds as i64) {
write.config = match Config::load() {
Ok(data) => data,
Err(e) => panic!("{}", e),
};
}
}
{
let read = data.read().await;
if login_error {
lemmy = match lemmy::login(&credentials, shared_data.config.instance.as_str()).await {
lemmy = match lemmy::login(&credentials, read.config.instance.as_str()).await {
Ok(data) => data,
Err(e) => {
shared_data.messages.push(Message::Error(format!("{}", e)));
drop(read);
let mut write = data.write().await;
write.messages.push(Message::Error(e.to_string()));
continue
}
};
login_error = false;
}
}
if shared_data.start - last_reload > Duration::seconds(shared_data.config.config_reload_seconds as i64) {
{
let read = data.read().await;
if read.start - last_reload > Duration::seconds(read.config.config_reload_seconds as i64) {
communities = match lemmy.get_communities().await {
Ok(data) => data,
Err(e) => {
login_error = true;
shared_data.messages.push(Message::Error(format!("{}", e)));
drop(read);
let mut write = data.write().await;
write.messages.push(Message::Error(e.to_string()));
continue
}
};
last_reload = Utc::now();
}
shared_data.post_history = match SeriesHistory::load_history() {
Ok(data) => data,
Err(e) => {
login_error = true;
shared_data.messages.push(Message::Warning(format!("{}", e)));
continue
}
};
for series in shared_data.config.series.clone() {
match handle_series(&series, &communities, &lemmy, &mut shared_data).await {
{
let mut write = data.write().await;
write.post_history = match SeriesHistory::load_history() {
Ok(data) => data,
Err(e) => {
login_error = true;
shared_data.messages.push(Message::Warning(format!("{}", e)));
write.messages.push(Message::Warning(e.to_string()));
continue
}
};
}
let _ = shared_data.downgrade();
{
let read = data.read().await;
let series = read.config.series.clone();
drop(read);
for series in series {
match handle_series(&series, &communities, &lemmy, &data).await {
Ok(data) => data,
Err(e) => {
login_error = true;
let mut write = data.write().await;
write.messages.push(Message::Warning(e.to_string()));
continue
}
};
}
}
let read = data.read().await;
if read.messages.len() > 15 {
let mut list = read.messages.clone();
drop(read);
list.reverse();
while list.len() > 15 {
list.pop();
}
list.reverse();
let mut write = data.write().await;
write.messages = list
}
idle(&data).await;
}
@ -124,7 +163,7 @@ async fn idle(data: &Arc<RwLock<SharedData>>) {
Ok(_) => {}
Err(e) => {
let mut write = data.write().await;
write.messages.push(Message::Error(format!("{}", e)))
write.messages.push(Message::Error(e.to_string()))
},
}
};
@ -134,26 +173,35 @@ async fn idle(data: &Arc<RwLock<SharedData>>) {
}
}
async fn handle_series<'a>(
async fn handle_series(
series: &SeriesConfig,
communities: &HashMap<String, CommunityId>,
lemmy: &Lemmy,
data: &mut RwLockWriteGuard<'a, SharedData>,
) -> Result<(), Box<dyn Error>> {
data: &Arc<RwLock<SharedData>>,
) -> Result<(), String> {
let mut post_list = match jnovel::check_feed(series.slug.as_str(), series.parted).await {
Ok(data) => data,
Err(e) => panic!("{:#?}", e),
Err(e) => {
return Err(e.to_string());
},
};
for (index, post_info) in post_list.clone().iter().enumerate() { // todo .clone() likely not needed
let post_part_info = post_info.get_part_info();
let post_lemmy_info = post_info.get_lemmy_info();
if data.post_history.check_for_post(series.slug.as_str(), post_part_info.as_string().as_str(), post_lemmy_info.title.as_str()) {
data.messages.push(Message::Info(format!("Skipping '{}' since already posted", post_lemmy_info.title)));
{
let read = data.read().await;
if read.post_history.check_for_post(series.slug.as_str(), post_part_info.as_string().as_str(), post_lemmy_info.title.as_str()) {
drop(read);
let mut write = data.write().await;
write.messages.push(Message::Info(format!("Skipping '{}' since already posted", post_lemmy_info.title)));
post_list.remove(index);
continue
}
}
let post_series_config = match post_info {
PostInfo::Chapter {..} => {&series.prepub_community},
@ -200,8 +248,10 @@ async fn handle_series<'a>(
lemmy.pin(post_id, PostFeatureType::Local).await?;
}
let mut series_history = data.post_history.get_series(series.slug.as_str());
let read = data.read().await;
let mut series_history = read.post_history.get_series(series.slug.as_str());
let mut part_history = series_history.get_part(post_part_info.as_string().as_str());
drop(read);
match post_info {
PostInfo::Chapter {..} => {
@ -213,8 +263,12 @@ async fn handle_series<'a>(
}
series_history.set_part(post_part_info.as_string().as_str(), part_history);
data.post_history.set_series(series.slug.as_str(), series_history);
let _ = data.post_history.save_history()?;
let mut write = data.write().await;
write.post_history.set_series(series.slug.as_str(), series_history);
let _ = match write.post_history.save_history() {
Ok(data) => data,
Err(e) => return Err(format!("{}", e))
};
}
Ok(())
}

View file

@ -1,4 +1,3 @@
use std::{error::Error};
use serde_derive::{Deserialize, Serialize};
use crate::config::PostBody::Description;
@ -11,8 +10,11 @@ pub(crate) struct Config {
}
impl Config {
pub(crate) fn load() -> Result<Self, Box<dyn Error>> {
let cfg: Self = confy::load_path("./config.toml")?;
pub(crate) fn load() -> Result<Self, String> {
let cfg: Self = match confy::load_path("./config.toml") {
Ok(data) => data,
Err(_) => panic!("config.toml not found!"),
};
if cfg.instance.is_empty() {
panic!("config.toml not found!")
}

View file

@ -1,6 +1,5 @@
use std::cmp::Ordering;
use std::collections::HashMap;
use std::error::Error;
use std::ops::Sub;
use chrono::{DateTime, Duration, Utc};
use serde_derive::{Deserialize, Serialize};
@ -9,7 +8,7 @@ use crate::{HTTP_CLIENT};
use crate::jnovel::PartInfo::{NoParts, Part};
use crate::jnovel::PostInfo::{Chapter, Volume};
static PAST_DAYS_ELIGIBLE: u8 = 4;
static PAST_DAYS_ELIGIBLE: u8 = 8;
macro_rules! api_url {
() => {
@ -223,15 +222,24 @@ impl PartialOrd for PostInfo {
}
}
pub(crate) async fn check_feed(series_slug: &str, series_has_parts: bool) -> Result<Vec<PostInfo>, Box<dyn Error>> {
let response = HTTP_CLIENT
pub(crate) async fn check_feed(series_slug: &str, series_has_parts: bool) -> Result<Vec<PostInfo>, String> {
let response = match HTTP_CLIENT
.get(api_url!() + "/series/" + series_slug + "/volumes?format=json")
.send()
.await?
.text()
.await?;
.await {
Ok(data) => {
match data.text().await {
Ok(data) => data,
Err(e) => return Err(format!("{}", e))
}
},
Err(e) => return Err(format!("{}", e))
};
let mut volume_brief_data: VolumesWrapper = serde_json::from_str(&response)?;
let mut volume_brief_data: VolumesWrapper = match serde_json::from_str(&response) {
Ok(data) => data,
Err(e) => return Err(format!("{}", e))
};
volume_brief_data.volumes.reverse(); // Makes breaking out of the volume loop easier
// If no parts just use 0 as Part indicator as no Series with Parts has a Part 0
@ -322,15 +330,25 @@ pub(crate) async fn check_feed(series_slug: &str, series_has_parts: bool) -> Res
Ok(result_vec)
}
async fn get_latest_prepub(volume_slug: &str) -> Result<Option<LemmyPostInfo>, Box<dyn Error>> {
let response = HTTP_CLIENT
async fn get_latest_prepub(volume_slug: &str) -> Result<Option<LemmyPostInfo>, String> {
let response = match HTTP_CLIENT
.get(api_url!() + "/volumes/" + volume_slug + "/parts?format=json")
.send()
.await?
.text()
.await?;
.await {
Ok(data) => {
match data.text().await {
Ok(data) => data,
Err(e) => return Err(e.to_string())
}
},
Err(e) => return Err(format!("{}", e))
};
let mut volume_prepub_parts_data: ChapterWrapper = serde_json::from_str(&response)?;
let mut volume_prepub_parts_data: ChapterWrapper = match serde_json::from_str(&response) {
Ok(data) => data,
Err(e) => return Err(format!("{}", e))
};
volume_prepub_parts_data.parts.reverse(); // Makes breaking out of the parts loop easier
let mut post_details: Option<LemmyPostInfo> = None;

View file

@ -1,7 +1,6 @@
use std::collections::HashMap;
use std::env;
use std::env::VarError;
use std::error::Error;
use lemmy_api_common::community::{ListCommunities};
use lemmy_api_common::person::{Login, LoginResponse};
use lemmy_api_common::post::{GetPosts};
@ -42,18 +41,21 @@ pub(crate) struct Lemmy {
instance: String,
}
pub(crate) async fn login(credentials: &Credentials, instance: &str) -> Result<Lemmy, Box<dyn Error>> {
pub(crate) async fn login(credentials: &Credentials, instance: &str) -> Result<Lemmy, String> {
let login_params = Login {
username_or_email: credentials.get_username(),
password: credentials.get_password(),
totp_2fa_token: None,
};
let response = HTTP_CLIENT
let response = match HTTP_CLIENT
.post(instance.to_string() + "/api/v3/user/login")
.json(&login_params)
.send()
.await?;
.await {
Ok(data) => data,
Err(e) => return Err(format!("{}", e))
};
match response.status() {
StatusCode::OK => {
@ -71,40 +73,55 @@ pub(crate) async fn login(credentials: &Credentials, instance: &str) -> Result<L
}
impl Lemmy {
pub(crate) async fn post(&self, post: CustomCreatePost) -> Result<PostId, Box<dyn Error>> {
let response = HTTP_CLIENT
pub(crate) async fn post(&self, post: CustomCreatePost) -> Result<PostId, String> {
let response = match HTTP_CLIENT
.post(format!("{}/api/v3/post", &self.instance))
.bearer_auth(&self.jwt_token.to_string())
.json(&post)
.send()
.await?
.text()
.await?;
.await {
Ok(data) => {
match data.text().await {
Ok(data) => data,
Err(e) => return Err(format!("{}", e))
}
},
Err(e) => return Err(format!("{}", e))
};
let json_data = serde_json::from_str::<HashMap<&str, CustomPostView>>(&response)?
.remove("post_view")
.expect("Element should be present");
let json_data = match serde_json::from_str::<HashMap<&str, CustomPostView>>(&response) {
Ok(mut data) => data.remove("post_view").expect("Element should be present"),
Err(e) => return Err(format!("{}", e))
};
Ok(json_data.post.id)
}
async fn feature(&self, params: CustomFeaturePost) -> Result<CustomPostView, Box<dyn Error>> {
let response = HTTP_CLIENT
async fn feature(&self, params: CustomFeaturePost) -> Result<CustomPostView, String> {
let response = match HTTP_CLIENT
.post(format!("{}/api/v3/post/feature", &self.instance))
.bearer_auth(&self.jwt_token.to_string())
.json(&params)
.send()
.await?
.text()
.await?;
.await {
Ok(data) => {
match data.text().await {
Ok(data) => data,
Err(e) => return Err(format!("{}", e))
}
},
Err(e) => return Err(format!("{}", e))
};
let json_data = match serde_json::from_str::<HashMap<&str, CustomPostView>>(&response) {
Ok(mut data) => data.remove("post_view").expect("Element should be present"),
Err(e) => return Err(format!("{}", e))
};
let json_data = serde_json::from_str::<HashMap<&str, CustomPostView>>(&response)?
.remove("post_view")
.expect("Element should be present");
Ok(json_data)
}
pub(crate) async fn unpin(&self, post_id: PostId, location: PostFeatureType) -> Result<CustomPostView, Box<dyn Error>> {
pub(crate) async fn unpin(&self, post_id: PostId, location: PostFeatureType) -> Result<CustomPostView, String> {
let pin_params = CustomFeaturePost {
post_id,
featured: false,
@ -113,7 +130,7 @@ impl Lemmy {
self.feature(pin_params).await
}
pub(crate) async fn pin(&self, post_id: PostId, location: PostFeatureType) -> Result<CustomPostView, Box<dyn Error>> {
pub(crate) async fn pin(&self, post_id: PostId, location: PostFeatureType) -> Result<CustomPostView, String> {
let pin_params = CustomFeaturePost {
post_id,
featured: true,
@ -122,23 +139,32 @@ impl Lemmy {
self.feature(pin_params).await
}
pub(crate) async fn get_community_pinned(&self, community: CommunityId) -> Result<Vec<CustomPostView>, Box<dyn Error>> {
pub(crate) async fn get_community_pinned(&self, community: CommunityId) -> Result<Vec<CustomPostView>, String> {
let list_params = GetPosts {
community_id: Some(community),
type_: Some(ListingType::Local),
..Default::default()
};
let response = HTTP_CLIENT
let response = match HTTP_CLIENT
.get(format!("{}/api/v3/post/list", &self.instance))
.bearer_auth(&self.jwt_token.to_string())
.query(&list_params)
.send()
.await?
.text()
.await?;
.await {
Ok(data) => {
match data.text().await {
Ok(data) => data,
Err(e) => return Err(format!("{}", e))
}
},
Err(e) => return Err(format!("{}", e))
};
let json_data: CustomGetPostsResponse = serde_json::from_str(&response)?;
let json_data: CustomGetPostsResponse = match serde_json::from_str(&response) {
Ok(data) => data,
Err(e) => return Err(format!("{}", e))
};
Ok(json_data.posts.iter().filter(|post| {
post.post.featured_community
@ -148,22 +174,31 @@ impl Lemmy {
)
}
pub(crate) async fn get_local_pinned(&self) -> Result<Vec<CustomPostView>, Box<dyn Error>> {
pub(crate) async fn get_local_pinned(&self) -> Result<Vec<CustomPostView>, String> {
let list_params = GetPosts {
type_: Some(ListingType::Local),
..Default::default()
};
let response = HTTP_CLIENT
let response = match HTTP_CLIENT
.get(format!("{}/api/v3/post/list", &self.instance))
.bearer_auth(&self.jwt_token.to_string())
.query(&list_params)
.send()
.await?
.text()
.await?;
.await {
Ok(data) => {
match data.text().await {
Ok(data) => data,
Err(e) => return Err(format!("{}", e))
}
},
Err(e) => return Err(format!("{}", e))
};
let json_data: CustomGetPostsResponse = serde_json::from_str(&response)?;
let json_data: CustomGetPostsResponse = match serde_json::from_str(&response) {
Ok(data) => data,
Err(e) => return Err(format!("{}", e))
};
Ok(json_data.posts.iter().filter(|post| {
post.post.featured_local
@ -173,22 +208,31 @@ impl Lemmy {
)
}
pub(crate) async fn get_communities(&self) -> Result<HashMap<String, CommunityId>, Box<dyn Error>> {
pub(crate) async fn get_communities(&self) -> Result<HashMap<String, CommunityId>, String> {
let list_params = ListCommunities {
type_: Some(ListingType::Local),
..Default::default()
};
let response = HTTP_CLIENT
let response = match HTTP_CLIENT
.get(format!("{}/api/v3/community/list", &self.instance))
.bearer_auth(&self.jwt_token.to_string())
.query(&list_params)
.send()
.await?
.text()
.await?;
.await {
Ok(data) => {
match data.text().await {
Ok(data) => data,
Err(e) => return Err(format!("{}", e))
}
},
Err(e) => return Err(format!("{}", e))
};
let json_data: CustomListCommunitiesResponse = serde_json::from_str(&response)?;
let json_data: CustomListCommunitiesResponse = match serde_json::from_str(&response) {
Ok(data) => data,
Err(e) => return Err(format!("{}", e))
};
let mut communities: HashMap<String, CommunityId> = HashMap::new();
for community_view in json_data.communities {

View file

@ -69,6 +69,16 @@ pub(crate) enum Message {
Error(String),
}
impl Message {
pub(crate) fn content(&self) -> String {
match self {
Message::Info(msg) => msg.clone(),
Message::Warning(msg) => msg.clone(),
Message::Error(msg) => msg.clone(),
}
}
}
#[tokio::main]
async fn main() {
dotenv().ok();

View file

@ -1,5 +1,4 @@
use std::collections::HashMap;
use std::error::Error;
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
@ -12,10 +11,13 @@ pub(crate) struct SeriesHistory {
}
impl SeriesHistory {
pub(crate) fn load_history() -> Result<Self, Box<dyn Error>> {
pub(crate) fn load_history() -> Result<Self, String> {
match Path::new("history.toml").exists() {
true => {
let file_contents: String = fs::read_to_string("history.toml")?;
let file_contents: String = match fs::read_to_string("history.toml") {
Ok(data) => data,
Err(e) => return Err(format!("{}", e)),
};
let history: Result<SeriesHistory, toml::de::Error> = match file_contents.len() {
0 => return Ok(SeriesHistory {
@ -26,7 +28,7 @@ impl SeriesHistory {
match history {
Ok(data) => Ok(data),
Err(e) => Err(Box::new(e))
Err(e) => Err(format!("{}", e))
}
},
false => {

View file

@ -79,10 +79,10 @@ async fn print_info<'a>(data: RwLockReadGuard<'a, SharedData>, min_len_series: &
});
println!("{:#<1$}", "", separator_width);
for error in data.get_messages(true, true, false).iter() {
println!("{}", error);
println!("{}", error.content());
}
println!("{:#<1$}", "", separator_width);
for message in data.get_messages(false, false, false).iter() {
println!("{}", message);
println!("{}", message.content());
}
}