offline_intelligence/api/
feedback_api.rs1use axum::{
3 extract::Json,
4 http::StatusCode,
5};
6use serde::{Deserialize, Serialize};
7use tracing::{info, error, warn};
8
9#[derive(Debug, Deserialize)]
10pub struct FeedbackRequest {
11 pub message: String,
12 #[serde(default)]
13 pub email: String,
14}
15
16#[derive(Debug, Serialize)]
17pub struct FeedbackResponse {
18 pub success: bool,
19 pub message: String,
20}
21
22pub async fn submit_feedback(
23 Json(payload): Json<FeedbackRequest>,
24) -> (StatusCode, Json<FeedbackResponse>) {
25 if payload.message.trim().is_empty() {
26 return (
27 StatusCode::BAD_REQUEST,
28 Json(FeedbackResponse {
29 success: false,
30 message: "Feedback message cannot be empty".to_string(),
31 }),
32 );
33 }
34
35 info!("Received feedback submission ({} chars)", payload.message.len());
36
37 if let Err(e) = save_feedback_locally(&payload) {
39 error!("Failed to save feedback locally: {}", e);
40 return (
41 StatusCode::INTERNAL_SERVER_ERROR,
42 Json(FeedbackResponse {
43 success: false,
44 message: "Failed to save feedback. Please try again.".to_string(),
45 }),
46 );
47 }
48
49 info!("Feedback saved locally");
50
51 match send_feedback_email(&payload).await {
53 Ok(feedback_number) => info!("Feedback email #{} sent to product team", feedback_number),
54 Err(e) => warn!("Email not sent (SMTP may not be configured): {}", e),
55 }
56
57 (
58 StatusCode::OK,
59 Json(FeedbackResponse {
60 success: true,
61 message: "Feedback submitted successfully. Thank you!".to_string(),
62 }),
63 )
64}
65
66fn aud_io_data_dir() -> std::path::PathBuf {
71 dirs::data_dir()
72 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default())
73 .join("Aud.io")
74 .join("data")
75}
76
77fn save_feedback_locally(payload: &FeedbackRequest) -> Result<(), Box<dyn std::error::Error>> {
78 let db_dir = aud_io_data_dir();
79 std::fs::create_dir_all(&db_dir)?;
80
81 let conn = rusqlite::Connection::open(db_dir.join("feedback.db"))?;
82
83 conn.execute_batch(
84 "CREATE TABLE IF NOT EXISTS feedback (
85 id INTEGER PRIMARY KEY AUTOINCREMENT,
86 message TEXT NOT NULL,
87 email TEXT DEFAULT '',
88 created_at DATETIME DEFAULT CURRENT_TIMESTAMP
89 );"
90 )?;
91
92 conn.execute(
93 "INSERT INTO feedback (message, email) VALUES (?1, ?2)",
94 rusqlite::params![payload.message, payload.email],
95 )?;
96
97 Ok(())
98}
99
100fn get_next_feedback_number() -> u64 {
102 let counter_path = aud_io_data_dir().join("feedback_counter.txt");
103
104 let current = if counter_path.exists() {
106 std::fs::read_to_string(&counter_path)
107 .ok()
108 .and_then(|s| s.trim().parse().ok())
109 .unwrap_or(0)
110 } else {
111 0
112 };
113
114 let next = current + 1;
115
116 if let Some(parent) = counter_path.parent() {
118 let _ = std::fs::create_dir_all(parent);
119 }
120 let _ = std::fs::write(counter_path, next.to_string());
121
122 next
123}
124
125async fn send_feedback_email(payload: &FeedbackRequest) -> Result<u64, Box<dyn std::error::Error>> {
126 use lettre::{
127 Message, SmtpTransport, Transport,
128 message::header::ContentType,
129 transport::smtp::authentication::Credentials,
130 };
131
132 let smtp_user = std::env::var("SMTP_USER")
135 .unwrap_or_else(|_| option_env!("SMTP_USER").unwrap_or("").to_string());
136 let smtp_pass = std::env::var("SMTP_PASS")
137 .unwrap_or_else(|_| option_env!("SMTP_PASS").unwrap_or("").to_string());
138
139 if smtp_user.is_empty() || smtp_pass.is_empty() {
140 return Err("SMTP credentials not configured".into());
141 }
142
143 let smtp_host = std::env::var("SMTP_HOST")
144 .unwrap_or_else(|_| option_env!("SMTP_HOST").unwrap_or("smtp.hostinger.com").to_string());
145 let smtp_port: u16 = std::env::var("SMTP_PORT")
146 .unwrap_or_else(|_| option_env!("SMTP_PORT").unwrap_or("587").to_string())
147 .parse()
148 .unwrap_or(587);
149
150 let feedback_number = get_next_feedback_number();
152
153 let from_address = format!("Aud.io Feedback <{}>", smtp_user);
156
157 let mut email_builder = Message::builder()
158 .from(from_address.parse()?)
159 .to("Product Team <product@offlineintelligence.io>".parse()?)
160 .subject(format!("FEEDBACK #{} - _Aud.io User Feedback", feedback_number));
161
162 if !payload.email.is_empty() {
164 email_builder = email_builder.reply_to(payload.email.parse()?);
165 }
166
167 let email = email_builder
168 .header(ContentType::TEXT_PLAIN)
169 .body(format!(
170 "FEEDBACK #{}\n\nNew feedback from _Aud.io user:\n\n{}\n\n---\nUser email: {}",
171 feedback_number,
172 payload.message,
173 if payload.email.is_empty() { "Not provided" } else { &payload.email }
174 ))?;
175
176 let creds = Credentials::new(smtp_user, smtp_pass);
177
178 let mailer = SmtpTransport::starttls_relay(&smtp_host)?
179 .port(smtp_port)
180 .credentials(creds)
181 .build();
182
183 mailer.send(&email)?;
184
185 Ok(feedback_number)
186}