Files
aleesa-webapp-go/internal/openweathermap/openweathermap.go
2024-09-17 23:59:38 +03:00

361 lines
10 KiB
Go

package openweathermap
import (
"aleesa-webapp-go/internal/config"
"aleesa-webapp-go/internal/pcachedb"
"fmt"
"net/http"
"strconv"
"strings"
"time"
owm "github.com/briandowns/openweathermap"
log "github.com/sirupsen/logrus"
)
// Структурка для передачи в обрабтчик запросов в openweathermap api.
type OwmItem struct {
City string
FakeName string
Where string
Lon float64
Lat float64
}
// OwmClient обёртка с кэшированием для openweatermap.org.
func OwmClient(cfg *config.MyConfig, city string, days uint32) (string, error) {
var (
answer string
err error
item OwmItem
)
if city == "" {
return "Мне нужно ИМЯ города.", err
}
if len(city) > 80 {
return "Длинновато для имени города", err
}
// "Нормализуем" название города.
city = strings.TrimSpace(city)
firstLetter := strings.SplitN(city, "", 2)[0]
city = fmt.Sprintf("%s%s", strings.ToUpper(firstLetter), city[1:])
city = strings.ToValidUTF8(city, "")
switch city {
case "Msk", "Default", "Dc", "Мск", "Dc-Universe":
item.City = "Москва"
case "Spb", "Спб":
item.City = "Санкт-Петербург"
case "Ект", "Ёбург", "Ебург", "Екат", "Ekt", "Eburg", "Ekat":
item.City = "Екатеринбург"
case "Кудыкина гора":
// Координаты Килиманжаро (-3.066524619045895, 37.35588473583672).
item.Lat = -3.066524619045895
item.Lon = 37.35588473583672
item.FakeName = "Килиманжаро"
item.Where = "на горе"
default:
item.City = city
item.Where = "в городе"
}
if days == 0 {
answer, err = QueryOwmCache(cfg, item)
}
if err != nil {
return "", err
}
return answer, nil
}
// OwmAPIClient делает запрос непосредственно в openweathermap.com api.
func OwmAPIClient(cfg *config.MyConfig, item OwmItem) (string, error) {
var (
answer string
cityName string
client = http.DefaultClient
)
w, err := owm.NewCurrent(
"C",
"ru",
cfg.OpenWeatherMap.Appid,
owm.WithHttpClient(client),
)
if err != nil {
err := fmt.Errorf("OWM-Api: %w", err)
return answer, err
}
if item.City != "" {
if err = w.CurrentByName(item.City); err != nil {
// TODO: extend error
return answer, err
}
cityName = item.City
} else {
coord := owm.Coordinates{Longitude: item.Lon, Latitude: item.Lat}
if err = w.CurrentByCoordinates(&coord); err != nil {
// TODO: extend error
return answer, err
}
if item.FakeName == "" {
cityName = w.Name
} else {
cityName = item.FakeName
}
}
// Тута парсер - направление ветра, градусы итд.
wind := "разный"
switch {
case w.Wind.Deg == 0:
wind = "северный"
case w.Wind.Deg > 0 && w.Wind.Deg <= 30:
wind = "северо-северо-восточный"
case w.Wind.Deg > 30 && w.Wind.Deg <= 60:
wind = "северо-восточный"
case w.Wind.Deg > 60 && w.Wind.Deg < 90:
wind = "восточно-северо-восточный"
case w.Wind.Deg == 90:
wind = "восточный"
case w.Wind.Deg > 90 && w.Wind.Deg <= 120:
wind = "восточно-юго-восточный"
case w.Wind.Deg > 120 && w.Wind.Deg <= 150:
wind = "юговосточный"
case w.Wind.Deg > 150 && w.Wind.Deg < 180:
wind = "юго-юго-восточный"
case w.Wind.Deg == 180:
wind = "южный"
case w.Wind.Deg > 180 && w.Wind.Deg <= 210:
wind = "юго-юго-западный"
case w.Wind.Deg > 210 && w.Wind.Deg <= 240:
wind = "юго-западный"
case w.Wind.Deg > 240 && w.Wind.Deg < 270:
wind = "западно-юго-западный"
case w.Wind.Deg == 270:
wind = "западный"
case w.Wind.Deg > 270 && w.Wind.Deg <= 300:
wind = "западно-северо-западный"
case w.Wind.Deg > 300 && w.Wind.Deg <= 330:
wind = "северо-западный"
case w.Wind.Deg > 330 && w.Wind.Deg < 360:
wind = "северо-северо-западный"
case w.Wind.Deg == 360:
wind = "северный"
}
if int(w.Main.TempMin) == int(w.Main.TempMax) {
if cfg.OpenWeatherMap.Country {
answer = fmt.Sprintf(
"Погода %s %s, %s:\\n%s, ветер %s %.1f м/c, температура %d°C, ощущается как %d°C, относительная влажность %d%%, давление %d мм.рт.ст",
item.Where,
cityName,
w.Sys.Country,
w.Weather[0].Description,
wind,
w.Wind.Speed,
int(w.Main.TempMin),
int(w.Main.FeelsLike),
w.Main.Humidity,
int(w.Main.Pressure*0.75006375541921),
)
} else {
answer = fmt.Sprintf(
"Погода %s %s, (ш:%f, д:%f):\n%s, ветер %s %.1f м/c, температура %d°C, ощущается как %d°C, относительная влажность %d%%, давление %d мм.рт.ст",
item.Where,
cityName,
w.GeoPos.Latitude,
w.GeoPos.Longitude,
w.Weather[0].Description,
wind,
w.Wind.Speed,
int(w.Main.TempMin),
int(w.Main.FeelsLike),
w.Main.Humidity,
int(w.Main.Pressure*0.75006375541921),
)
}
} else {
if cfg.OpenWeatherMap.Country {
answer = fmt.Sprintf(
"Погода %s %s, %s:\n%s, ветер %s %.1f м/c, температура от %d до %d°C, ощущается как %d°C, относительная влажность %d%%, давление %d мм.рт.ст",
item.Where,
cityName,
w.Sys.Country,
w.Weather[0].Description,
wind,
w.Wind.Speed,
int(w.Main.TempMin),
int(w.Main.TempMax),
int(w.Main.FeelsLike),
w.Main.Humidity,
int(w.Main.Pressure*0.75006375541921),
)
} else {
answer = fmt.Sprintf(
"Погода %s %s, (ш:%f, д:%f):\n%s, ветер %s %.1f м/c, температура от %d до %d°C, ощущается как %d°C, относительная влажность %d%%, давление %d мм.рт.ст",
item.Where,
cityName,
w.GeoPos.Latitude,
w.GeoPos.Longitude,
w.Weather[0].Description,
wind,
w.Wind.Speed,
int(w.Main.TempMin),
int(w.Main.TempMax),
int(w.Main.FeelsLike),
w.Main.Humidity,
int(w.Main.Pressure*0.75006375541921),
)
}
}
return answer, err
}
// UpdateOwmCache обновляет кэш.
func UpdateOwmCache(cfg *config.MyConfig, tsNowUnix int64, entry string, value string) error {
key := fmt.Sprintf("%s+timestamp", entry)
// "Кэшируем" на 3 часа.
ts := tsNowUnix + int64((3 * time.Hour).Seconds())
timestamp := fmt.Sprintf("%d", ts)
if err := pcachedb.SaveKeyWithValue(cfg, "cache", key, timestamp); err != nil {
return fmt.Errorf("OWM-Cache: unable to save timestamp to cache: %w", err)
}
key = fmt.Sprintf("%s+value", entry)
if err := pcachedb.SaveKeyWithValue(cfg, "cache", key, value); err != nil {
return fmt.Errorf("OWM-Cache: unable to save answer to cache: %w", err)
}
return nil
}
// QueryOwmCache пытается вынуть из кэша ответ относительно данного города или координат.
// Если кэш пуст, делает запрос к api owm.
func QueryOwmCache(cfg *config.MyConfig, item OwmItem) (string, error) {
var (
answer string
err error
tsNow = time.Now().Unix()
key string
)
if item.City != "" {
key = item.City + "+timestamp"
} else {
key = fmt.Sprintf("%f+%f+timestamp", item.Lon, item.Lat)
}
// А вот тут мы реализовываем механику кэширования.
tsCacheString := pcachedb.GetValue(cfg, "cache", key)
// Для этого города в кэше пока ничего нету, вероятно это первый запрос.
if tsCacheString == "" {
log.Debugf("OWM no cache entry for city %s", key)
answer, err := OwmAPIClient(cfg, item)
// Здесь мы ничего сделать не можем.
if err != nil {
return "", err
}
// Если всё хорошо, надо обновить кэш.
if err := UpdateOwmCache(cfg, tsNow, key, answer); err != nil {
return "", err
}
return answer, err
}
tsCache, err := strconv.ParseInt(tsCacheString, 10, 64)
// Если конверсия не удалась, значит значение в кэше хреновое, надо делать запрос в api и обновлять кэш.
// Первое, что приходит в голову - коллизия хэшей sha256.
if err != nil {
log.Warnf("OWM-Cache: unable to parse string as int64 (sha256 collision?): %s", err)
answer, err := OwmAPIClient(cfg, item)
// Бинго! у нас ещё и ошибка при обращении к api openwethermap, здесь мы ничего сделать не можем.
if err != nil {
return "", err
}
// Если всё хорошо, надо обновить кэш.
if err := UpdateOwmCache(cfg, tsNow, key, answer); err != nil {
return "", err
}
return answer, err
}
// Кэш просрочен
if tsCache < tsNow {
log.Debugf("OWM cache miss for city: %s", key)
answer, err := OwmAPIClient(cfg, item)
// Здесь мы ничего сделать не можем.
if err != nil {
return "", err
}
// Если всё хорошо, надо обновить кэш.
if err := UpdateOwmCache(cfg, tsNow, key, answer); err != nil {
log.Error(err)
}
return answer, err
}
key += "+value"
answer = pcachedb.GetValue(cfg, "cache", key)
// Какие-то проблемы с кэшом - ts есть, но самого кэша нету. Более подробная инфа по идее уже попала в лог.
// Или в кэше что-то не то - длина ответа *точно* не может быть меньше 140 символов.
// Попробуем достать данные из api и сохранить в тот самый многострадальный кэш.
if len(answer) < 140 {
log.Debugf("OWM cache miss for city: %s", key)
if len(answer) > 0 {
log.Errorf("OWM-Cache: record for city %s in cache is too short, less than 140 chars", key)
}
answer, err = OwmAPIClient(cfg, item)
// Здесь мы ничего сделать не можем.
if err != nil {
return "", err
}
// Если всё хорошо, надо обновить кэш.
if err := UpdateOwmCache(cfg, tsNow, key, answer); err != nil {
return "", err
}
return answer, err
}
log.Debugf("OWM cache hit for city: %s", key)
return answer, err
}
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */