412 lines
12 KiB
Rust
412 lines
12 KiB
Rust
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<String>,
|
|
instance: String,
|
|
communities: HashMap<String, CommunityId>,
|
|
}
|
|
|
|
|
|
#[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<Ordering> {
|
|
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<PartInfo>,
|
|
pub(crate) lemmy_info: PostInfoInner,
|
|
pub(crate) description: Option<String>,
|
|
pub(crate) post_type: Option<PostType>
|
|
}
|
|
|
|
impl PostInfo {
|
|
pub(crate)fn get_info(&self) -> PostInfoInner {
|
|
self.lemmy_info.clone()
|
|
}
|
|
|
|
pub(crate)fn get_description(&self) -> Option<String> {
|
|
self.description.clone()
|
|
}
|
|
|
|
pub(crate) fn get_part_info(&self) -> Option<PartInfo> {
|
|
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<Ordering> {
|
|
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<Config>) -> Result<Self, ()> {
|
|
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<PostId, ()> {
|
|
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<PostView, ()> {
|
|
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<PostView, ()> {
|
|
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<PostView, ()> {
|
|
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<Vec<PostView>, ()> {
|
|
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<Vec<PostView>, ()> {
|
|
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::<ListCommunitiesResponse>(&response).await {
|
|
Ok(data) => data,
|
|
Err(_) => {
|
|
error!("Unable to parse data from json");
|
|
return;
|
|
},
|
|
};
|
|
|
|
let mut communities: HashMap<String, CommunityId> = 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<T: Serialize>(&self, route: &str, json: &T ) -> Result<String,()> {
|
|
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<T: Serialize>(&self, route: &str, param: &T ) -> Result<String,()> {
|
|
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<reqwest::Response, reqwest::Error>) -> Result<String,()> {
|
|
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<T,()> {
|
|
match serde_json::from_str::<T>(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<T,()> {
|
|
match serde_json::from_str::<HashMap<&str, T>>(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(())
|
|
}
|
|
}
|
|
}
|
|
}
|