Skip to main content

st/
feedback_client.rs

1// -----------------------------------------------------------------------------
2// 🌮 Feedback API Client - Helping Smart Tree Survive the Franchise Wars!
3// -----------------------------------------------------------------------------
4// This module handles communication with f.8b.is for feedback submission and
5// update checking. All feedback helps make Smart Tree better!
6//
7// Endpoints:
8// - POST https://f.8b.is/api/feedback - Submit feedback and feature requests
9// - GET  https://f.8b.is/api/smart-tree/latest - Get latest version info (cached)
10// -----------------------------------------------------------------------------
11
12use anyhow::Result;
13use reqwest::{Client, StatusCode};
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use std::time::Duration;
17
18const FEEDBACK_API_BASE: &str = "https://f.8b.is";
19const USER_AGENT: &str = concat!("smart-tree/", env!("CARGO_PKG_VERSION"));
20
21/// Feedback submission request structure
22#[derive(Debug, Serialize)]
23pub struct FeedbackRequest {
24    pub category: String,
25    pub title: String,
26    pub description: String,
27    pub impact_score: u8,
28    pub frequency_score: u8,
29    pub affected_command: Option<String>,
30    pub mcp_tool: Option<String>,
31    pub proposed_fix: Option<String>,
32    pub proposed_solution: Option<String>,
33    pub fix_complexity: Option<String>,
34    pub auto_fixable: Option<bool>,
35    pub tags: Vec<String>,
36    pub examples: Vec<FeedbackExample>,
37    pub smart_tree_version: String,
38    pub anonymous: bool,
39    pub github_url: Option<String>,
40}
41
42#[derive(Debug, Serialize)]
43pub struct FeedbackExample {
44    pub description: String,
45    pub code: String,
46    pub expected_output: Option<String>,
47}
48
49/// Tool request structure
50#[derive(Debug, Serialize)]
51pub struct ToolRequest {
52    pub tool_name: String,
53    pub description: String,
54    pub use_case: String,
55    pub expected_output: String,
56    pub productivity_impact: String,
57    pub proposed_parameters: Option<Value>,
58    pub smart_tree_version: String,
59    pub anonymous: bool,
60    pub github_url: Option<String>,
61}
62
63/// Response from feedback API
64#[derive(Debug, Deserialize)]
65pub struct FeedbackResponse {
66    pub feedback_id: String,
67    pub message: String,
68    pub status: String,
69}
70
71/// Latest version info
72#[derive(Debug, Deserialize)]
73pub struct VersionInfo {
74    pub version: String,
75    pub release_date: String,
76    pub download_url: String,
77    pub release_notes_url: String,
78    pub features: Vec<String>,
79    pub ai_benefits: Vec<String>,
80}
81
82/// API client for f.8b.is
83pub struct FeedbackClient {
84    client: Client,
85}
86
87impl FeedbackClient {
88    pub fn new() -> Result<Self> {
89        let client = Client::builder()
90            .user_agent(USER_AGENT)
91            .timeout(Duration::from_secs(30))
92            .build()?;
93
94        Ok(Self { client })
95    }
96
97    /// Submit feedback to f.8b.is
98    pub async fn submit_feedback(&self, feedback: FeedbackRequest) -> Result<FeedbackResponse> {
99        let url = format!("{}/api/feedback", FEEDBACK_API_BASE);
100
101        let response = self.client.post(&url).json(&feedback).send().await?;
102
103        match response.status() {
104            StatusCode::OK => {
105                let data = response.json::<FeedbackResponse>().await?;
106                Ok(data)
107            }
108            StatusCode::TOO_MANY_REQUESTS => Err(anyhow::anyhow!(
109                "Rate limit exceeded. Please try again later."
110            )),
111            status => {
112                let error_text = response
113                    .text()
114                    .await
115                    .unwrap_or_else(|_| "Unknown error".to_string());
116                Err(anyhow::anyhow!("API error ({}): {}", status, error_text))
117            }
118        }
119    }
120
121    /// Submit tool request to f.8b.is
122    pub async fn submit_tool_request(&self, request: ToolRequest) -> Result<FeedbackResponse> {
123        let url = format!("{}/api/tool-request", FEEDBACK_API_BASE);
124
125        let response = self.client.post(&url).json(&request).send().await?;
126
127        match response.status() {
128            StatusCode::OK => {
129                let data = response.json::<FeedbackResponse>().await?;
130                Ok(data)
131            }
132            StatusCode::TOO_MANY_REQUESTS => Err(anyhow::anyhow!(
133                "Rate limit exceeded. Please try again later."
134            )),
135            status => {
136                let error_text = response
137                    .text()
138                    .await
139                    .unwrap_or_else(|_| "Unknown error".to_string());
140                Err(anyhow::anyhow!("API error ({}): {}", status, error_text))
141            }
142        }
143    }
144
145    /// Check for latest version (cached on server for 1 hour)
146    pub async fn check_for_updates(&self) -> Result<VersionInfo> {
147        let url = format!("{}/api/smart-tree/latest", FEEDBACK_API_BASE);
148
149        let response = self.client.get(&url).send().await?;
150
151        match response.status() {
152            StatusCode::OK => {
153                let data = response.json::<VersionInfo>().await?;
154                Ok(data)
155            }
156            status => {
157                let error_text = response
158                    .text()
159                    .await
160                    .unwrap_or_else(|_| "Unknown error".to_string());
161                Err(anyhow::anyhow!("API error ({}): {}", status, error_text))
162            }
163        }
164    }
165}
166
167impl Default for FeedbackClient {
168    fn default() -> Self {
169        Self::new().expect("Failed to create feedback client")
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_feedback_client_creation() {
179        let client = FeedbackClient::new();
180        assert!(client.is_ok());
181    }
182}