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
9pub 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#[derive(Debug, Error)]
32pub enum PieFedError {
33 #[error("HTTP communication failed: {0}")]
35 ReqwestError(#[from] reqwest::Error),
36
37 #[error("Login failed. Status code {response_status} and response body {response_body:#?}")]
39 Login {
40 response_status: StatusCode,
42 response_body: Value,
44 },
45
46 #[error(
48 "Community {community_name} not found. Status code {response_status} and response body {response_body:#?}"
49 )]
50 CommunityNotFound {
51 community_name: String,
53 response_status: StatusCode,
55 response_body: Value,
57 },
58
59 #[error("Language {language_code} not found. Availabe languages are {available_languages:?}")]
61 LanguageNotFound {
62 language_code: String,
64 available_languages: Vec<String>,
66 },
67
68 #[error(
70 "Failed to create post. Status code {response_status} and response body {response_body:#?}"
71 )]
72 Post {
73 response_status: StatusCode,
75 response_body: Value,
77 },
78
79 #[error("Community id {community_id:#?} is not an integer.")]
81 CommunityIdNotInteger {
82 community_id: Value,
84 },
85
86 #[error("All languages field is not an array. Value: {all_languages:#?}")]
88 AllLanguagesNotArray {
89 all_languages: Value,
91 },
92
93 #[error("Language code {language_code:#?} is not a string.")]
95 LanguageCodeNotString {
96 language_code: Value,
98 },
99
100 #[error(
102 "Unable to fetch site meta information. Status code {response_status} and response body {response_body:#?}"
103 )]
104 SiteRequestFailed {
105 response_status: StatusCode,
107 response_body: Value,
109 },
110
111 #[error("Language id {language_id:#?} is not an integer.")]
113 LanguageIdNotInteger {
114 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}