use crate::{Attachment, AttachmentId, Error, Session};
use derive_more::{Display, From, FromStr, Into};
use reqwest::Method;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Debug};
#[allow(clippy::module_name_repetitions)]
#[derive(
Clone,
Copy,
Debug,
Default,
Deserialize,
Display,
Eq,
From,
FromStr,
Hash,
Into,
Ord,
PartialEq,
PartialOrd,
Serialize,
)]
#[serde(transparent)]
pub struct PostId(pub u64);
#[derive(Debug, Default)]
#[must_use]
pub struct Post {
pub adult_content: bool,
pub headline: String,
pub attachments: Vec<Attachment>,
pub markdown: String,
pub tags: Vec<String>,
pub content_warnings: Vec<String>,
pub draft: bool,
}
impl Post {
#[must_use]
pub fn is_empty(&self) -> bool {
self.attachments.is_empty() && self.headline.is_empty() && self.markdown.is_empty()
}
pub(crate) async fn send(
&mut self,
session: &Session,
method: Method,
path: &str,
project: &str,
shared_post: Option<PostId>,
) -> Result<PostId, Error> {
if self.is_empty() && shared_post.is_none() {
return Err(Error::EmptyPost);
}
if self.attachments.iter().any(Attachment::is_failed) {
return Err(Error::FailedAttachment);
}
let need_upload = self.attachments.iter().any(Attachment::is_new);
let PostResponse { post_id } = session
.client
.request(method, path)
.json(&self.as_api(need_upload, shared_post))
.send()
.await?
.error_for_status()?
.json()
.await?;
tracing::info!(%post_id);
if need_upload {
futures::future::try_join_all(
self.attachments
.iter_mut()
.map(|attachment| attachment.upload(&session.client, project, post_id)),
)
.await?;
session
.client
.put(&format!("project/{}/posts/{}", project, post_id))
.json(&self.as_api(false, shared_post))
.send()
.await?
.error_for_status()?;
}
Ok(post_id)
}
#[tracing::instrument]
fn as_api(&self, force_draft: bool, shared_post: Option<PostId>) -> ApiPost<'_> {
let mut blocks = self
.attachments
.iter()
.map(|attachment| ApiBlock::Attachment {
attachment: ApiAttachment {
alt_text: &attachment.alt_text,
attachment_id: attachment.id().unwrap_or_default(),
},
})
.collect::<Vec<_>>();
if !self.markdown.is_empty() {
for block in self.markdown.split("\n\n") {
blocks.push(ApiBlock::Markdown {
markdown: ApiMarkdown { content: block },
});
}
}
let post = ApiPost {
adult_content: self.adult_content,
blocks,
cws: &self.content_warnings,
headline: &self.headline,
post_state: if force_draft || self.draft { 0 } else { 1 },
share_of_post_id: shared_post,
tags: &self.tags,
};
tracing::debug!(?post);
post
}
}
#[allow(clippy::module_name_repetitions)]
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ApiPost<'a> {
adult_content: bool,
blocks: Vec<ApiBlock<'a>>,
cws: &'a [String],
headline: &'a str,
post_state: u64,
#[serde(skip_serializing_if = "Option::is_none")]
share_of_post_id: Option<PostId>,
tags: &'a [String],
}
impl Debug for ApiPost<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", serde_json::to_value(self).map_err(|_| fmt::Error)?)
}
}
#[derive(Serialize)]
#[serde(tag = "type", rename_all = "camelCase")]
enum ApiBlock<'a> {
Attachment { attachment: ApiAttachment<'a> },
Markdown { markdown: ApiMarkdown<'a> },
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ApiAttachment<'a> {
alt_text: &'a str,
attachment_id: AttachmentId,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ApiMarkdown<'a> {
content: &'a str,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct PostResponse {
post_id: PostId,
}