Initial commit
This commit is contained in:
360
internal/openweathermap/openweathermap.go
Normal file
360
internal/openweathermap/openweathermap.go
Normal file
@@ -0,0 +1,360 @@
|
||||
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: */
|
||||
Reference in New Issue
Block a user