botserver/src/basic/keywords/weather.rs

190 lines
6.3 KiB
Rust
Raw Normal View History

2025-11-21 10:44:29 -03:00
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::{error, trace};
use reqwest::Client;
use rhai::{Dynamic, Engine};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Deserialize, Serialize)]
struct WeatherData {
pub location: String,
pub temperature: String,
pub condition: String,
pub forecast: String,
}
/// Fetches weather data from 7Timer! API (free, no auth)
async fn fetch_weather(location: &str) -> Result<WeatherData, Box<dyn std::error::Error>> {
// Parse location to get coordinates (simplified - in production use geocoding)
let (lat, lon) = parse_location(location)?;
// 7Timer! API endpoint
let url = format!(
"http://www.7timer.info/bin/api.pl?lon={}&lat={}&product=civil&output=json",
lon, lat
);
trace!("Fetching weather from: {}", url);
let client = Client::builder()
.timeout(Duration::from_secs(10))
.build()?;
let response = client.get(&url).send().await?;
if !response.status().is_success() {
return Err(format!("Weather API returned status: {}", response.status()).into());
}
let json: serde_json::Value = response.json().await?;
// Parse 7Timer response
let dataseries = json["dataseries"]
.as_array()
.ok_or("Invalid weather response")?;
if dataseries.is_empty() {
return Err("No weather data available".into());
}
let current = &dataseries[0];
let temp = current["temp2m"].as_i64().unwrap_or(0);
let weather_code = current["weather"].as_str().unwrap_or("unknown");
let condition = match weather_code {
"clear" => "Clear sky",
"pcloudy" => "Partly cloudy",
"cloudy" => "Cloudy",
"rain" => "Rain",
"lightrain" => "Light rain",
"humid" => "Humid",
"snow" => "Snow",
"lightsnow" => "Light snow",
_ => "Unknown",
};
// Build forecast string
let mut forecast_parts = Vec::new();
for (i, item) in dataseries.iter().take(3).enumerate() {
if let (Some(temp), Some(weather)) = (
item["temp2m"].as_i64(),
item["weather"].as_str(),
) {
forecast_parts.push(format!("{}h: {}°C, {}", i * 3, temp, weather));
}
}
let forecast = forecast_parts.join("; ");
Ok(WeatherData {
location: location.to_string(),
temperature: format!("{}°C", temp),
condition: condition.to_string(),
forecast,
})
}
/// Simple location parser (lat,lon or city name)
fn parse_location(location: &str) -> Result<(f64, f64), Box<dyn std::error::Error>> {
// Check if it's coordinates (lat,lon)
if let Some((lat_str, lon_str)) = location.split_once(',') {
let lat = lat_str.trim().parse::<f64>()?;
let lon = lon_str.trim().parse::<f64>()?;
return Ok((lat, lon));
}
// Default city coordinates (extend as needed)
let coords = match location.to_lowercase().as_str() {
"london" => (51.5074, -0.1278),
"paris" => (48.8566, 2.3522),
"new york" | "newyork" => (40.7128, -74.0060),
"tokyo" => (35.6762, 139.6503),
"sydney" => (-33.8688, 151.2093),
"são paulo" | "sao paulo" => (-23.5505, -46.6333),
"rio de janeiro" | "rio" => (-22.9068, -43.1729),
"brasília" | "brasilia" => (-15.8267, -47.9218),
"buenos aires" => (-34.6037, -58.3816),
"berlin" => (52.5200, 13.4050),
"madrid" => (40.4168, -3.7038),
"rome" => (41.9028, 12.4964),
"moscow" => (55.7558, 37.6173),
"beijing" => (39.9042, 116.4074),
"mumbai" => (19.0760, 72.8777),
"dubai" => (25.2048, 55.2708),
"los angeles" | "la" => (34.0522, -118.2437),
"chicago" => (41.8781, -87.6298),
"toronto" => (43.6532, -79.3832),
"mexico city" => (19.4326, -99.1332),
_ => return Err(format!("Unknown location: {}. Use 'lat,lon' format or known city", location).into()),
};
Ok(coords)
}
/// Register WEATHER keyword in Rhai engine
pub fn weather_keyword(
_state: Arc<AppState>,
_user_session: UserSession,
engine: &mut Engine,
) {
engine.register_custom_syntax(
&["WEATHER", "$expr$"],
false,
move |context, inputs| {
let location = context.eval_expression_tree(&inputs[0])?;
let location_str = location.to_string();
trace!("WEATHER keyword called for: {}", location_str);
// Create channel for async result
let (tx, rx) = std::sync::mpsc::channel();
// Spawn blocking task
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build();
let result = if let Ok(rt) = rt {
rt.block_on(async {
match fetch_weather(&location_str).await {
Ok(weather) => {
let msg = format!(
"Weather for {}: {} ({}). Forecast: {}",
weather.location,
weather.temperature,
weather.condition,
weather.forecast
);
Ok(msg)
}
Err(e) => {
error!("Weather fetch failed: {}", e);
Err(format!("Could not fetch weather: {}", e))
}
}
})
} else {
Err("Failed to create runtime".to_string())
};
let _ = tx.send(result);
});
// Wait for result
match rx.recv() {
Ok(Ok(weather_msg)) => Ok(Dynamic::from(weather_msg)),
Ok(Err(e)) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
e.into(),
rhai::Position::NONE,
))),
Err(_) => Err(Box::new(rhai::EvalAltResult::ErrorRuntime(
"Weather request timeout".into(),
rhai::Position::NONE,
))),
}
},
);
}