main
  1use anyhow::{Context, Result};
  2use chrono::{DateTime, Utc};
  3use serde::{Deserialize, Serialize};
  4use std::process::Command;
  5
  6#[derive(Debug, Deserialize, Serialize, Clone)]
  7pub struct Notification {
  8    pub id: String,
  9    pub unread: bool,
 10    pub reason: String,
 11    pub updated_at: DateTime<Utc>,
 12    pub last_read_at: Option<DateTime<Utc>>,
 13    pub subject: Subject,
 14    pub repository: Repository,
 15}
 16
 17#[derive(Debug, Deserialize, Serialize, Clone)]
 18pub struct Subject {
 19    pub title: String,
 20    #[serde(rename = "type")]
 21    pub subject_type: String,
 22    pub url: Option<String>,
 23}
 24
 25#[derive(Debug, Deserialize, Serialize, Clone)]
 26pub struct Repository {
 27    pub full_name: String,
 28    pub name: String,
 29    pub owner: Owner,
 30}
 31
 32#[derive(Debug, Deserialize, Serialize, Clone)]
 33pub struct Owner {
 34    pub login: String,
 35}
 36
 37pub struct GitHubClient {
 38    token: String,
 39    client: reqwest::blocking::Client,
 40}
 41
 42impl GitHubClient {
 43    /// Create a new GitHub client with authentication
 44    /// 
 45    /// Tries to get token from:
 46    /// 1. `gh auth token` command (if gh CLI is available)
 47    /// 2. GITHUB_TOKEN environment variable
 48    pub fn new() -> Result<Self> {
 49        let token = Self::get_token()?;
 50        
 51        let client = reqwest::blocking::Client::builder()
 52            .user_agent("github-notif-manager/0.1.0")
 53            .build()
 54            .context("Failed to create HTTP client")?;
 55
 56        Ok(Self { token, client })
 57    }
 58
 59    /// Get GitHub token using gh CLI or environment variable
 60    fn get_token() -> Result<String> {
 61        // Try gh auth token first
 62        if let Ok(output) = Command::new("gh").arg("auth").arg("token").output() {
 63            if output.status.success() {
 64                let token = String::from_utf8_lossy(&output.stdout).trim().to_string();
 65                if !token.is_empty() {
 66                    return Ok(token);
 67                }
 68            }
 69        }
 70
 71        // Fall back to GITHUB_TOKEN environment variable
 72        std::env::var("GITHUB_TOKEN")
 73            .or_else(|_| std::env::var("GH_TOKEN"))
 74            .context("No GitHub token found. Either run 'gh auth login' or set GITHUB_TOKEN environment variable")
 75    }
 76
 77    /// Fetch notifications from GitHub
 78    /// 
 79    /// If `since` is provided, only fetches notifications updated after that timestamp.
 80    pub fn get_notifications(
 81        &self,
 82        all: bool,
 83        since: Option<&str>,
 84        progress: Option<&indicatif::ProgressBar>,
 85    ) -> Result<Vec<Notification>> {
 86        let mut notifications = Vec::new();
 87        let mut page = 1;
 88        let base_url = "https://api.github.com/notifications";
 89
 90        loop {
 91            let mut url = format!("{}?all={}&per_page=100&page={}", base_url, all, page);
 92            if let Some(since) = since {
 93                url.push_str(&format!("&since={}", since));
 94            }
 95            
 96            let response = self
 97                .client
 98                .get(&url)
 99                .header("Authorization", format!("Bearer {}", self.token))
100                .header("Accept", "application/vnd.github+json")
101                .header("X-GitHub-Api-Version", "2022-11-28")
102                .send()
103                .context("Failed to fetch notifications")?;
104
105            if !response.status().is_success() {
106                anyhow::bail!(
107                    "GitHub API returned error: {}",
108                    response.status(),
109                );
110            }
111
112            let page_notifications: Vec<Notification> = response
113                .json()
114                .context("Failed to parse notifications JSON")?;
115
116            if page_notifications.is_empty() {
117                break;
118            }
119
120            notifications.extend(page_notifications);
121
122            if let Some(pb) = progress {
123                pb.set_message(format!("{} notifications", notifications.len()));
124                pb.tick();
125            }
126
127            page += 1;
128        }
129
130        Ok(notifications)
131    }
132
133    /// Mark a notification thread as done (archive it)
134    pub fn mark_thread_done(&self, thread_id: &str) -> Result<()> {
135        let url = format!("https://api.github.com/notifications/threads/{}", thread_id);
136        
137        let response = self
138            .client
139            .delete(&url)
140            .header("Authorization", format!("Bearer {}", self.token))
141            .header("Accept", "application/vnd.github+json")
142            .header("X-GitHub-Api-Version", "2022-11-28")
143            .send()
144            .context("Failed to mark thread as done")?;
145
146        let status = response.status().as_u16();
147        if status != 204 && status != 205 {
148            anyhow::bail!("Failed to mark thread as done: {}", response.status());
149        }
150
151        Ok(())
152    }
153}