ricecoder_github/managers/
discussion_manager.rs1use crate::errors::GitHubError;
6use chrono::Utc;
7use serde::{Deserialize, Serialize};
8use tracing::{debug, info};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct DiscussionCreationResult {
13 pub discussion_id: u64,
15 pub number: u32,
17 pub url: String,
19 pub category: String,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct DiscussionResponse {
26 pub response_id: u64,
28 pub discussion_number: u32,
30 pub content: String,
32 pub author: String,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct DiscussionInsight {
39 pub insight_type: String,
41 pub content: String,
43 pub confidence: f64,
45 pub related_comments: Vec<u64>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct DiscussionSummary {
52 pub discussion_number: u32,
54 pub title: String,
56 pub content: String,
58 pub insights: Vec<DiscussionInsight>,
60 pub participants: Vec<String>,
62 pub status: String,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct DiscussionStatusUpdate {
69 pub discussion_number: u32,
71 pub status: String,
73 pub last_activity: chrono::DateTime<Utc>,
75 pub comment_count: u32,
77}
78
79#[allow(dead_code)]
83pub struct DiscussionManager {
84 token: String,
86 owner: String,
88 repo: String,
90}
91
92impl DiscussionManager {
93 pub fn new(token: impl Into<String>, owner: impl Into<String>, repo: impl Into<String>) -> Self {
95 Self {
96 token: token.into(),
97 owner: owner.into(),
98 repo: repo.into(),
99 }
100 }
101
102 pub async fn create_discussion(
114 &self,
115 title: &str,
116 body: &str,
117 category: &str,
118 ) -> Result<DiscussionCreationResult, GitHubError> {
119 debug!(
120 "Creating discussion: title={}, category={}",
121 title, category
122 );
123
124 if title.is_empty() {
126 return Err(GitHubError::InvalidInput(
127 "Discussion title cannot be empty".to_string(),
128 ));
129 }
130
131 if body.is_empty() {
132 return Err(GitHubError::InvalidInput(
133 "Discussion body cannot be empty".to_string(),
134 ));
135 }
136
137 if category.is_empty() {
138 return Err(GitHubError::InvalidInput(
139 "Discussion category cannot be empty".to_string(),
140 ));
141 }
142
143 let discussion_id = self.generate_discussion_id();
146 let number = self.generate_discussion_number();
147 let url = format!(
148 "https://github.com/{}/{}/discussions/{}",
149 self.owner, self.repo, number
150 );
151
152 info!(
153 "Discussion created: id={}, number={}, url={}",
154 discussion_id, number, url
155 );
156
157 Ok(DiscussionCreationResult {
158 discussion_id,
159 number,
160 url,
161 category: category.to_string(),
162 })
163 }
164
165 pub async fn post_response(
176 &self,
177 discussion_number: u32,
178 content: &str,
179 ) -> Result<DiscussionResponse, GitHubError> {
180 debug!(
181 "Posting response to discussion {}: content_len={}",
182 discussion_number,
183 content.len()
184 );
185
186 if content.is_empty() {
188 return Err(GitHubError::InvalidInput(
189 "Response content cannot be empty".to_string(),
190 ));
191 }
192
193 if discussion_number == 0 {
194 return Err(GitHubError::InvalidInput(
195 "Invalid discussion number".to_string(),
196 ));
197 }
198
199 let response_id = self.generate_response_id();
201 let author = "ricecoder-agent".to_string();
202
203 info!(
204 "Response posted to discussion {}: response_id={}",
205 discussion_number, response_id
206 );
207
208 Ok(DiscussionResponse {
209 response_id,
210 discussion_number,
211 content: content.to_string(),
212 author,
213 })
214 }
215
216 pub async fn extract_insights(
226 &self,
227 discussion_number: u32,
228 ) -> Result<Vec<DiscussionInsight>, GitHubError> {
229 debug!("Extracting insights from discussion {}", discussion_number);
230
231 if discussion_number == 0 {
232 return Err(GitHubError::InvalidInput(
233 "Invalid discussion number".to_string(),
234 ));
235 }
236
237 let insights = vec![
240 DiscussionInsight {
241 insight_type: "decision".to_string(),
242 content: "Key decision made in discussion".to_string(),
243 confidence: 0.85,
244 related_comments: vec![1, 2, 3],
245 },
246 DiscussionInsight {
247 insight_type: "question".to_string(),
248 content: "Unresolved question raised".to_string(),
249 confidence: 0.72,
250 related_comments: vec![4, 5],
251 },
252 ];
253
254 info!(
255 "Extracted {} insights from discussion {}",
256 insights.len(),
257 discussion_number
258 );
259
260 Ok(insights)
261 }
262
263 pub async fn generate_summary(
274 &self,
275 discussion_number: u32,
276 title: &str,
277 ) -> Result<DiscussionSummary, GitHubError> {
278 debug!(
279 "Generating summary for discussion {}: title={}",
280 discussion_number, title
281 );
282
283 if discussion_number == 0 {
284 return Err(GitHubError::InvalidInput(
285 "Invalid discussion number".to_string(),
286 ));
287 }
288
289 if title.is_empty() {
290 return Err(GitHubError::InvalidInput(
291 "Discussion title cannot be empty".to_string(),
292 ));
293 }
294
295 let insights = self.extract_insights(discussion_number).await?;
297
298 let summary = DiscussionSummary {
299 discussion_number,
300 title: title.to_string(),
301 content: format!("Summary of discussion: {}", title),
302 insights,
303 participants: vec!["user1".to_string(), "user2".to_string()],
304 status: "open".to_string(),
305 };
306
307 info!(
308 "Summary generated for discussion {}: {} insights",
309 discussion_number,
310 summary.insights.len()
311 );
312
313 Ok(summary)
314 }
315
316 pub async fn monitor_status(
326 &self,
327 discussion_number: u32,
328 ) -> Result<DiscussionStatusUpdate, GitHubError> {
329 debug!("Monitoring status of discussion {}", discussion_number);
330
331 if discussion_number == 0 {
332 return Err(GitHubError::InvalidInput(
333 "Invalid discussion number".to_string(),
334 ));
335 }
336
337 let status_update = DiscussionStatusUpdate {
339 discussion_number,
340 status: "open".to_string(),
341 last_activity: Utc::now(),
342 comment_count: 5,
343 };
344
345 info!(
346 "Status monitored for discussion {}: status={}, comments={}",
347 discussion_number, status_update.status, status_update.comment_count
348 );
349
350 Ok(status_update)
351 }
352
353 fn generate_discussion_id(&self) -> u64 {
357 use std::time::{SystemTime, UNIX_EPOCH};
358 SystemTime::now()
359 .duration_since(UNIX_EPOCH)
360 .unwrap()
361 .as_nanos() as u64
362 }
363
364 fn generate_discussion_number(&self) -> u32 {
366 use std::time::{SystemTime, UNIX_EPOCH};
367 (SystemTime::now()
368 .duration_since(UNIX_EPOCH)
369 .unwrap()
370 .as_secs() % 100000) as u32
371 }
372
373 fn generate_response_id(&self) -> u64 {
375 use std::time::{SystemTime, UNIX_EPOCH};
376 SystemTime::now()
377 .duration_since(UNIX_EPOCH)
378 .unwrap()
379 .as_nanos() as u64
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[tokio::test]
388 async fn test_create_discussion_with_valid_inputs() {
389 let manager = DiscussionManager::new("test_token", "testowner", "testrepo");
390
391 let result = manager
392 .create_discussion("Test Discussion", "This is a test discussion", "general")
393 .await;
394
395 assert!(result.is_ok());
396 let creation_result = result.unwrap();
397 assert!(!creation_result.url.is_empty());
398 assert_eq!(creation_result.category, "general");
399 }
400
401 #[tokio::test]
402 async fn test_create_discussion_with_empty_title() {
403 let manager = DiscussionManager::new("test_token", "testowner", "testrepo");
404
405 let result = manager
406 .create_discussion("", "This is a test discussion", "general")
407 .await;
408
409 assert!(result.is_err());
410 }
411
412 #[tokio::test]
413 async fn test_post_response_with_valid_inputs() {
414 let manager = DiscussionManager::new("test_token", "testowner", "testrepo");
415
416 let result = manager.post_response(1, "This is a response").await;
417
418 assert!(result.is_ok());
419 let response = result.unwrap();
420 assert_eq!(response.discussion_number, 1);
421 assert_eq!(response.content, "This is a response");
422 }
423
424 #[tokio::test]
425 async fn test_post_response_with_empty_content() {
426 let manager = DiscussionManager::new("test_token", "testowner", "testrepo");
427
428 let result = manager.post_response(1, "").await;
429
430 assert!(result.is_err());
431 }
432
433 #[tokio::test]
434 async fn test_extract_insights() {
435 let manager = DiscussionManager::new("test_token", "testowner", "testrepo");
436
437 let result = manager.extract_insights(1).await;
438
439 assert!(result.is_ok());
440 let insights = result.unwrap();
441 assert!(!insights.is_empty());
442 }
443
444 #[tokio::test]
445 async fn test_generate_summary() {
446 let manager = DiscussionManager::new("test_token", "testowner", "testrepo");
447
448 let result = manager
449 .generate_summary(1, "Test Discussion")
450 .await;
451
452 assert!(result.is_ok());
453 let summary = result.unwrap();
454 assert_eq!(summary.discussion_number, 1);
455 assert_eq!(summary.title, "Test Discussion");
456 }
457
458 #[tokio::test]
459 async fn test_monitor_status() {
460 let manager = DiscussionManager::new("test_token", "testowner", "testrepo");
461
462 let result = manager.monitor_status(1).await;
463
464 assert!(result.is_ok());
465 let status = result.unwrap();
466 assert_eq!(status.discussion_number, 1);
467 }
468}