1use serde::{Deserialize, Serialize};
2use serde_json::json;
3use url::Url;
4
5use crate::{auth::TokenProvider, client::Client, Error};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct PostResponse {
10 pub id: String,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct TextPost {
17 pub author_urn: String,
19 pub text: String,
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub link: Option<url::Url>,
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub visibility: Option<String>,
27}
28
29impl TextPost {
30 pub fn new(author_urn: impl Into<String>, text: impl Into<String>) -> Self {
32 Self {
33 author_urn: author_urn.into(),
34 text: text.into(),
35 link: None,
36 visibility: None,
37 }
38 }
39 pub fn with_link(mut self, link: Url) -> Self {
41 self.link = Some(link);
42 self
43 }
44 pub fn with_visibility(mut self, vis: impl Into<String>) -> Self {
46 self.visibility = Some(vis.into());
47 self
48 }
49}
50
51pub struct PostsClient<TP: TokenProvider> {
53 inner: Client<TP>,
54}
55
56impl<TP: TokenProvider> PostsClient<TP> {
57 #[must_use]
59 pub fn new(inner: Client<TP>) -> Self {
60 Self { inner }
61 }
62
63 pub async fn create_text_post(&self, post: &TextPost) -> Result<PostResponse, Error> {
69 let url = self
71 .inner
72 .base()
73 .join("rest/posts")
74 .map_err(|e| Error::Config(format!("invalid base url: {e}")))?;
75
76 let mut commentary = post.text.trim().to_string();
78 if let Some(link) = &post.link {
79 if !commentary.is_empty() {
80 commentary.push_str("\n\n");
81 }
82 commentary.push_str(link.as_str());
83 }
84
85 let body = json!({
87 "author": post.author_urn,
88 "commentary": commentary,
89 "visibility": post.visibility.clone().unwrap_or_else(|| "PUBLIC".to_string()),
90 "distribution": { "feedDistribution": "MAIN_FEED" },
91 "lifecycleState": "PUBLISHED",
92 "isReshareDisabledByAuthor": false
93 });
94
95 let mut headers = self.inner.auth_headers()?;
97 headers.insert("X-Restli-Protocol-Version", "2.0.0".parse().unwrap());
98 headers.insert("Content-Type", "application/json".parse().unwrap());
99
100 let resp = self
102 .inner
103 .http()
104 .post(url)
105 .headers(headers)
106 .json(&body)
107 .send()
108 .await?;
109
110 if resp.status().is_success() || resp.status().as_u16() == 201 {
111 let id = resp
113 .headers()
114 .get("x-restli-id")
115 .and_then(|v| v.to_str().ok())
116 .map(|s| s.to_string())
117 .or_else(|| {
118 resp.headers()
119 .get("location")
120 .and_then(|v| v.to_str().ok())
121 .map(|s| s.to_string())
122 })
123 .unwrap_or_else(|| "created".to_string());
124 return Ok(PostResponse { id });
125 }
126
127 if resp.status().as_u16() == 429 {
128 let retry_after = resp
129 .headers()
130 .get("retry-after")
131 .and_then(|v| v.to_str().ok())
132 .and_then(|s| s.parse::<u64>().ok())
133 .unwrap_or(0);
134 return Err(Error::RateLimited { retry_after });
135 }
136
137 let status = resp.status().as_u16();
138 let message = resp.text().await.unwrap_or_default();
139 Err(Error::Api { status, message })
140 }
141}