Skip to main content

fedi_post/
piefed.rs

1use log::{debug, info, warn};
2use reqwest::{Client, StatusCode, header::HeaderMap};
3use serde::Serialize;
4use serde_json::Value;
5use thiserror::Error;
6
7use crate::{ServerCredentials, ServerPost};
8
9/// The API version to use when posting to PieFed.
10pub static API_VERSION: &str = "alpha";
11
12#[derive(Debug, Serialize)]
13struct LoginRequest {
14    username: String,
15    password: String,
16}
17
18#[derive(Debug, Serialize)]
19struct CreatePostRequest {
20    title: String,
21    community_id: i64,
22    alt_text: Option<String>,
23    body: Option<String>,
24    url: Option<String>,
25    nsfw: Option<bool>,
26    ai_generated: Option<bool>,
27    language_id: Option<i64>,
28}
29
30/// Errors that can occur when posting to PieFed.
31#[derive(Debug, Error)]
32pub enum PieFedError {
33    /// An error occurred during HTTP communication with the PieFed server.
34    #[error("HTTP communication failed: {0}")]
35    ReqwestError(#[from] reqwest::Error),
36
37    /// Error while logging into PieFed.
38    #[error("Login failed. Status code {response_status} and response body {response_body:#?}")]
39    Login {
40        /// HTTP status code returned by the PieFed server.
41        response_status: StatusCode,
42        /// The HTTP response body returned by the PieFed server, parsed as JSON.
43        response_body: Value,
44    },
45
46    /// Error while finding a community in PieFed.
47    #[error(
48        "Community {community_name} not found. Status code {response_status} and response body {response_body:#?}"
49    )]
50    CommunityNotFound {
51        /// The name of the community that was searched for.
52        community_name: String,
53        /// HTTP status code returned by the PieFed server.
54        response_status: StatusCode,
55        /// The HTTP response body returned by the PieFed server, parsed as JSON.
56        response_body: Value,
57    },
58
59    /// Error while finding a language in PieFed.
60    #[error("Language {language_code} not found. Availabe languages are {available_languages:?}")]
61    LanguageNotFound {
62        /// The language code that was searched for.
63        language_code: String,
64        /// The available language codes on the server.
65        available_languages: Vec<String>,
66    },
67
68    /// Error while creating a post in PieFed.
69    #[error(
70        "Failed to create post. Status code {response_status} and response body {response_body:#?}"
71    )]
72    Post {
73        /// HTTP status code returned by the PieFed server.
74        response_status: StatusCode,
75        /// The HTTP response body returned by the PieFed server, parsed as JSON.
76        response_body: Value,
77    },
78
79    /// The community id returned by the PieFed server is not an integer.
80    #[error("Community id {community_id:#?} is not an integer.")]
81    CommunityIdNotInteger {
82        /// The offending community id value.
83        community_id: Value,
84    },
85
86    /// The all languages field returned by the PieFed server is not an array.
87    #[error("All languages field is not an array. Value: {all_languages:#?}")]
88    AllLanguagesNotArray {
89        /// The offending all languages value.
90        all_languages: Value,
91    },
92
93    /// A language code returned by the PieFed server is not a string.
94    #[error("Language code {language_code:#?} is not a string.")]
95    LanguageCodeNotString {
96        /// The offending language code value.
97        language_code: Value,
98    },
99
100    /// Fetching the site meta information from the PieFed server failed.
101    #[error(
102        "Unable to fetch site meta information. Status code {response_status} and response body {response_body:#?}"
103    )]
104    SiteRequestFailed {
105        /// HTTP status code returned by the PieFed server.
106        response_status: StatusCode,
107        /// The HTTP response body returned by the PieFed server, parsed as JSON.
108        response_body: Value,
109    },
110
111    /// The language id returned by the PieFed server is not an integer.
112    #[error("Language id {language_id:#?} is not an integer.")]
113    LanguageIdNotInteger {
114        /// The offending language id value.
115        language_id: Value,
116    },
117}
118
119pub(super) async fn post_to_piefed(
120    credentials: &ServerCredentials,
121    post: ServerPost,
122) -> Result<(), PieFedError> {
123    let client = Client::new();
124    let mut post_headers = HeaderMap::new();
125    post_headers.insert("Content-Type", "application/json".parse().unwrap());
126    post_headers.insert("Accept", "application/json".parse().unwrap());
127    let mut get_headers = HeaderMap::new();
128    get_headers.insert("Accept", "application/json".parse().unwrap());
129
130    let jwt;
131    {
132        info!("Logging into piefed at {}", credentials.domain);
133        let request_body = LoginRequest {
134            username: credentials.username.clone(),
135            password: credentials.password.clone(),
136        };
137
138        let response = client
139            .post(format!(
140                "https://{}/api/{API_VERSION}/user/login",
141                credentials.domain
142            ))
143            .headers(post_headers.clone())
144            .json(&request_body)
145            .send()
146            .await?;
147
148        debug!("Login request body: {:?}", request_body);
149        debug!("Login response: {:?}", response);
150        let status = response.status();
151        let body = response.json::<serde_json::Value>().await?;
152        debug!("Login response body: {:?}", body);
153
154        if status != 200 {
155            return Err(PieFedError::Login {
156                response_status: status,
157                response_body: body,
158            });
159        }
160
161        jwt = body["jwt"].to_string();
162        info!("JWT: {}", jwt);
163    }
164
165    post_headers.insert("Authorization", format!("Bearer {}", jwt).parse().unwrap());
166    get_headers.insert("Authorization", format!("Bearer {}", jwt).parse().unwrap());
167
168    let community_id;
169    {
170        info!("Getting community id for {}", post.community);
171        let response = client
172            .get(format!(
173                "https://{}/api/{API_VERSION}/community?name={}",
174                credentials.domain, post.community,
175            ))
176            .headers(get_headers.clone())
177            .send()
178            .await?;
179
180        debug!("GetCommunity response: {:?}", response);
181        let status = response.status();
182        let body = response.json::<serde_json::Value>().await?;
183        debug!("GetCommunity response body: {:?}", body);
184
185        if status != 200 {
186            return Err(PieFedError::CommunityNotFound {
187                community_name: post.community.clone(),
188                response_status: status,
189                response_body: body,
190            });
191        }
192
193        community_id = body["community_view"]["community"]["id"]
194            .as_i64()
195            .ok_or_else(|| PieFedError::CommunityIdNotInteger {
196                community_id: body["community_view"]["community"]["id"].clone(),
197            })?;
198        info!("Community id for {} is {}", post.community, community_id,);
199    }
200
201    let language_id;
202    if let Some(language_code) = &post.language {
203        info!("Getting language id for {}", language_code);
204        let response = client
205            .get(format!(
206                "https://{}/api/{API_VERSION}/site",
207                credentials.domain
208            ))
209            .headers(get_headers.clone())
210            .send()
211            .await?;
212
213        debug!("GetSite response: {:?}", response);
214        let status = response.status();
215        let body = response.json::<serde_json::Value>().await?;
216        debug!("GetSite response body: {:?}", body);
217
218        if status != 200 {
219            return Err(PieFedError::SiteRequestFailed {
220                response_status: status,
221                response_body: body,
222            });
223        }
224
225        let all_languages = body["site"]["all_languages"].as_array().ok_or_else(|| {
226            PieFedError::AllLanguagesNotArray {
227                all_languages: body["site"]["all_languages"].clone(),
228            }
229        })?;
230        let language_id_value = &all_languages
231            .iter()
232            .find(|language| language["code"].as_str() == Some(language_code.as_str()))
233            .ok_or_else(|| PieFedError::LanguageNotFound {
234                language_code: language_code.clone(),
235                available_languages: all_languages
236                    .iter()
237                    .map(|language| language["code"].to_string())
238                    .collect(),
239            })?["id"];
240
241        language_id =
242            Some(
243                language_id_value
244                    .as_i64()
245                    .ok_or_else(|| PieFedError::LanguageIdNotInteger {
246                        language_id: language_id_value.clone(),
247                    })?,
248            );
249        info!(
250            "Language id for {} is {}",
251            language_code,
252            language_id.unwrap(),
253        );
254    } else {
255        info!("No language specified, skipping getting language id");
256        language_id = None;
257    }
258
259    {
260        info!("Posting to PieFed");
261        debug!("Post data: {:?}", post);
262
263        let ServerPost {
264            community: _community,
265            title,
266            url,
267            body,
268            language: _language,
269            alt_text,
270            nsfw,
271            nsfl,
272            ai_generated,
273            custom_thumbnail,
274        } = post;
275
276        let request_body = CreatePostRequest {
277            title,
278            community_id,
279            alt_text,
280            body,
281            url,
282            nsfw,
283            ai_generated,
284            language_id,
285        };
286
287        if let Some(nsfl) = nsfl {
288            warn!(
289                "The nsfl field is set to {nsfl}, but PieFed does not support NSFL. Ignoring this field."
290            );
291        }
292        if let Some(custom_thumbnail) = custom_thumbnail {
293            warn!(
294                "The custom_thumbnail field is set to {custom_thumbnail}, but PieFed does not support custom thumbnails. Ignoring this field."
295            );
296        }
297
298        let response = client
299            .post(format!(
300                "https://{}/api/{API_VERSION}/post",
301                credentials.domain
302            ))
303            .headers(post_headers.clone())
304            .json(&request_body)
305            .send()
306            .await?;
307
308        debug!("CreatePost request body: {:?}", request_body);
309        debug!("CreatePost response: {:?}", response);
310        let status = response.status();
311        let body = response.json::<serde_json::Value>().await.unwrap();
312        info!("CreatePost response body: {:?}", body);
313
314        if status != 200 {
315            return Err(PieFedError::Post {
316                response_status: status,
317                response_body: body,
318            });
319        }
320
321        info!("Post created successfully");
322        Ok(())
323    }
324}