use std::cmp::Ordering; use crate::config::{Config, PostBody, PostConfig, SeriesConfig}; use crate::{HTTP_CLIENT}; use lemmy_api_common::community::{ListCommunities, ListCommunitiesResponse}; use lemmy_api_common::lemmy_db_views::structs::PostView; use lemmy_api_common::person::{Login, LoginResponse}; use lemmy_api_common::post::{CreatePost, FeaturePost, GetPosts, GetPostsResponse}; use lemmy_api_common::sensitive::Sensitive; use lemmy_db_schema::newtypes::{CommunityId, LanguageId, PostId}; use lemmy_db_schema::{ListingType, PostFeatureType}; use reqwest::StatusCode; use std::collections::HashMap; use std::sync::{RwLock}; use serde::{Deserialize, Serialize}; use url::Url; use systemd_journal_logger::connected_to_journal; macro_rules! error { ($msg:tt) => { match connected_to_journal() { true => log::error!("[ERROR] {}", $msg), false => eprintln!("[ERROR] {}", $msg), } }; } pub(crate) struct Lemmy { jwt_token: Sensitive, instance: String, communities: HashMap, } #[derive(Debug, Clone)] pub(crate) struct PostInfoInner { pub(crate) title: String, pub(crate) url: Url, } #[derive(Debug, Copy, Clone)] pub(crate) enum PartInfo { NoParts, Part(u8), } impl PartInfo { pub(crate) fn as_u8(&self) -> u8 { match self { PartInfo::Part(number) => *number, PartInfo::NoParts => 0, } } pub(crate) fn as_string(&self) -> String { self.as_u8().to_string() } } impl PartialEq for PartInfo { fn eq(&self, other: &Self) -> bool { let self_numeric = self.as_u8(); let other_numeric = other.as_u8(); self_numeric == other_numeric } } impl PartialOrd for PartInfo { fn partial_cmp(&self, other: &Self) -> Option { if self.gt(other) { Some(Ordering::Greater) } else if self.eq(other) { Some(Ordering::Equal) } else { Some(Ordering::Less) } } fn lt(&self, other: &Self) -> bool { let self_numeric = self.as_u8(); let other_numeric = other.as_u8(); self_numeric < other_numeric } fn le(&self, other: &Self) -> bool { !self.gt(other) } fn gt(&self, other: &Self) -> bool { let self_numeric = self.as_u8(); let other_numeric = other.as_u8(); self_numeric > other_numeric } fn ge(&self, other: &Self) -> bool { !self.lt(other) } } #[derive(Debug, Clone, Copy)] pub(crate) enum PostType { Chapter, Volume } #[derive(Debug, Clone)] pub(crate) struct PostInfo { pub(crate) part: Option, pub(crate) lemmy_info: PostInfoInner, pub(crate) description: Option, pub(crate) post_type: Option } impl PostInfo { pub(crate)fn get_info(&self) -> PostInfoInner { self.lemmy_info.clone() } pub(crate)fn get_description(&self) -> Option { self.description.clone() } pub(crate) fn get_part_info(&self) -> Option { self.part } pub(crate) fn get_post_config(&self, series: &SeriesConfig) -> PostConfig { match self.post_type { Some(post_type) => { match post_type { PostType::Chapter => series.prepub_community.clone(), PostType::Volume => series.volume_community.clone(), } } None => series.prepub_community.clone(), } } pub(crate) fn get_post_data(&self, series: &SeriesConfig, lemmy: &Lemmy) -> CreatePost { let post_config = self.get_post_config(series); let post_body = match &post_config.post_body { PostBody::None => None, PostBody::Description => self.get_description(), PostBody::Custom(text) => Some(text.clone()), }; let community_id: CommunityId = lemmy.get_community_id(&post_config.name); CreatePost { name: self.get_info().title.clone(), community_id, url: Some(self.get_info().url), body: post_body, honeypot: None, nsfw: None, 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 } } } impl PartialEq for PostInfo { fn eq(&self, other: &Self) -> bool { self.part.eq(&other.part) } } impl PartialOrd for PostInfo { fn partial_cmp(&self, other: &Self) -> Option { if self.gt(other) { Some(Ordering::Greater) } else if self.eq(other) { Some(Ordering::Equal) } else { Some(Ordering::Less) } } fn lt(&self, other: &Self) -> bool { self.part < other.part } fn le(&self, other: &Self) -> bool { !self.gt(other) } fn gt(&self, other: &Self) -> bool { self.part > other.part } fn ge(&self, other: &Self) -> bool { !self.lt(other) } } impl Lemmy { pub(crate) fn get_community_id(&self, name: &str) -> CommunityId { *self.communities.get(name).expect("Given community is invalid") } pub(crate) async fn new(config: &RwLock) -> Result { let read_config = config.read().expect("Read Lock Failed").clone(); let login_params = Login { username_or_email: read_config.get_username(), password: read_config.get_password(), totp_2fa_token: None, }; let response = match HTTP_CLIENT .post(read_config.instance.to_owned() + "/api/v3/user/login") .json(&login_params) .send() .await { Ok(data) => data, Err(e) => { let err_msg = format!("{e}"); error!(err_msg); return Err(()); } }; match response.status() { StatusCode::OK => { let data: LoginResponse = response .json() .await .expect("Successful Login Request should return JSON"); match data.jwt { Some(token) => Ok(Lemmy { jwt_token: token.clone(), instance: read_config.instance.to_owned(), communities: HashMap::new(), }), None => { let err_msg = "Login did not return JWT token. Are the credentials valid?".to_owned(); error!(err_msg); Err(()) } } } status => { let err_msg = format!("Unexpected HTTP Status '{}' during Login", status); error!(err_msg); Err(()) } } } pub(crate) async fn logout(&self) { let _ = self.post_data_json("/api/v3/user/logout", &"").await; } pub(crate) async fn post(&self, post: CreatePost) -> Result { let response: String = self.post_data_json("/api/v3/post", &post).await?; let json_data: PostView = self.parse_json_map(&response).await?; Ok(json_data.post.id) } async fn feature(&self, params: FeaturePost) -> Result { let response: String = self.post_data_json("/api/v3/post/feature", ¶ms).await?; let json_data: PostView = self.parse_json_map(&response).await?; Ok(json_data) } pub(crate) async fn unpin(&self, post_id: PostId, location: PostFeatureType) -> Result { let pin_params = FeaturePost { post_id, featured: false, feature_type: location, }; self.feature(pin_params).await } pub(crate) async fn pin(&self, post_id: PostId, location: PostFeatureType) -> Result { let pin_params = FeaturePost { post_id, featured: true, feature_type: location, }; self.feature(pin_params).await } pub(crate) async fn get_community_pinned(&self, community: CommunityId) -> Result, ()> { let list_params = GetPosts { community_id: Some(community), type_: Some(ListingType::Local), ..Default::default() }; let response: String = self.get_data_query("/api/v3/post/list", &list_params).await?; let json_data: GetPostsResponse = self.parse_json(&response).await?; Ok(json_data .posts .iter() .filter(|post| post.post.featured_community) .cloned() .collect()) } pub(crate) async fn get_local_pinned(&self) -> Result, ()> { let list_params = GetPosts { type_: Some(ListingType::Local), ..Default::default() }; let response: String = self.get_data_query("/api/v3/post/list", &list_params).await?; let json_data: GetPostsResponse = self.parse_json(&response).await?; Ok(json_data .posts .iter() .filter(|post| post.post.featured_local) .cloned() .collect()) } pub(crate) async fn get_communities(&mut self) { let list_params = ListCommunities { type_: Some(ListingType::Local), ..Default::default() }; let response: String = match self.get_data_query("/api/v3/community/list", &list_params).await { Ok(data) => data, Err(_) => { error!("Unable to extract data from request"); return; } }; let json_data: ListCommunitiesResponse = match self.parse_json::(&response).await { Ok(data) => data, Err(_) => { error!("Unable to parse data from json"); return; }, }; let mut communities: HashMap = HashMap::new(); for community_view in json_data.communities { let community = community_view.community; communities.insert(community.name, community.id); } self.communities = communities; } async fn post_data_json(&self, route: &str, json: &T ) -> Result { let res = HTTP_CLIENT .post(format!("{}{route}", &self.instance)) .bearer_auth(&self.jwt_token.to_string()) .json(&json) .send() .await; self.extract_data(res).await } async fn get_data_query(&self, route: &str, param: &T ) -> Result { let res = HTTP_CLIENT .get(format!("{}{route}", &self.instance)) .bearer_auth(&self.jwt_token.to_string()) .query(¶m) .send() .await; self.extract_data(res).await } async fn extract_data(&self, response: Result) -> Result { match response { Ok(data) => match data.text().await { Ok(data) => Ok(data), Err(e) => { let err_msg = format!("{e}"); error!(err_msg); Err(()) } }, Err(e) => { let err_msg = format!("{e}"); error!(err_msg); Err(()) } } } async fn parse_json<'a, T: Deserialize<'a>>(&self, response: &'a str) -> Result { match serde_json::from_str::(response) { Ok(data) => Ok(data), Err(e) => { let err_msg = format!("{e} while parsing JSON"); error!(err_msg); Err(()) } } } async fn parse_json_map<'a, T: Deserialize<'a>>(&self, response: &'a str) -> Result { match serde_json::from_str::>(response) { Ok(mut data) => Ok(data.remove("post_view").expect("Element should be present")), Err(e) => { let err_msg = format!("{e} while parsing JSON HashMap"); error!(err_msg); Err(()) } } } }