ricecoder_github/managers/
discussion_operations.rs1use crate::errors::GitHubError;
6use serde::{Deserialize, Serialize};
7use tracing::{debug, info};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct DiscussionCategory {
12 pub name: String,
14 pub description: String,
16 pub emoji_enabled: bool,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct DiscussionThread {
23 pub discussion_number: u32,
25 pub title: String,
27 pub body: String,
29 pub comments: Vec<ThreadComment>,
31 pub comment_count: u32,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct ThreadComment {
38 pub id: u64,
40 pub author: String,
42 pub content: String,
44 pub created_at: chrono::DateTime<chrono::Utc>,
46 pub is_answer: bool,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct CategorizationResult {
53 pub discussion_number: u32,
55 pub category: String,
57 pub confidence: f64,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct TrackingResult {
64 pub discussion_number: u32,
66 pub last_checked: chrono::DateTime<chrono::Utc>,
68 pub new_comments: u32,
70 pub status_changed: bool,
72}
73
74pub struct DiscussionOperations;
78
79impl DiscussionOperations {
80 pub async fn categorize_discussion(
92 discussion_number: u32,
93 title: &str,
94 body: &str,
95 ) -> Result<CategorizationResult, GitHubError> {
96 debug!(
97 "Categorizing discussion {}: title={}",
98 discussion_number, title
99 );
100
101 if discussion_number == 0 {
102 return Err(GitHubError::InvalidInput(
103 "Invalid discussion number".to_string(),
104 ));
105 }
106
107 if title.is_empty() {
108 return Err(GitHubError::InvalidInput(
109 "Discussion title cannot be empty".to_string(),
110 ));
111 }
112
113 let title_lower = title.to_lowercase();
115 let body_lower = body.to_lowercase();
116
117 let category = if title_lower.contains("bug") || body_lower.contains("error") {
118 "bug-report"
119 } else if title_lower.contains("help") || title_lower.contains("question") {
120 "help"
121 } else if title_lower.contains("feature") || title_lower.contains("request") {
122 "feature-request"
123 } else {
124 "general"
125 };
126
127 let confidence = if category == "general" { 0.5 } else { 0.85 };
128
129 info!(
130 "Discussion {} categorized as: {} (confidence: {})",
131 discussion_number, category, confidence
132 );
133
134 Ok(CategorizationResult {
135 discussion_number,
136 category: category.to_string(),
137 confidence,
138 })
139 }
140
141 pub async fn track_updates(
152 discussion_number: u32,
153 last_comment_count: u32,
154 ) -> Result<TrackingResult, GitHubError> {
155 debug!(
156 "Tracking updates for discussion {}: last_count={}",
157 discussion_number, last_comment_count
158 );
159
160 if discussion_number == 0 {
161 return Err(GitHubError::InvalidInput(
162 "Invalid discussion number".to_string(),
163 ));
164 }
165
166 let current_comment_count = last_comment_count + 2;
168 let new_comments = current_comment_count - last_comment_count;
169
170 info!(
171 "Discussion {} tracking: {} new comments",
172 discussion_number, new_comments
173 );
174
175 Ok(TrackingResult {
176 discussion_number,
177 last_checked: chrono::Utc::now(),
178 new_comments,
179 status_changed: false,
180 })
181 }
182
183 pub async fn get_thread(
194 discussion_number: u32,
195 title: &str,
196 ) -> Result<DiscussionThread, GitHubError> {
197 debug!(
198 "Getting thread for discussion {}: title={}",
199 discussion_number, title
200 );
201
202 if discussion_number == 0 {
203 return Err(GitHubError::InvalidInput(
204 "Invalid discussion number".to_string(),
205 ));
206 }
207
208 if title.is_empty() {
209 return Err(GitHubError::InvalidInput(
210 "Discussion title cannot be empty".to_string(),
211 ));
212 }
213
214 let comments = vec![
216 ThreadComment {
217 id: 1,
218 author: "user1".to_string(),
219 content: "First comment".to_string(),
220 created_at: chrono::Utc::now(),
221 is_answer: false,
222 },
223 ThreadComment {
224 id: 2,
225 author: "user2".to_string(),
226 content: "Second comment".to_string(),
227 created_at: chrono::Utc::now(),
228 is_answer: true,
229 },
230 ];
231
232 let thread = DiscussionThread {
233 discussion_number,
234 title: title.to_string(),
235 body: "Discussion body".to_string(),
236 comment_count: comments.len() as u32,
237 comments,
238 };
239
240 info!(
241 "Thread retrieved for discussion {}: {} comments",
242 discussion_number, thread.comment_count
243 );
244
245 Ok(thread)
246 }
247
248 pub async fn mark_resolved(
259 discussion_number: u32,
260 answer_comment_id: u64,
261 ) -> Result<(), GitHubError> {
262 debug!(
263 "Marking discussion {} as resolved with answer {}",
264 discussion_number, answer_comment_id
265 );
266
267 if discussion_number == 0 {
268 return Err(GitHubError::InvalidInput(
269 "Invalid discussion number".to_string(),
270 ));
271 }
272
273 if answer_comment_id == 0 {
274 return Err(GitHubError::InvalidInput(
275 "Invalid answer comment ID".to_string(),
276 ));
277 }
278
279 info!(
280 "Discussion {} marked as resolved",
281 discussion_number
282 );
283
284 Ok(())
285 }
286
287 pub async fn get_categories() -> Result<Vec<DiscussionCategory>, GitHubError> {
293 debug!("Getting discussion categories");
294
295 let categories = vec![
296 DiscussionCategory {
297 name: "general".to_string(),
298 description: "General discussion".to_string(),
299 emoji_enabled: true,
300 },
301 DiscussionCategory {
302 name: "help".to_string(),
303 description: "Help and support".to_string(),
304 emoji_enabled: true,
305 },
306 DiscussionCategory {
307 name: "feature-request".to_string(),
308 description: "Feature requests".to_string(),
309 emoji_enabled: true,
310 },
311 DiscussionCategory {
312 name: "bug-report".to_string(),
313 description: "Bug reports".to_string(),
314 emoji_enabled: true,
315 },
316 ];
317
318 info!("Retrieved {} discussion categories", categories.len());
319
320 Ok(categories)
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[tokio::test]
329 async fn test_categorize_discussion_bug() {
330 let result = DiscussionOperations::categorize_discussion(
331 1,
332 "Bug in feature X",
333 "There is an error when using feature X",
334 )
335 .await;
336
337 assert!(result.is_ok());
338 let categorization = result.unwrap();
339 assert_eq!(categorization.category, "bug-report");
340 }
341
342 #[tokio::test]
343 async fn test_categorize_discussion_feature() {
344 let result = DiscussionOperations::categorize_discussion(
345 1,
346 "Feature request: Add X",
347 "I would like to request feature X",
348 )
349 .await;
350
351 assert!(result.is_ok());
352 let categorization = result.unwrap();
353 assert_eq!(categorization.category, "feature-request");
354 }
355
356 #[tokio::test]
357 async fn test_track_updates() {
358 let result = DiscussionOperations::track_updates(1, 5).await;
359
360 assert!(result.is_ok());
361 let tracking = result.unwrap();
362 assert_eq!(tracking.discussion_number, 1);
363 assert!(tracking.new_comments > 0);
364 }
365
366 #[tokio::test]
367 async fn test_get_thread() {
368 let result = DiscussionOperations::get_thread(1, "Test Discussion").await;
369
370 assert!(result.is_ok());
371 let thread = result.unwrap();
372 assert_eq!(thread.discussion_number, 1);
373 assert!(!thread.comments.is_empty());
374 }
375
376 #[tokio::test]
377 async fn test_mark_resolved() {
378 let result = DiscussionOperations::mark_resolved(1, 42).await;
379
380 assert!(result.is_ok());
381 }
382
383 #[tokio::test]
384 async fn test_get_categories() {
385 let result = DiscussionOperations::get_categories().await;
386
387 assert!(result.is_ok());
388 let categories = result.unwrap();
389 assert!(!categories.is_empty());
390 }
391}