use log::{debug, info, warn};
use reqwest::{Client, StatusCode, header::HeaderMap};
use serde::Serialize;
use serde_json::Value;
use thiserror::Error;
use crate::{ServerCredentials, ServerPost};
pub static API_VERSION: &str = "alpha";
#[derive(Debug, Serialize)]
struct LoginRequest {
username: String,
password: String,
}
#[derive(Debug, Serialize)]
struct CreatePostRequest {
title: String,
community_id: i64,
alt_text: Option<String>,
body: Option<String>,
url: Option<String>,
nsfw: Option<bool>,
ai_generated: Option<bool>,
language_id: Option<i64>,
}
#[derive(Debug, Error)]
pub enum PieFedError {
#[error("HTTP communication failed: {0}")]
ReqwestError(#[from] reqwest::Error),
#[error("Login failed. Status code {response_status} and response body {response_body:#?}")]
Login {
response_status: StatusCode,
response_body: Value,
},
#[error(
"Community {community_name} not found. Status code {response_status} and response body {response_body:#?}"
)]
CommunityNotFound {
community_name: String,
response_status: StatusCode,
response_body: Value,
},
#[error("Language {language_code} not found. Availabe languages are {available_languages:?}")]
LanguageNotFound {
language_code: String,
available_languages: Vec<String>,
},
#[error(
"Failed to create post. Status code {response_status} and response body {response_body:#?}"
)]
Post {
response_status: StatusCode,
response_body: Value,
},
#[error("Community id {community_id:#?} is not an integer.")]
CommunityIdNotInteger {
community_id: Value,
},
#[error("All languages field is not an array. Value: {all_languages:#?}")]
AllLanguagesNotArray {
all_languages: Value,
},
#[error("Language code {language_code:#?} is not a string.")]
LanguageCodeNotString {
language_code: Value,
},
#[error(
"Unable to fetch site meta information. Status code {response_status} and response body {response_body:#?}"
)]
SiteRequestFailed {
response_status: StatusCode,
response_body: Value,
},
#[error("Language id {language_id:#?} is not an integer.")]
LanguageIdNotInteger {
language_id: Value,
},
}
pub(super) async fn post_to_piefed(
credentials: &ServerCredentials,
post: ServerPost,
) -> Result<(), PieFedError> {
let client = Client::new();
let mut post_headers = HeaderMap::new();
post_headers.insert("Content-Type", "application/json".parse().unwrap());
post_headers.insert("Accept", "application/json".parse().unwrap());
let mut get_headers = HeaderMap::new();
get_headers.insert("Accept", "application/json".parse().unwrap());
let jwt;
{
info!("Logging into piefed at {}", credentials.domain);
let request_body = LoginRequest {
username: credentials.username.clone(),
password: credentials.password.clone(),
};
let response = client
.post(format!(
"https://{}/api/{API_VERSION}/user/login",
credentials.domain
))
.headers(post_headers.clone())
.json(&request_body)
.send()
.await?;
debug!("Login request body: {:?}", request_body);
debug!("Login response: {:?}", response);
let status = response.status();
let body = response.json::<serde_json::Value>().await?;
debug!("Login response body: {:?}", body);
if status != 200 {
return Err(PieFedError::Login {
response_status: status,
response_body: body,
});
}
jwt = body["jwt"].to_string();
info!("JWT: {}", jwt);
}
post_headers.insert("Authorization", format!("Bearer {}", jwt).parse().unwrap());
get_headers.insert("Authorization", format!("Bearer {}", jwt).parse().unwrap());
let community_id;
{
info!("Getting community id for {}", post.community);
let response = client
.get(format!(
"https://{}/api/{API_VERSION}/community?name={}",
credentials.domain, post.community,
))
.headers(get_headers.clone())
.send()
.await?;
debug!("GetCommunity response: {:?}", response);
let status = response.status();
let body = response.json::<serde_json::Value>().await?;
debug!("GetCommunity response body: {:?}", body);
if status != 200 {
return Err(PieFedError::CommunityNotFound {
community_name: post.community.clone(),
response_status: status,
response_body: body,
});
}
community_id = body["community_view"]["community"]["id"]
.as_i64()
.ok_or_else(|| PieFedError::CommunityIdNotInteger {
community_id: body["community_view"]["community"]["id"].clone(),
})?;
info!("Community id for {} is {}", post.community, community_id,);
}
let language_id;
if let Some(language_code) = &post.language {
info!("Getting language id for {}", language_code);
let response = client
.get(format!(
"https://{}/api/{API_VERSION}/site",
credentials.domain
))
.headers(get_headers.clone())
.send()
.await?;
debug!("GetSite response: {:?}", response);
let status = response.status();
let body = response.json::<serde_json::Value>().await?;
debug!("GetSite response body: {:?}", body);
if status != 200 {
return Err(PieFedError::SiteRequestFailed {
response_status: status,
response_body: body,
});
}
let all_languages = body["site"]["all_languages"].as_array().ok_or_else(|| {
PieFedError::AllLanguagesNotArray {
all_languages: body["site"]["all_languages"].clone(),
}
})?;
let language_id_value = &all_languages
.iter()
.find(|language| language["code"].as_str() == Some(language_code.as_str()))
.ok_or_else(|| PieFedError::LanguageNotFound {
language_code: language_code.clone(),
available_languages: all_languages
.iter()
.map(|language| language["code"].to_string())
.collect(),
})?["id"];
language_id =
Some(
language_id_value
.as_i64()
.ok_or_else(|| PieFedError::LanguageIdNotInteger {
language_id: language_id_value.clone(),
})?,
);
info!(
"Language id for {} is {}",
language_code,
language_id.unwrap(),
);
} else {
info!("No language specified, skipping getting language id");
language_id = None;
}
{
info!("Posting to PieFed");
debug!("Post data: {:?}", post);
let ServerPost {
community: _community,
title,
url,
body,
language: _language,
alt_text,
nsfw,
nsfl,
ai_generated,
custom_thumbnail,
} = post;
let request_body = CreatePostRequest {
title,
community_id,
alt_text,
body,
url,
nsfw,
ai_generated,
language_id,
};
if let Some(nsfl) = nsfl {
warn!(
"The nsfl field is set to {nsfl}, but PieFed does not support NSFL. Ignoring this field."
);
}
if let Some(custom_thumbnail) = custom_thumbnail {
warn!(
"The custom_thumbnail field is set to {custom_thumbnail}, but PieFed does not support custom thumbnails. Ignoring this field."
);
}
let response = client
.post(format!(
"https://{}/api/{API_VERSION}/post",
credentials.domain
))
.headers(post_headers.clone())
.json(&request_body)
.send()
.await?;
debug!("CreatePost request body: {:?}", request_body);
debug!("CreatePost response: {:?}", response);
let status = response.status();
let body = response.json::<serde_json::Value>().await.unwrap();
info!("CreatePost response body: {:?}", body);
if status != 200 {
return Err(PieFedError::Post {
response_status: status,
response_body: body,
});
}
info!("Post created successfully");
Ok(())
}
}