1use std::sync::atomic::{AtomicU64, Ordering};
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use crate::dto::topic::{
9 CreatePostResponseDTO, CreateReplyDTO, CreateTopicDTO, PostSubmissionDTO, TopicDetailDTO,
10 TopicListItemDTO, TopicPostDTO, TopicRecommendationsDTO,
11};
12use crate::dto::user::CursorListDTO;
13use crate::{Error, PickcatAccound};
14
15pub trait TopicBehavior {
18 fn list_topics(
21 &self,
22 limit: Option<u32>,
23 tags: &[String],
24 tag_mode: Option<&str>,
25 cursor: Option<&str>,
26 ) -> impl std::future::Future<Output = Result<CursorListDTO<TopicListItemDTO>, Error>> + Send;
27
28 fn get_topic_recommendations(
30 &self,
31 sort: Option<&str>,
32 limit: Option<u32>,
33 cursor: Option<&str>,
34 ) -> impl std::future::Future<Output = Result<TopicRecommendationsDTO, Error>> + Send;
35
36 fn get_topic(
38 &self,
39 topic_id: &str,
40 ) -> impl std::future::Future<Output = Result<TopicDetailDTO, Error>> + Send;
41
42 fn get_topic_posts(
44 &self,
45 topic_id: &str,
46 limit: Option<u32>,
47 sort: Option<&str>,
48 cursor: Option<&str>,
49 ) -> impl std::future::Future<Output = Result<CursorListDTO<TopicPostDTO>, Error>> + Send;
50
51 fn create_topic(
56 &self,
57 idempotency_key: &str,
58 title: &str,
59 markdown: &str,
60 kind: &str,
61 tag_ids: Vec<String>,
62 ) -> impl std::future::Future<Output = Result<CreatePostResponseDTO, Error>> + Send;
63
64 fn create_reply(
66 &self,
67 idempotency_key: &str,
68 topic_id: &str,
69 markdown: &str,
70 reply_to_post_number: Option<u64>,
71 ) -> impl std::future::Future<Output = Result<CreatePostResponseDTO, Error>> + Send;
72
73 fn list_post_submissions(
75 &self,
76 content_role: Option<&str>,
77 status: Option<&str>,
78 topic_id: Option<&str>,
79 limit: Option<u32>,
80 cursor: Option<&str>,
81 ) -> impl std::future::Future<Output = Result<CursorListDTO<PostSubmissionDTO>, Error>> + Send;
82
83 fn get_post_submission(
85 &self,
86 submission_id: &str,
87 ) -> impl std::future::Future<Output = Result<PostSubmissionDTO, Error>> + Send;
88}
89
90pub fn generate_idempotency_key() -> String {
94 static COUNTER: AtomicU64 = AtomicU64::new(0);
95
96 let nanos = SystemTime::now()
97 .duration_since(UNIX_EPOCH)
98 .map(|d| d.as_nanos())
99 .unwrap_or(0);
100 let count = u128::from(COUNTER.fetch_add(1, Ordering::Relaxed));
101 let value = nanos ^ (count << 64);
102
103 let mut hex: Vec<char> = format!("{value:032x}").chars().collect();
104 hex[12] = '4'; hex[16] = '8'; let hex: String = hex.into_iter().collect();
107
108 format!(
109 "{}-{}-{}-{}-{}",
110 &hex[0..8],
111 &hex[8..12],
112 &hex[12..16],
113 &hex[16..20],
114 &hex[20..32]
115 )
116}
117
118fn push_query(params: &mut Vec<(&str, String)>, key: &'static str, value: Option<String>) {
120 if let Some(value) = value {
121 params.push((key, value));
122 }
123}
124
125impl TopicBehavior for PickcatAccound {
126 async fn list_topics(
127 &self,
128 limit: Option<u32>,
129 tags: &[String],
130 tag_mode: Option<&str>,
131 cursor: Option<&str>,
132 ) -> Result<CursorListDTO<TopicListItemDTO>, Error> {
133 let mut params: Vec<(&str, String)> = Vec::new();
134 push_query(&mut params, "limit", limit.map(|v| v.to_string()));
135 for tag in tags {
136 params.push(("tag", tag.clone()));
137 }
138 push_query(&mut params, "tagMode", tag_mode.map(str::to_string));
139 push_query(&mut params, "cursor", cursor.map(str::to_string));
140
141 let mut request = self.client.get(format!("{}/api/v1/topics", self.base_url));
142 if !params.is_empty() {
143 request = request.query(¶ms);
144 }
145 Ok(request
146 .send()
147 .await?
148 .json::<CursorListDTO<TopicListItemDTO>>()
149 .await?)
150 }
151
152 async fn get_topic_recommendations(
153 &self,
154 sort: Option<&str>,
155 limit: Option<u32>,
156 cursor: Option<&str>,
157 ) -> Result<TopicRecommendationsDTO, Error> {
158 let mut params: Vec<(&str, String)> = Vec::new();
159 push_query(&mut params, "sort", sort.map(str::to_string));
160 push_query(&mut params, "limit", limit.map(|v| v.to_string()));
161 push_query(&mut params, "cursor", cursor.map(str::to_string));
162
163 let mut request = self
164 .client
165 .get(format!("{}/api/v1/topic-recommendations", self.base_url));
166 if !params.is_empty() {
167 request = request.query(¶ms);
168 }
169 Ok(request
170 .send()
171 .await?
172 .json::<TopicRecommendationsDTO>()
173 .await?)
174 }
175
176 async fn get_topic(&self, topic_id: &str) -> Result<TopicDetailDTO, Error> {
177 Ok(self
178 .client
179 .get(format!("{}/api/v1/topics/{}", self.base_url, topic_id))
180 .send()
181 .await?
182 .json::<TopicDetailDTO>()
183 .await?)
184 }
185
186 async fn get_topic_posts(
187 &self,
188 topic_id: &str,
189 limit: Option<u32>,
190 sort: Option<&str>,
191 cursor: Option<&str>,
192 ) -> Result<CursorListDTO<TopicPostDTO>, Error> {
193 let mut params: Vec<(&str, String)> = Vec::new();
194 push_query(&mut params, "limit", limit.map(|v| v.to_string()));
195 push_query(&mut params, "sort", sort.map(str::to_string));
196 push_query(&mut params, "cursor", cursor.map(str::to_string));
197
198 let mut request = self.client.get(format!(
199 "{}/api/v1/topics/{}/posts",
200 self.base_url, topic_id
201 ));
202 if !params.is_empty() {
203 request = request.query(¶ms);
204 }
205 Ok(request
206 .send()
207 .await?
208 .json::<CursorListDTO<TopicPostDTO>>()
209 .await?)
210 }
211
212 async fn create_topic(
213 &self,
214 idempotency_key: &str,
215 title: &str,
216 markdown: &str,
217 kind: &str,
218 tag_ids: Vec<String>,
219 ) -> Result<CreatePostResponseDTO, Error> {
220 let dto = CreateTopicDTO {
221 title: title.to_string(),
222 markdown: markdown.to_string(),
223 kind: kind.to_string(),
224 tag_ids,
225 };
226 Ok(self
227 .client
228 .post(format!("{}/api/v1/posts", self.base_url))
229 .header("Idempotency-Key", idempotency_key)
230 .header("origin", self.base_url.clone())
231 .json(&dto)
232 .send()
233 .await?
234 .json::<CreatePostResponseDTO>()
235 .await?)
236 }
237
238 async fn create_reply(
239 &self,
240 idempotency_key: &str,
241 topic_id: &str,
242 markdown: &str,
243 reply_to_post_number: Option<u64>,
244 ) -> Result<CreatePostResponseDTO, Error> {
245 let dto = CreateReplyDTO {
246 topic_id: topic_id.to_string(),
247 markdown: markdown.to_string(),
248 reply_to_post_number,
249 };
250 Ok(self
251 .client
252 .post(format!("{}/api/v1/posts", self.base_url))
253 .header("Idempotency-Key", idempotency_key)
254 .header("origin", self.base_url.clone())
255 .json(&dto)
256 .send()
257 .await?
258 .json::<CreatePostResponseDTO>()
259 .await?)
260 }
261
262 async fn list_post_submissions(
263 &self,
264 content_role: Option<&str>,
265 status: Option<&str>,
266 topic_id: Option<&str>,
267 limit: Option<u32>,
268 cursor: Option<&str>,
269 ) -> Result<CursorListDTO<PostSubmissionDTO>, Error> {
270 let mut params: Vec<(&str, String)> = Vec::new();
271 push_query(&mut params, "contentRole", content_role.map(str::to_string));
272 push_query(&mut params, "status", status.map(str::to_string));
273 push_query(&mut params, "topicId", topic_id.map(str::to_string));
274 push_query(&mut params, "limit", limit.map(|v| v.to_string()));
275 push_query(&mut params, "cursor", cursor.map(str::to_string));
276
277 let mut request = self
278 .client
279 .get(format!("{}/api/v1/post-submissions", self.base_url));
280 if !params.is_empty() {
281 request = request.query(¶ms);
282 }
283 Ok(request
284 .send()
285 .await?
286 .json::<CursorListDTO<PostSubmissionDTO>>()
287 .await?)
288 }
289
290 async fn get_post_submission(&self, submission_id: &str) -> Result<PostSubmissionDTO, Error> {
291 Ok(self
292 .client
293 .get(format!(
294 "{}/api/v1/post-submissions/{}",
295 self.base_url, submission_id
296 ))
297 .send()
298 .await?
299 .json::<PostSubmissionDTO>()
300 .await?)
301 }
302}