1use anyhow::{Context, Result};
2use chrono::{DateTime, Utc, Duration};
3use colored::*;
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::PathBuf;
7#[derive(Debug, Serialize, Deserialize)]
8struct AdminMessageResponse {
9 success: bool,
10 has_message: bool,
11 rate_limited: Option<bool>,
12 message: Option<AdminMessage>,
13}
14#[derive(Debug, Serialize, Deserialize)]
15struct AdminMessage {
16 id: i32,
17 title: String,
18 content: String,
19 #[serde(rename = "type")]
20 message_type: String,
21 priority: String,
22}
23#[derive(Debug, Serialize, Deserialize)]
24struct MessageCheckRecord {
25 last_checks: Vec<DateTime<Utc>>,
26 seen_message_ids: Vec<i32>,
27}
28impl Default for MessageCheckRecord {
29 fn default() -> Self {
30 Self {
31 last_checks: Vec::new(),
32 seen_message_ids: Vec::new(),
33 }
34 }
35}
36fn get_message_record_path() -> PathBuf {
37 dirs::home_dir()
38 .unwrap_or_else(|| PathBuf::from("."))
39 .join(".shipwreck")
40 .join("admin_msg_record.json")
41}
42fn load_check_record() -> Result<MessageCheckRecord> {
43 let path = get_message_record_path();
44 if !path.exists() {
45 return Ok(MessageCheckRecord::default());
46 }
47 let content = fs::read_to_string(&path)?;
48 let record: MessageCheckRecord = serde_json::from_str(&content)?;
49 Ok(record)
50}
51fn save_check_record(record: &MessageCheckRecord) -> Result<()> {
52 let path = get_message_record_path();
53 if let Some(parent) = path.parent() {
54 fs::create_dir_all(parent)?;
55 }
56 let json = serde_json::to_string_pretty(record)?;
57 fs::write(&path, json)?;
58 Ok(())
59}
60fn should_check_messages() -> Result<bool> {
61 let mut record = load_check_record()?;
62 let now = Utc::now();
63 let one_day_ago = now - Duration::hours(24);
64 record.last_checks.retain(|check| *check > one_day_ago);
65 if record.last_checks.len() >= 3 {
66 return Ok(false);
67 }
68 if let Some(last_check) = record.last_checks.last() {
69 let four_hours_ago = now - Duration::hours(4);
70 if *last_check > four_hours_ago {
71 return Ok(false);
72 }
73 }
74 Ok(true)
75}
76fn record_message_check() -> Result<()> {
77 let mut record = load_check_record()?;
78 record.last_checks.push(Utc::now());
79 save_check_record(&record)?;
80 Ok(())
81}
82fn has_seen_message(message_id: i32) -> Result<bool> {
83 let record = load_check_record()?;
84 Ok(record.seen_message_ids.contains(&message_id))
85}
86fn mark_message_seen(message_id: i32) -> Result<()> {
87 let mut record = load_check_record()?;
88 if !record.seen_message_ids.contains(&message_id) {
89 record.seen_message_ids.push(message_id);
90 save_check_record(&record)?;
91 }
92 Ok(())
93}
94fn get_user_id() -> Result<String> {
95 let user_id_path = dirs::home_dir()
96 .unwrap_or_else(|| PathBuf::from("."))
97 .join(".shipwreck")
98 .join("user.id");
99 if user_id_path.exists() {
100 fs::read_to_string(&user_id_path)
101 .map(|s| s.trim().to_string())
102 .context("Failed to read user ID")
103 } else {
104 Ok(format!("CM-USER-{}", Utc::now().timestamp()))
105 }
106}
107pub async fn check_and_display_message() -> Result<()> {
108 if !should_check_messages()? {
109 return Ok(());
110 }
111 record_message_check()?;
112 let user_id = get_user_id()?;
113 let version = env!("CARGO_PKG_VERSION");
114 let client = reqwest::Client::new();
115 let response = client
116 .get("https://cargo.do/api/admin/msg")
117 .query(&[("user_id", user_id.as_str()), ("version", version)])
118 .timeout(std::time::Duration::from_secs(5))
119 .send()
120 .await;
121 match response {
122 Ok(resp) if resp.status().is_success() => {
123 if let Ok(data) = resp.json::<AdminMessageResponse>().await {
124 if data.success && data.has_message {
125 if let Some(message) = data.message {
126 if !has_seen_message(message.id)? {
127 display_admin_message(&message);
128 mark_message_seen(message.id)?;
129 let _ = client
130 .post("https://cargo.do/api/admin/msg")
131 .form(
132 &[
133 ("user_id", user_id.as_str()),
134 ("message_id", &message.id.to_string()),
135 ("mark_seen", "true"),
136 ],
137 )
138 .timeout(std::time::Duration::from_secs(2))
139 .send()
140 .await;
141 }
142 }
143 }
144 }
145 }
146 _ => {}
147 }
148 Ok(())
149}
150fn display_admin_message(message: &AdminMessage) {
151 println!();
152 let border_color = match message.message_type.as_str() {
153 "error" => "red",
154 "warning" => "yellow",
155 "success" => "green",
156 _ => "cyan",
157 };
158 match message.priority.as_str() {
159 "critical" => {
160 println!(
161 "{}",
162 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
163 .red().bold()
164 );
165 println!("{} {}", "🚨".red(), message.title.red().bold());
166 println!(
167 "{}",
168 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
169 .red().bold()
170 );
171 }
172 "urgent" => {
173 println!(
174 "{}",
175 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
176 .yellow()
177 );
178 println!("{} {}", "⚠️ ".yellow(), message.title.yellow().bold());
179 println!(
180 "{}",
181 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
182 .yellow()
183 );
184 }
185 _ => {
186 let line = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━";
187 match border_color {
188 "red" => println!("{}", line.red()),
189 "yellow" => println!("{}", line.yellow()),
190 "green" => println!("{}", line.green()),
191 _ => println!("{}", line.cyan()),
192 }
193 let icon = match message.message_type.as_str() {
194 "error" => "❌",
195 "warning" => "⚠️",
196 "success" => "✅",
197 _ => "ℹ️",
198 };
199 println!("{} {}", icon, message.title.bold());
200 match border_color {
201 "red" => println!("{}", line.red()),
202 "yellow" => println!("{}", line.yellow()),
203 "green" => println!("{}", line.green()),
204 _ => println!("{}", line.cyan()),
205 }
206 }
207 }
208 println!();
209 for line in message.content.lines() {
210 println!(" {}", line);
211 }
212 println!();
213 match message.priority.as_str() {
214 "critical" => {
215 println!(
216 "{}",
217 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
218 .red().bold()
219 )
220 }
221 "urgent" => {
222 println!(
223 "{}",
224 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
225 .yellow()
226 )
227 }
228 _ => {
229 let line = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━";
230 match border_color {
231 "red" => println!("{}", line.red()),
232 "yellow" => println!("{}", line.yellow()),
233 "green" => println!("{}", line.green()),
234 _ => println!("{}", line.cyan()),
235 }
236 }
237 }
238 println!();
239}
240pub async fn force_check_message() -> Result<()> {
241 let mut record = load_check_record()?;
242 record.last_checks.clear();
243 save_check_record(&record)?;
244 check_and_display_message().await
245}