From 9a8387157139893ef547eed46017d057941130b0 Mon Sep 17 00:00:00 2001 From: synzr Date: Tue, 2 Dec 2025 09:19:14 +0500 Subject: [PATCH] feat(clients/weather): yandex weather integration --- src/lib/clients/weather.ts | 175 +++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 src/lib/clients/weather.ts diff --git a/src/lib/clients/weather.ts b/src/lib/clients/weather.ts new file mode 100644 index 0000000..7a97052 --- /dev/null +++ b/src/lib/clients/weather.ts @@ -0,0 +1,175 @@ +import crypto from "node:crypto"; + +/** + * User agent header. + */ +const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36'; + +/** + * Token secret. Used to hash the token. + */ +const WEATHER_TOKEN_SECRET = "gravisfrontem"; + +interface YandexWeatherData { + /** + * Message from the server. + */ + message?: string | null; + + info: { + /** + * URL of the location page on Yandex Weather + **/ + url: string; + }; + + fact: { + /** + * Current air temperature in °C + **/ + temp: number; + + /** + * Water temperature in °C (may be missing) + **/ + temp_water?: number; + + /** + * Perceived (“feels like”) temperature in °C + **/ + feels_like: number; + + /** + * Weather condition code (e.g., "clear", "rain") + **/ + condition: string; + + /** + * Wind speed in m/s + **/ + wind_speed: number; + + /** + * Wind gust speed in m/s (may be missing) + **/ + wind_gust?: number; + + /** + * Wind direction code (e.g., "nw", "s") + **/ + wind_dir: string; + + /** + * Atmospheric pressure in mmHg + **/ + pressure_mm: number; + + /** + * Air humidity percentage + **/ + humidity: number; + }; +} + +export interface FetchWeatherResult { + /** + * Link to the weather in location. + **/ + url: string; + + temperature: { + /** + * Current temperature in °C. + **/ + current: number; + + /** + * Water temperature in °C. + **/ + water?: number; + + /** + * Perceived ("feels like") temperature in °C. + **/ + feelsLike: number; + }; + + /** + * Weather condition. + **/ + condition: string; + + wind: { + /** + * Wind speed in m/s. + **/ + speed: number; + + /** + * Wind gust speed in m/s. + **/ + gust?: number; + + /** + * Wind direction code. + **/ + direction: string; + }; + + /** + * Atmospheric pressure in mmHg. + **/ + pressure: number; + + /** + * Air humidity percentage. + **/ + humidity: number; +} + +function getWeatherHeaders() { + const timestamp = Date.now(); + + return { + // NOTE: no one would pay ~$7k to have a weather on their personal website + 'x-yandex-weather-timestamp': timestamp.toString(), + 'x-yandex-weather-token': + crypto.createHash('md5').update(`${WEATHER_TOKEN_SECRET}${timestamp}`).digest('hex'), + 'x-yandex-weather-client': 'YandexWeatherFront2', + }; +} + +export default async function fetchWeather(lat: number, lon: number): Promise { + const response = await fetch( + `https://api.weather.yandex.ru/v2/informers?lat=${lat}&lon=${lon}&lang=en_US`, + { + headers: { + 'user-agent': USER_AGENT, + ...getWeatherHeaders(), + }, + } + ); + + const result: YandexWeatherData = await response.json(); + + if (!response.ok) { + throw new Error(`Yandex Weather API error: ${result.message}`) + } + + return { + url: result.info.url, + temperature: { + current: result.fact.temp, + water: result.fact.temp_water, + feelsLike: result.fact.feels_like, + }, + condition: result.fact.condition, + wind: { + speed: result.fact.wind_speed, + gust: result.fact.wind_gust, + direction: result.fact.wind_dir, + }, + pressure: result.fact.pressure_mm, + humidity: result.fact.humidity, + }; +}