Replace prazdnikisegodnyaru with celebratoday, small underhood fixes

- Replace prazdnikisegodnyaru with celebratoday as first is banned in Russia
- Update .golangci.yml rule set
- Megatons of fixes for linter
- Update README.md
This commit is contained in:
eleksir
2026-02-10 20:05:13 +03:00
parent ff57b0af7d
commit f2fc168abe
28 changed files with 1298 additions and 268 deletions

View File

@@ -1,3 +1,4 @@
// Package anekdotru implemets client for anekdot.ru service.
package anekdotru
import (
@@ -23,11 +24,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
url = "https://www.anekdot.ru/rss/randomu.html"
)
err := requests.
URL(url).
UserAgent(userAgent).
ToBytesBuffer(&respBody).
Fetch(ctx)
err := requests.URL(url).UserAgent(userAgent).ToBytesBuffer(&respBody).Fetch(ctx)
if err != nil {
return "", fmt.Errorf("unable to GET %s: %w", url, err)
@@ -42,6 +39,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
if (len(aneks) >= 2) && (aneks[1] != "") {
re = regexp.MustCompile(`'\);`)
a := re.Split(aneks[1], 2)
if (len(a) >= 2) && (a[0] != "") {
@@ -51,7 +49,9 @@ func APIClient(cfg *config.MyConfig) (string, error) {
anek = re.ReplaceAll(anek, []byte(`"`))
re = regexp.MustCompile(`\\"`)
anekJSON := re.ReplaceAll(anek, []byte(`"`))
anekJSON = bytes.ToValidUTF8(anekJSON, []byte{0xef, 0xbf, 0xbd})
var anekMap []string
@@ -60,6 +60,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
}
re = regexp.MustCompile(`<br>`)
answer := re.ReplaceAll([]byte(anekMap[0]), []byte("\n"))
return string(answer), nil

View File

@@ -1,3 +1,4 @@
// Package bunicomic implements client for bunicomic.com service.
package bunicomic
import (
@@ -48,13 +49,13 @@ func APIClient(cfg *config.MyConfig) (string, error) {
if n.Type == html.ElementNode && n.Data == "meta" {
for _, meta := range n.Attr {
if meta.Key == "content" {
// adds a new link entry when the attribute matches
// Adds a new link entry when the attribute matches.
metaTags = append(metaTags, meta.Val)
}
}
}
// traverses the HTML of the webpage from the first child node
// Traverses the HTML of the webpage from the first child node.
for c := n.FirstChild; c != nil; c = c.NextSibling {
meta(c)
}

View File

@@ -0,0 +1,223 @@
// Package celebratoday implements client for celebratoday.com service.
package celebratoday
import (
"aleesa-webapp-go/internal/config"
"aleesa-webapp-go/internal/log"
"aleesa-webapp-go/internal/pcachedb"
"context"
"errors"
"fmt"
"io"
"slices"
"strconv"
"strings"
"time"
"github.com/carlmjohnson/requests"
"golang.org/x/net/html"
)
// Client обёртка для клиента сервиса celebratoday.com, работающая с кэшем. Запросы надо делать
// именно через неё.
func Client(cfg *config.MyConfig) (string, error) {
tsNow := time.Now()
// eTime TTL кэша. На текущий момент +1 час к текущему времени.
eTime := tsNow.Unix() + 3600
tsStr := pcachedb.GetValue(cfg, "cache", "celebratoday_timestamp")
// Кэш пустой, запросов пока не было.
if tsStr == "" {
log.Debugf("CT no cache entry for celebratoday.com")
answer, err := APIClient(cfg)
if err != nil {
return "", err
}
if err := UpdateCache(cfg, eTime, answer); err != nil {
log.Errorf("%s", err)
}
return answer, nil
}
tsCache, err := strconv.ParseInt(tsStr, 10, 64)
if err != nil {
log.Debug("KSP cache miss")
log.Warnf("KSP-Cache: unable to parse string as int64 (sha256 collision?): %s", err)
answer, err := APIClient(cfg)
// Бинго! у нас ещё и ошибка при обращении к "api" celebratoday.com, здесь мы ничего сделать не можем.
if err != nil {
return "", err
}
// Если всё хорошо, надо обновить кэш.
if err := UpdateCache(cfg, eTime, answer); err != nil {
log.Errorf("%s", err)
}
return answer, nil
}
// Текущее время в Unix timestamp.
tsNowUnix := tsNow.Unix()
// Кэш просрочен.
if tsCache < tsNowUnix {
log.Debug("KSP cache miss")
answer, err := APIClient(cfg)
// Здесь мы ничего сделать не можем.
if err != nil {
return "", err
}
// Если всё хорошо, надо обновить кэш.
if err := UpdateCache(cfg, eTime, answer); err != nil {
log.Errorf("%s", err)
}
return answer, nil
}
// TODO: как-то научиться определять ситуацию с отсутствующим значением в кэше.
// TODO: Научиться определять ситуацию, когда значением является unix timestamp.
log.Debug("CT cache hit")
return pcachedb.GetValue(cfg, "cache", "celebratoday_value"), nil
}
// APIClient клиент сервиса https://celebratoday.com/.
func APIClient(cfg *config.MyConfig) (string, error) {
var (
ctx = context.Background()
url = "https://celebratoday.com/ru/today"
respBody string
orangeClass int8
tgHref int8
)
// Формальная попытка притвориться браузером.
_, _, day := time.Now().Date()
userAgent := cfg.UserAgents[day]
if err := requests.URL(url).UserAgent(userAgent).
Header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8").
Header("Accept-Language", "en-US,en;q=0.5").
Header("User-Agent", "ru-RU").
Header("Accept-Charset", "utf-8").ToString(&respBody).Fetch(ctx); err != nil {
return "", fmt.Errorf("unable to GET %s: %w", url, err)
}
var holidays []string
// Парсим body ответа от сервера.
tokenizer := html.NewTokenizer(strings.NewReader(respBody))
for {
tokenType := tokenizer.Next()
if tokenType == html.ErrorToken {
err := tokenizer.Err()
if errors.Is(err, io.EOF) {
// End of the file, break out of the loop.
break
}
return "", err
}
if tokenType == html.StartTagToken {
token := tokenizer.Token()
if token.Data == "a" {
for _, attribute := range token.Attr {
// Кнопка о том, что у нас было вчера.
if attribute.Key == "class" && attribute.Val == "text-orange-500 underline hover:no-underline" {
orangeClass++
continue
}
// Наши ссылки находятся после первого и до второго упоминания о телеграме.
if attribute.Key == "href" && attribute.Val == "https://t.me/celebratoday" {
tgHref++
continue
}
if attribute.Key == "title" && tgHref == 1 && orangeClass == 2 {
switch {
case strings.HasPrefix(attribute.Val, "Праздники "):
continue
case strings.HasPrefix(attribute.Val, "Международные праздники "):
continue
case strings.HasPrefix(attribute.Val, "Всемирные праздники "):
continue
case strings.HasPrefix(attribute.Val, "Спортивные праздники "):
continue
case strings.HasPrefix(attribute.Val, "Медицинские праздники"):
continue
case strings.HasPrefix(attribute.Val, "Подробнее про"):
continue
default:
holidays = append(holidays, strings.TrimSuffix(attribute.Val, " | celebratoday.com"))
}
continue
}
}
}
}
// Если мы встретили вторую ссылку на телеграм, то конечная, дальше нам не надо.
if tgHref >= 2 {
break
}
}
if len(holidays) == 0 {
return "", errors.New("unable to parse response from celebratoday.com: no holidays found")
}
slices.Sort(holidays)
holidays = slices.Compact(holidays)
return "* " + strings.Join(holidays, "\n* "), nil
}
// UpdateCache сохраняет в кэш ответ от сервиса celebratoday и временной штамп.
func UpdateCache(cfg *config.MyConfig, eTime int64, value string) error {
key := "celebratoday_timestamp"
timestamp := strconv.FormatInt(eTime, 10)
if err := pcachedb.SaveKeyWithValue(cfg, "cache", key, timestamp); err != nil {
return fmt.Errorf("CT-Cache: unable to save timestamp to cache: %w", err)
}
key = "celebratoday_value"
if err := pcachedb.SaveKeyWithValue(cfg, "cache", key, value); err != nil {
return fmt.Errorf("CT-Cache: unable to save answer to cache: %w", err)
}
return nil
}
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

View File

@@ -1,4 +1,4 @@
// Этот package нужен, чтобы разбить кольцевую зависимость.
// Package config нужен, чтобы разбить кольцевую зависимость.
package config
import (
@@ -6,32 +6,34 @@ import (
"github.com/go-redis/redis/v8"
)
// MyConfig структурка, описывающая конфиг.
type MyConfig struct {
Server string `json:"server,omitempty"`
Port int `json:"port,omitempty"`
Timeout int `json:"timeout,omitempty"`
Loglevel string `json:"loglevel,omitempty"`
Log string `json:"log,omitempty"`
Channel string `json:"channel,omitempty"`
DataDir string `json:"datadir,omitempty"`
Csign string `json:"csign,omitempty"`
ForwardsMax int64 `json:"forwards_max,omitempty"`
OpenWeatherMap struct {
Enabled bool `json:"enabled,omitempty"`
Country bool `json:"country,omitempty"`
Appid string `json:"appid,omitempty"`
} `json:"openweathermap,omitempty"`
Flickr struct {
Enabled bool `json:"enabled,omitempty"`
Key string `json:"key,omitempty"`
Secret string `json:"secret,omitempty"`
OAuthToken string `json:"oauth_token,omitempty"`
OAuthTokenSecret string `json:"oauth_token_secret,omitempty"`
} `json:"flickr,omitempty"`
UserAgents []string `json:"user_agents,omitempty"`
PcacheDB map[string]*pebble.DB
RedisClient *redis.Client
}
type (
// MyConfig структурка, описывающая конфиг.
MyConfig struct { //nolint: govet // Идите в пень, мне так удобнее.
Server string `json:"server,omitempty"`
Port int `json:"port,omitempty"`
Timeout int `json:"timeout,omitempty"`
Loglevel string `json:"loglevel,omitempty"`
Log string `json:"log,omitempty"`
Channel string `json:"channel,omitempty"`
DataDir string `json:"datadir,omitempty"`
Csign string `json:"csign,omitempty"`
ForwardsMax int64 `json:"forwards_max,omitempty"`
OpenWeatherMap struct { //nolint: govet // Идите в пень, мне так удобнее.
Enabled bool `json:"enabled,omitempty"`
Country bool `json:"country,omitempty"`
Appid string `json:"appid,omitempty"`
} `json:"openweathermap"`
Flickr struct { //nolint: govet // Идите в пень, мне так удобнее.
Enabled bool `json:"enabled,omitempty"`
Key string `json:"key,omitempty"`
Secret string `json:"secret,omitempty"`
OAuthToken string `json:"oauth_token,omitempty"`
OAuthTokenSecret string `json:"oauth_token_secret,omitempty"`
} `json:"flickr"`
UserAgents []string `json:"user_agents,omitempty"`
PcacheDB map[string]*pebble.DB
RedisClient *redis.Client
}
)
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

View File

@@ -1,3 +1,4 @@
// Package flickr implements flickr.com service client.
package flickr
import (
@@ -17,27 +18,29 @@ import (
"gopkg.in/masci/flickr.v3/test"
)
// SearchResult структура, описывающая ответ api на поисковой запрос.
type SearchResult struct {
Photos struct {
Page int64 `json:"page,omitempty"`
Pages int64 `json:"pages,omitempty"`
Perpage int64 `json:"perpage,omitempty"`
Total int64 `json:"total,omitempty"`
Photo []struct {
Farm int64 `json:"farm,omitempty"`
ID string `json:"id,omitempty"`
Isfamily int64 `json:"isfamily,omitempty"`
Isfriend int64 `json:"isfriend,omitempty"`
Ispublic int64 `json:"ispublic,omitempty"`
Owner string `json:"owner,omitempty"`
Secret string `json:"secret,omitempty"`
Server string `json:"server,omitempty"`
Title string `json:"title,omitempty"`
} `json:"photo,omitempty"`
} `json:"photos,omitempty"`
Stat string `json:"stat,omitempty"`
}
type (
// SearchResult структура, описывающая ответ api на поисковой запрос.
SearchResult struct { //nolint: govet // Идите в пень, мне так удобнее.
Photos struct { //nolint: govet // Идите в пень, мне так удобнее.
Page int64 `json:"page,omitempty"`
Pages int64 `json:"pages,omitempty"`
Perpage int64 `json:"perpage,omitempty"`
Total int64 `json:"total,omitempty"`
Photo []struct { //nolint: govet // Идите в пень, мне так удобнее.
Farm int64 `json:"farm,omitempty"`
ID string `json:"id,omitempty"`
Isfamily int64 `json:"isfamily,omitempty"`
Isfriend int64 `json:"isfriend,omitempty"`
Ispublic int64 `json:"ispublic,omitempty"`
Owner string `json:"owner,omitempty"`
Secret string `json:"secret,omitempty"`
Server string `json:"server,omitempty"`
Title string `json:"title,omitempty"`
} `json:"photo"`
} `json:"photos"`
Stat string `json:"stat,omitempty"`
}
)
// APIClientInit генерит и сохраняет token и token secret с помощью даденных пользователем key и secret.
// В процессе выдаём юзеру в консоль урл и запрос открыть его в браузере, чтобы получить verifier key, который нам
@@ -47,15 +50,15 @@ func APIClientInit(cfg *config.MyConfig) error {
client := flickr.NewFlickrClient(cfg.Flickr.Key, cfg.Flickr.Secret)
// Первым делом достанем request token
// Первым делом достанем request token.
tok, err := flickr.GetRequestToken(client)
if err != nil {
return fmt.Errorf("unable to get Request Token: %w", err)
}
// Соорудим авторизационный URL
url, err := flickr.GetAuthorizeUrl(client, tok)
// Соорудим авторизационный URL.
link, err := flickr.GetAuthorizeUrl(client, tok)
if err != nil {
return fmt.Errorf("unable to get auth URL: %w", err)
@@ -63,11 +66,12 @@ func APIClientInit(cfg *config.MyConfig) error {
// А теперь ручные действия пользователя.
// Выдаём ему url, который надо ввести в адресную строку браузера, в браузере сгенерится verifier code, который нам
// нужен, чтобы вынуть из flicker api oauth token, secret, access token
// нужен, чтобы вынуть из flicker api oauth token, secret, access token.
var oauthVerifier string
fmt.Println("Open your browser at this url:", url)
fmt.Println("Open your browser at this url:", link)
fmt.Print("Then, insert the code: ")
_, _ = fmt.Scanln(&oauthVerifier)
// Наконец, достаём Access Token и Aceess Token Secret и сохраняем их в базку на диске.
@@ -127,7 +131,8 @@ func SearchByTags(cfg *config.MyConfig, tags []string) (string, error) {
)
// Заполняем поля для оаутха.
c.Init() // c.EndpointUrl
c.Init()
c.OAuthToken = oauthToken
c.OAuthTokenSecret = oauthSecret
@@ -152,12 +157,12 @@ func SearchByTags(cfg *config.MyConfig, tags []string) (string, error) {
// разметки, а во-вторых, этот самый flickr.DoGet() валится с segfault-ом, просто потому что. Схема не такая, на
// которую он рассчитан. Но нам повезло, что можно просто вытащить подписанный url и заправить его в свой
// http-клиент.
url := c.GetUrl()
link := c.GetUrl()
var response SearchResult
err := requests.
URL(url).
URL(link).
UserAgent(userAgent).
ToJSON(&response).
Fetch(ctx)
@@ -179,10 +184,10 @@ func SearchByTags(cfg *config.MyConfig, tags []string) (string, error) {
// Подписываем запрос oauth-говном.
c.OAuthSign()
url = c.GetUrl()
link = c.GetUrl()
err = requests.
URL(url).
URL(link).
UserAgent(userAgent).
ToJSON(&response).
Fetch(ctx)
@@ -200,10 +205,10 @@ func SearchByTags(cfg *config.MyConfig, tags []string) (string, error) {
var u SearchResult
for i := 0; i < len(response.Photos.Photo); i++ {
for i := range len(response.Photos.Photo) {
skip := false
for j := 0; j < len(u.Photos.Photo); j++ {
for j := range len(u.Photos.Photo) {
if response.Photos.Photo[i].Title == u.Photos.Photo[j].Title {
skip = true

View File

@@ -1,3 +1,4 @@
// Package log implements logging facility as golang slog module wrapper.
package log
import (
@@ -33,9 +34,9 @@ func Init(level string, fileDescriptor *os.File) {
opts := &slog.HandlerOptions{
// Use the ReplaceAttr function on the handler options
// to be able to replace any single attribute in the log output
// to be able to replace any single attribute in the log output.
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { //nolint: revive
// check that we are handling the time key
// Check that we are handling the time key.
if a.Key != slog.TimeKey {
return a
}
@@ -62,7 +63,7 @@ func Init(level string, fileDescriptor *os.File) {
)
}
// Error passes message directly to slog.Error().
// Error passes message directly to [slog.Error()].
func Error(message string) {
slog.Error(message)
}
@@ -72,7 +73,7 @@ func Errorf(format string, a ...any) {
slog.Error(fmt.Sprintf(format, a...))
}
// Warn passes message directly to slog.Warn().
// Warn passes message directly to [slog.Warn()].
func Warn(message string) {
slog.Warn(message)
}
@@ -82,7 +83,7 @@ func Warnf(format string, a ...any) {
slog.Warn(fmt.Sprintf(format, a...))
}
// Info passes message directly to slog.Info().
// Info passes message directly to [slog.Info()].
func Info(message string) {
slog.Info(message)
}
@@ -92,7 +93,7 @@ func Infof(format string, a ...any) {
slog.Info(fmt.Sprintf(format, a...))
}
// Debug passes message directly to slog.Debug().
// Debug passes message directly to [slog.Debug()].
func Debug(message string) {
slog.Debug(message)
}

View File

@@ -1,3 +1,4 @@
// Package monkeyuser implements client for service www.monkeyusers.com.
package monkeyuser
import (
@@ -14,11 +15,13 @@ import (
"golang.org/x/net/html"
)
// MonkeyUsers описывает структуру json-массива ответа www.monkeyusers.com.
type MonkeyUsers []struct {
Title string `json:"title"`
URL string `json:"url"`
}
type (
// MonkeyUsers описывает структуру json-массива ответа www.monkeyusers.com.
MonkeyUsers []struct {
Title string `json:"title"`
URL string `json:"url"`
}
)
// APIClient клиент сервиса monkeyuser.com.
func APIClient(cfg *config.MyConfig) (string, error) {
@@ -75,7 +78,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
return "", fmt.Errorf("unable to GET %s: %w", pageURL, err)
}
// Парсим body ответа от сервера
// Парсим body ответа от сервера.
doc, err := html.Parse(strings.NewReader(body))
if err != nil {
@@ -91,7 +94,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
if n.Type == html.ElementNode && n.Data == "img" {
for _, a_href := range n.Attr { //nolint: revive
// Если у нас class == "lazyload small-image", то после него ключ data-src должен содержать
// искомый relative url
// искомый relative url.
if a_href.Key == "class" && a_href.Val == "logo" {
continue
}
@@ -105,7 +108,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
}
}
// traverses the HTML of the webpage from the first child node
// traverses the HTML of the webpage from the first child node.
for c := n.FirstChild; c != nil; c = c.NextSibling {
link(c)
}
@@ -113,7 +116,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
link(doc)
// Возвращаем ссылку на картинку buni comic
// Возвращаем ссылку на картинку buni comic.
if hyperlink != "" {
return fmt.Sprintf("%s%s", baseURL, hyperlink), nil
}

View File

@@ -1,3 +1,4 @@
// Package oboobs implements api.oboobs.ru service client.
package oboobs
import (
@@ -9,14 +10,16 @@ import (
"github.com/carlmjohnson/requests"
)
// Oboobs структура, возвращаемая на запрос в oboobs.ru.
type Oboobs struct {
ID int `json:"id,omitempty"`
Author string `json:"author,omitempty"`
Rank int `json:"rank,omitempty"`
Model string `json:"model,omitempty"`
Preview string `json:"preview"`
}
type (
// Oboobs структура, возвращаемая на запрос в oboobs.ru.
Oboobs struct { //nolint: govet // Идите в пень, мне так удобнее.
ID int `json:"id,omitempty"`
Author string `json:"author,omitempty"`
Rank int `json:"rank,omitempty"`
Model string `json:"model,omitempty"`
Preview string `json:"preview"`
}
)
// APIClient клиент сервиса oboobs.ru.
func APIClient(cfg *config.MyConfig) (string, error) {

View File

@@ -1,3 +1,4 @@
// Package obutts implements api.obutts.ru service client.
package obutts
import (
@@ -9,14 +10,16 @@ import (
"github.com/carlmjohnson/requests"
)
// Obutts структура, возвращаемая на запрос в obutts.ru.
type Obutts struct {
ID int `json:"id,omitempty"`
Author string `json:"author,omitempty"`
Rank int `json:"rank,omitempty"`
Model string `json:"model,omitempty"`
Preview string `json:"preview"`
}
type (
// Obutts структура, возвращаемая на запрос в obutts.ru.
Obutts struct { //nolint: govet // Идите в пень, мне так удобнее.
ID int `json:"id,omitempty"`
Author string `json:"author,omitempty"`
Rank int `json:"rank,omitempty"`
Model string `json:"model,omitempty"`
Preview string `json:"preview"`
}
)
// APIClient клиент сервиса obutts.ru.
func APIClient(cfg *config.MyConfig) (string, error) {

View File

@@ -1,3 +1,4 @@
// Package openweathermap implements openweatermap.org service client.
package openweathermap
import (
@@ -14,14 +15,16 @@ import (
owm "github.com/briandowns/openweathermap"
)
// Структурка для передачи в обрабтчик запросов в openweathermap api.
type OwmItem struct {
City string
FakeName string
Where string
Lon float64
Lat float64
}
type (
// OwmItem структурка для передачи в обрабтчик запросов в openweathermap api.
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) {
@@ -41,23 +44,29 @@ func OwmClient(cfg *config.MyConfig, city string, days uint32) (string, error) {
// "Нормализуем" название города.
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 = "в городе"
@@ -96,8 +105,8 @@ func OwmAPIClient(cfg *config.MyConfig, item OwmItem) (string, error) {
}
if item.City != "" {
if err = w.CurrentByName(item.City); err != nil {
// TODO: extend error
if err := w.CurrentByName(item.City); err != nil {
// TODO: extend error.
return answer, err
}
@@ -105,8 +114,8 @@ func OwmAPIClient(cfg *config.MyConfig, item OwmItem) (string, error) {
} else {
coord := owm.Coordinates{Longitude: item.Lon, Latitude: item.Lat}
if err = w.CurrentByCoordinates(&coord); err != nil {
// TODO: extend error
if err := w.CurrentByCoordinates(&coord); err != nil {
// TODO: extend error.
return answer, err
}
@@ -291,6 +300,7 @@ func QueryOwmCache(cfg *config.MyConfig, item OwmItem) (string, error) {
// Первое, что приходит в голову - коллизия хэшей 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, здесь мы ничего сделать не можем.
@@ -306,7 +316,7 @@ func QueryOwmCache(cfg *config.MyConfig, item OwmItem) (string, error) {
return answer, err
}
// Кэш просрочен
// Кэш просрочен.
if tsCache < tsNow {
log.Debugf("OWM cache miss for city: %s", key)
@@ -326,6 +336,7 @@ func QueryOwmCache(cfg *config.MyConfig, item OwmItem) (string, error) {
}
key += "+value"
answer = pcachedb.GetValue(cfg, "cache", key)
// Какие-то проблемы с кэшом - ts есть, но самого кэша нету. Более подробная инфа по идее уже попала в лог.
@@ -334,7 +345,7 @@ func QueryOwmCache(cfg *config.MyConfig, item OwmItem) (string, error) {
if len(answer) < 140 {
log.Debugf("OWM cache miss for city: %s", key)
if len(answer) > 0 {
if answer != "" {
log.Errorf("OWM-Cache: record for city %s in cache is too short, less than 140 chars", key)
}
@@ -373,7 +384,7 @@ func UpdateOwmUserCache(cfg *config.MyConfig, chatid string, entry string, value
return err
}
// Возвращает из кэша город, которым интересовался пользователь из даденного чятика.
// QueryOwmUserCache возвращает из кэша город, которым интересовался пользователь из даденного чятика.
func QueryOwmUserCache(cfg *config.MyConfig, chatid string, entry string) string {
// Chatid как есть лучше на диск не пробовать класть, мало ли что туда юзер впишет. Возьмём от него sha256.
var (

View File

@@ -1,3 +1,4 @@
// Package pcachedb implements persistent cache using PebbleDB key-value library.
package pcachedb
import (
@@ -51,12 +52,12 @@ func GetValue(cfg *config.MyConfig, cacheID string, setting string) string {
cacheHash := sha256.Sum256([]byte(cacheID))
database := fmt.Sprintf("cache_db/%x", cacheHash)
// Если БД не открыта, откроем её
// Если БД не открыта, откроем её.
if _, ok := cfg.PcacheDB[database]; !ok {
var options pebble.Options
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
// настройками порождается огромное количество файлов. Умолчальное ограничение на количество файлов - 500 штук,
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений
// что нас не устраивает, поэтому немного снизим эту цифру до более приемлемых значений.
options.L0CompactionFileThreshold = 8
cfg.PcacheDB[database], err = pebble.Open(cfg.DataDir+"/"+database, &options)
@@ -88,7 +89,7 @@ func GetValue(cfg *config.MyConfig, cacheID string, setting string) string {
return value
}
// saveKeyWithValue сохраним данные в БД.
// SaveKeyWithValue сохраним данные в БД.
func SaveKeyWithValue(cfg *config.MyConfig, cacheID string, setting string, value string) error {
var (
cacheHash = sha256.Sum256([]byte(cacheID))
@@ -96,7 +97,7 @@ func SaveKeyWithValue(cfg *config.MyConfig, cacheID string, setting string, valu
err error
)
// Если БД не открыта, откроем её
// Если БД не открыта, откроем её.
if _, ok := cfg.PcacheDB[database]; !ok {
var options pebble.Options
// По дефолту ограничение ставится на мегабайты данных, а не на количество файлов, поэтому с дефолтными
@@ -115,6 +116,12 @@ func SaveKeyWithValue(cfg *config.MyConfig, cacheID string, setting string, valu
if err := StoreKV(cfg.PcacheDB[database], setting, value); err != nil {
log.Errorf("Unable to save setting %s in database %s: %s", setting, database, err)
return err
}
if _, err = cfg.PcacheDB[database].AsyncFlush(); err != nil {
log.Errorf("Unable to flush data to disk for database %s: %s", database, err)
}
return err

View File

@@ -1,3 +1,5 @@
// Package prazdnikisegodnyaru implements prazdniki-segodnya.ru service client.
// Now defunct on Russian Federation area for some legal reasons.
package prazdnikisegodnyaru
import (
@@ -20,14 +22,14 @@ import (
func PsrClient(cfg *config.MyConfig) (string, error) {
tsNow := time.Now()
// Вычислим expiration time для MSK+0 в формате unix timestamp
// Вычислим expiration time для MSK+0 в формате unix timestamp.
loc, _ := time.LoadLocation("Europe/Moscow")
year, month, day := tsNow.In(loc).Date()
eTime := time.Date(year, month, day, 0, 0, 0, 0, loc).AddDate(0, 0, +1).Unix()
tsStr := pcachedb.GetValue(cfg, "cache", "prazdnikisegodhyaru_timestamp")
// Кэш пустой, запросов пока не было
// Кэш пустой, запросов пока не было.
if tsStr == "" {
log.Debugf("PSR no cache entry for prazdnikisegodhyaru")
@@ -49,14 +51,15 @@ func PsrClient(cfg *config.MyConfig) (string, error) {
if err != nil {
log.Debug("PSR cache miss")
log.Warnf("PSR-Cache: unable to parse string as int64 (sha256 collision?): %s", err)
answer, err := PsrAPIClient(cfg)
// Бинго! у нас ещё и ошибка при обращении к "api" prazdnikisegodhyaru, здесь мы ничего сделать не можем
// Бинго! у нас ещё и ошибка при обращении к "api" prazdnikisegodhyaru, здесь мы ничего сделать не можем.
if err != nil {
return "", err
}
// Если всё хорошо, надо обновить кэш
// Если всё хорошо, надо обновить кэш.
if err := UpdatePsrCache(cfg, eTime, answer); err != nil {
log.Errorf("%s", err)
}
@@ -64,21 +67,21 @@ func PsrClient(cfg *config.MyConfig) (string, error) {
return answer, nil
}
// Текущее время в Unix timestamp
// Текущее время в Unix timestamp.
tsNowUnix := tsNow.Unix()
// Кэш просрочен
// Кэш просрочен.
if tsCache < tsNowUnix {
log.Debug("PSR cache miss")
answer, err := PsrAPIClient(cfg)
// Здесь мы ничего сделать не можем
// Здесь мы ничего сделать не можем.
if err != nil {
return "", err
}
// Если всё хорошо, надо обновить кэш
// Если всё хорошо, надо обновить кэш.
if err := UpdatePsrCache(cfg, eTime, answer); err != nil {
log.Errorf("%s", err)
}
@@ -86,8 +89,8 @@ func PsrClient(cfg *config.MyConfig) (string, error) {
return answer, nil
}
// TODO: как-то научиться определять ситуацию с отсутствующим значением в кэше
// TODO: Научиться определять ситуацию, когда значением является unix timestamp
// TODO: как-то научиться определять ситуацию с отсутствующим значением в кэше.
// TODO: Научиться определять ситуацию, когда значением является unix timestamp.
log.Debug("PSR cache hit")
return pcachedb.GetValue(cfg, "cache", "prazdnikisegodhyaru_value"), nil
@@ -97,22 +100,22 @@ func PsrClient(cfg *config.MyConfig) (string, error) {
func PsrAPIClient(cfg *config.MyConfig) (string, error) {
var (
ctx = context.Background()
url = "https://prazdniki-segodnya.ru/"
link = "https://prazdniki-segodnya.ru/"
respBody string
)
// Формальная попытка притвориться браузером
// Формальная попытка притвориться браузером.
_, _, day := time.Now().Date()
userAgent := cfg.UserAgents[day]
if err := requests.URL(url).UserAgent(userAgent).Header("User-Agent", "ru-RU").
if err := requests.URL(link).UserAgent(userAgent).Header("User-Agent", "ru-RU").
Header("Accept-Charset", "utf-8").ToString(&respBody).Fetch(ctx); err != nil {
return "", fmt.Errorf("unable to GET %s: %w", url, err)
return "", fmt.Errorf("unable to GET %s: %w", link, err)
}
var holidays []string
// Парсим body ответа от сервера
// Парсим body ответа от сервера.
tokenizer := html.NewTokenizer(strings.NewReader(respBody))
for {
@@ -122,7 +125,7 @@ func PsrAPIClient(cfg *config.MyConfig) (string, error) {
err := tokenizer.Err()
if errors.Is(err, io.EOF) {
// end of the file, break out of the loop
// end of the file, break out of the loop.
break
}
@@ -139,6 +142,7 @@ func PsrAPIClient(cfg *config.MyConfig) (string, error) {
if tokenType == html.TextToken {
str := tokenizer.Token().Data
holidays = append(holidays, strings.TrimSpace(str))
}
}

View File

@@ -1,3 +1,4 @@
// Package randomfox implements randomfox.ca service client.
package randomfox
import (
@@ -10,11 +11,13 @@ import (
"github.com/carlmjohnson/requests"
)
// RandomFox структура, возвращаемая на запрос в randomfox.ca API.
type RandomFox struct {
Image string `json:"image,omitempty"`
Link string `json:"link"`
}
type (
// RandomFox структура, возвращаемая на запрос в randomfox.ca API.
RandomFox struct {
Image string `json:"image,omitempty"`
Link string `json:"link"`
}
)
// APIClient клиент сервиса randomfox.ca.
func APIClient(cfg *config.MyConfig) (string, error) {

View File

@@ -1,3 +1,4 @@
// Package thecatapi implements api.thecatapi.com service client.
package thecatapi
import (
@@ -9,16 +10,18 @@ import (
"github.com/carlmjohnson/requests"
)
// TheCatAPIinnerStruct структурка, описывающая элемент массива, возвращемый Тhe cat api.
type TheCatAPIinnerStruct struct {
ID string `json:"id,omitempty"`
URL string `json:"url"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
}
type (
// TheCatAPIinnerStruct структурка, описывающая элемент массива, возвращемый Тhe cat api.
TheCatAPIinnerStruct struct {
ID string `json:"id,omitempty"`
URL string `json:"url"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
}
// TheCatAPI структурка, описывающая массив, возвращаемый на запрос урла картинки из The Cat API.
type TheCatAPI []TheCatAPIinnerStruct
// TheCatAPI структурка, описывающая массив, возвращаемый на запрос урла картинки из The Cat API.
TheCatAPI []TheCatAPIinnerStruct
)
// APIClient клиент сервиса thecatapi.com.
func APIClient(cfg *config.MyConfig) (string, error) {
@@ -29,11 +32,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
cat TheCatAPI
)
err := requests.
URL(url).
UserAgent(userAgent).
ToJSON(&cat).
Fetch(ctx)
err := requests.URL(url).UserAgent(userAgent).ToJSON(&cat).Fetch(ctx)
if err != nil {
return "", fmt.Errorf("unable to GET %s: %w", url, err)

View File

@@ -1,8 +1,10 @@
// Package webapp is main package implementing application logic.
package webapp
import (
"aleesa-webapp-go/internal/anekdotru"
"aleesa-webapp-go/internal/bunicomic"
"aleesa-webapp-go/internal/celebratoday"
"aleesa-webapp-go/internal/config"
"aleesa-webapp-go/internal/flickr"
"aleesa-webapp-go/internal/log"
@@ -10,7 +12,6 @@ import (
"aleesa-webapp-go/internal/oboobs"
"aleesa-webapp-go/internal/obutts"
"aleesa-webapp-go/internal/openweathermap"
"aleesa-webapp-go/internal/prazdnikisegodnyaru"
"aleesa-webapp-go/internal/randomfox"
"aleesa-webapp-go/internal/thecatapi"
"aleesa-webapp-go/internal/xkcdru"
@@ -49,7 +50,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
return
}
// Если у нас циклическая пересылка сообщения, попробуем её тут разорвать, отбросив сообщение
// Если у нас циклическая пересылка сообщения, попробуем её тут разорвать, отбросив сообщение.
if j.Misc.Fwdcnt > cfg.ForwardsMax {
log.Warnf("Discarding msg with fwd_cnt exceeding max value: %s", msg)
@@ -60,15 +61,15 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
sendTo = j.Plugin
// Классифицируем входящие сообщения. Первым делом, попробуем определить команды
// Классифицируем входящие сообщения. Первым делом, попробуем определить команды.
if j.Message[0:len(j.Misc.Csign)] == j.Misc.Csign {
var cmd = j.Message[len(j.Misc.Csign):]
switch {
case cmd == "cat" || cmd == "кис":
// Пытаемся вычитать ответ thecatapi 3 раза
for i := 0; i < 3; i++ {
answer, err = thecatapi.APIClient(cfg)
// Пытаемся вычитать ответ thecatapi 3 раза.
for i := range 3 {
answer, err = thecatapi.APIClient(cfg) //nolint: contextcheck // Fuck off.
if err != nil {
log.Errorf("Try %d/3 unable to query api.thecatapi.com: %v", i+1, err)
@@ -83,18 +84,20 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
if answer == "" {
j.Message = "Нету кошечек, все разбежались."
} else {
// for telegram return answer in markdown format
// for telegram return answer in markdown format.
if j.Plugin == "telegram" {
cats := []string{"龴ↀ◡ↀ龴", "=^..^=", "≧◔◡◔≦", "^ↀᴥↀ^"}
cats := []string{"龴ↀ◡ↀ龴", "=^..^=", "≧◔◡◔≦", "^ↀᴥↀ^"} //nolint: gosmopolitan
j.Message = fmt.Sprintf("[%s](%s)", cats[rand.IntN(len(cats))], answer)
} else {
j.Message = answer
}
}
case cmd == "fox" || cmd == "лис":
// Пытаемся вычитать ответ randonfox.ca 3 раза
for i := 0; i < 3; i++ {
answer, err = randomfox.APIClient(cfg)
// Пытаемся вычитать ответ randonfox.ca 3 раза.
for i := range 3 {
answer, err = randomfox.APIClient(cfg) //nolint: contextcheck // Fuck off.
if err != nil {
log.Errorf("Try %d/3 unable to query randomfox.ca: %v", i+1, err)
@@ -115,9 +118,10 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
j.Message = answer
}
}
case cmd == "xkcd":
// Пытаемся вычитать ответ xkcd.ru 3 раза
for i := 0; i < 3; i++ {
// Пытаемся вычитать ответ xkcd.ru 3 раза.
for i := range 3 {
answer, err = xkcdru.APIClient(cfg)
if err != nil {
@@ -139,10 +143,11 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
j.Message = answer
}
}
case cmd == "buni":
// Пытаемся вычитать ответ www.bunicomic.com 3 раза
for i := 0; i < 3; i++ {
answer, err = bunicomic.APIClient(cfg)
// Пытаемся вычитать ответ www.bunicomic.com 3 раза.
for i := range 3 {
answer, err = bunicomic.APIClient(cfg) //nolint: contextcheck // Fuck off.
if err != nil {
log.Errorf("Try %d/3 unable to query www.bunicomic.com: %v", i+1, err)
@@ -163,10 +168,11 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
j.Message = answer
}
}
case cmd == "anek" || cmd == "анек" || cmd == "анекдот":
// Пытаемся вычитать ответ www.anekdot.ru 3 раза
for i := 0; i < 3; i++ {
answer, err = anekdotru.APIClient(cfg)
// Пытаемся вычитать ответ www.anekdot.ru 3 раза.
for i := range 3 {
answer, err = anekdotru.APIClient(cfg) //nolint: contextcheck // Fuck off.
if err != nil {
log.Errorf("Try %d/3 unable to query www.anekdot.ru: %v", i+1, err)
@@ -187,10 +193,11 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
j.Message = answer
}
}
case cmd == "monkeyuser":
// Пытаемся вычитать ответ www.bunicomic.com 3 раза
for i := 0; i < 3; i++ {
answer, err = monkeyuser.APIClient(cfg)
// Пытаемся вычитать ответ monkeyuser.com 3 раза.
for i := range 3 {
answer, err = monkeyuser.APIClient(cfg) //nolint: contextcheck // Fuck off.
if err != nil {
log.Errorf("Try %d/3 unable to query monkeyuser.com: %v", i+1, err)
@@ -211,10 +218,11 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
j.Message = answer
}
}
case cmd == "butt" || cmd == "booty" || cmd == "ass" || cmd == "попа" || cmd == "попка":
// Пытаемся вычитать ответ api.obutts.ru 3 раза
for i := 0; i < 3; i++ {
answer, err = obutts.APIClient(cfg)
// Пытаемся вычитать ответ api.obutts.ru 3 раза.
for i := range 3 {
answer, err = obutts.APIClient(cfg) //nolint: contextcheck // Fuck off.
if err != nil {
log.Errorf("Try %d/3 unable to query api.obutts.ru: %v", i+1, err)
@@ -231,15 +239,17 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
} else {
if j.Plugin == "telegram" {
arts := []string{"(__(__)", "(_!_)", "(__.__)"}
j.Message = fmt.Sprintf("[%s](%s)", arts[rand.IntN(len(arts))], answer)
} else {
j.Message = answer
}
}
case cmd == "titts" || cmd == "boobs" || cmd == "tities" || cmd == "boobies" || cmd == "сиси" || cmd == "сисечки":
// Пытаемся вычитать ответ api.oboobs.ru 3 раза
for i := 0; i < 3; i++ {
answer, err = oboobs.APIClient(cfg)
// Пытаемся вычитать ответ api.oboobs.ru 3 раза.
for i := range 3 {
answer, err = oboobs.APIClient(cfg) //nolint: contextcheck // Fuck off.
if err != nil {
log.Errorf("Try %d/3 unable to query api.oboobs.ru: %v", i+1, err)
@@ -256,18 +266,20 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
} else {
if j.Plugin == "telegram" {
arts := []string{"(. )( .)", "( . Y . )", "(o)(o)", "( @ )( @ )", "(.)(.)"}
j.Message = fmt.Sprintf("[%s](%s)", arts[rand.IntN(len(arts))], answer)
} else {
j.Message = answer
}
}
case cmd == "drink" || cmd == "праздник":
// Пытаемся вычитать ответ prazdniki-segodnya.ru 3 раза
for i := 0; i < 3; i++ {
answer, err = prazdnikisegodnyaru.PsrClient(cfg)
// Пытаемся вычитать ответ celebratoday.com 3 раза.
for i := range 3 {
answer, err = celebratoday.Client(cfg) //nolint: contextcheck // Fuck off.
if err != nil {
log.Errorf("Try %d/3 unable to query prazdniki-segodnya.ru: %v", i+1, err)
log.Errorf("Try %d/3 unable to query celebratoday.com: %v", i+1, err)
time.Sleep(1 * time.Second)
continue
@@ -283,8 +295,8 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
}
case cmd == "frog" || cmd == "лягушка":
for i := 0; i < 3; i++ {
answer, err = flickr.SearchByTags(cfg, []string{"frog", "toad", "amphibian"})
for i := range 3 {
answer, err = flickr.SearchByTags(cfg, []string{"frog", "toad", "amphibian"}) //nolint: contextcheck // Fuck off.
if err != nil {
log.Errorf("Try %d/3 unable to query flickr api about frogs: %s", i+1, err)
@@ -302,6 +314,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
// Костыль для красивого ответа в telegram.
if j.Plugin == "telegram" {
art := []string{"frog", "toad", "лягушка"}
answer = fmt.Sprintf("[%s](%s)", art[rand.IntN(len(art))], answer)
}
@@ -309,8 +322,8 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
}
case cmd == "owl" || cmd == "сова" || cmd == "сыч":
for i := 0; i < 3; i++ {
answer, err = flickr.SearchByTags(cfg, []string{"owlet", "owl", "raptor", "bird of prey", "nocturnal bird", "barn owl", "tawny owl", "brown owl"})
for i := range 3 {
answer, err = flickr.SearchByTags(cfg, []string{"owlet", "owl", "raptor", "bird of prey", "nocturnal bird", "barn owl", "tawny owl", "brown owl"}) //nolint: contextcheck // Fuck off.
if err != nil {
log.Errorf("Try %d/3 unable to query flickr api about owls: %s", i+1, err)
@@ -334,8 +347,8 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
}
case cmd == "horse" || cmd == "лошадь" || cmd == "лошадка":
for i := 0; i < 3; i++ {
answer, err = flickr.SearchByTags(cfg, []string{"horse", "equine", "stallion", "mare", "steed", "thoroughbred"})
for i := range 3 {
answer, err = flickr.SearchByTags(cfg, []string{"horse", "equine", "stallion", "mare", "steed", "thoroughbred"}) //nolint: contextcheck // Fuck off.
if err != nil {
log.Errorf("Try %d/3 unable to query flickr api about horses: %s", i+1, err)
@@ -353,6 +366,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
// Костыль для красивого ответа в telegram.
if j.Plugin == "telegram" {
art := []string{"horse", "лошадь", "лошадка"}
answer = fmt.Sprintf("[%s](%s)", art[rand.IntN(len(art))], answer)
}
@@ -360,8 +374,8 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
}
case cmd == "rabbit" || cmd == "bunny" || cmd == "кролик":
for i := 0; i < 3; i++ {
answer, err = flickr.SearchByTags(cfg, []string{"bunny", "rabbit", "buck"})
for i := range 3 {
answer, err = flickr.SearchByTags(cfg, []string{"bunny", "rabbit", "buck"}) //nolint: contextcheck // Fuck off.
if err != nil {
log.Errorf("Try %d/3 unable to query flickr api about rabbits: %s", i+1, err)
@@ -385,8 +399,8 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
}
case cmd == "snail" || cmd == "улитка":
for i := 0; i < 3; i++ {
answer, err = flickr.SearchByTags(cfg, []string{"snail", "slug"})
for i := range 3 {
answer, err = flickr.SearchByTags(cfg, []string{"snail", "slug"}) //nolint: contextcheck // Fuck off.
if err != nil {
log.Errorf("Try %d/3 unable to query flickr api about snails: %s", i+1, err)
@@ -404,6 +418,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
// Костыль для красивого ответа в telegram.
if j.Plugin == "telegram" {
art := []string{"'-'_@_", "@╜", "@_'-'"}
answer = fmt.Sprintf("[%s](%s)", art[rand.IntN(len(art))], answer)
}
@@ -419,6 +434,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
} else {
if answer, err := openweathermap.OwmClient(cfg, city, 0); err != nil {
log.Errorf("Unable to handle city %s in openweartermap api: %s", city, err)
j.Message = "Я не знаю, какая погода в " + city
} else {
j.Message = answer
@@ -433,6 +449,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
if regexp.MustCompile(`5$`).MatchString(cmdStr) {
if answer, err := openweathermap.OwmClient(cfg, city, 5); err != nil {
log.Errorf("Unable to handle city %s in openweartermap api: %s", city, err)
j.Message = "Я не знаю, какая погода в " + city
} else {
j.Message = answer
@@ -440,6 +457,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
} else {
if answer, err := openweathermap.OwmClient(cfg, city, 0); err != nil {
log.Errorf("Unable to handle city %s in openweartermap api: %s", city, err)
j.Message = "Я не знаю, какая погода в " + city
} else {
j.Message = answer
@@ -467,7 +485,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
return
}
// Настало время формировать json и засылать его в дальше
// Настало время формировать json и засылать его в дальше.
var message SMsg
message.From = j.From
@@ -493,7 +511,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint
return
}
// Заталкиваем наш json в редиску
// Заталкиваем наш json в редиску.
if err := cfg.RedisClient.Publish(ctx, sendTo, data).Err(); err != nil {
log.Warnf("Unable to send data to redis channel %s: %s", sendTo, err)
} else {

View File

@@ -25,12 +25,12 @@ func ReadConfig() error {
for _, location := range locations {
fileInfo, err := os.Stat(location)
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём
// Предполагаем, что файла либо нет, либо мы не можем его прочитать, второе надо бы логгировать, но пока забьём.
if err != nil {
continue
}
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата
// Конфиг-файл длинноват для конфига, попробуем следующего кандидата.
if fileInfo.Size() > ConfigFileSize {
log.Warnf("Config file %s is too long for config, skipping", location)
@@ -39,7 +39,7 @@ func ReadConfig() error {
buf, err := os.ReadFile(location)
// Не удалось прочитать, попробуем следующего кандидата
// Не удалось прочитать, попробуем следующего кандидата.
if err != nil {
log.Warnf("Skip reading config file %s: %s", location, err)
@@ -51,12 +51,12 @@ func ReadConfig() error {
// сериализуем в json, а потом json преврщааем в стркутурку. Не очень эффективно, но он и не часто требуется.
var (
sampleConfig *config.MyConfig
tmp map[string]interface{}
tmp map[string]any
)
err = hjson.Unmarshal(buf, &tmp)
// Не удалось распарсить - попробуем следующего кандидата
// Не удалось распарсить - попробуем следующего кандидата.
if err != nil {
log.Warnf("Skip parsing config file %s: %s", location, err)
@@ -65,7 +65,7 @@ func ReadConfig() error {
tmpjson, err := json.Marshal(tmp)
// Не удалось преобразовать map-ку в json
// Не удалось преобразовать map-ку в json.
if err != nil {
log.Warnf("Skip parsing config file %s: %s", location, err)
@@ -78,7 +78,7 @@ func ReadConfig() error {
continue
}
// Валидируем значения из конфига
// Валидируем значения из конфига.
if sampleConfig.Server == "" {
sampleConfig.Server = Host
}
@@ -95,7 +95,7 @@ func ReadConfig() error {
sampleConfig.Loglevel = Loglevel
}
// sampleConfig.Log = "" if not set
// sampleConfig.Log = "" if not set.
if sampleConfig.Channel == "" {
log.Errorf("Channel field in config file %s must be set", location)
@@ -129,7 +129,7 @@ func ReadConfig() error {
continue
}
// sampleConfig.OpenWeatherMap.Country is false by default
// sampleConfig.OpenWeatherMap.Country is false by default.
// Теперь надо бы попробовать загрузить список User Agent-ов, с которыми ходить в разные апишки.
uaFile := sampleConfig.DataDir + "/useragents.txt"

View File

@@ -21,14 +21,14 @@ func SigHandler(cfg *config.MyConfig) {
case syscall.SIGQUIT:
log.Info("Got SIGQUIT, quitting")
// Заходим на новую итерацию, если у нас "неинтересный" сигнал
// Заходим на новую итерацию, если у нас "неинтересный" сигнал.
default:
continue
}
Shutdown = true
// Отпишемся от всех каналов и закроем коннект к редиске
// Отпишемся от всех каналов и закроем коннект к редиске.
if err = Subscriber.Unsubscribe(Ctx); err != nil {
log.Errorf("Unable to unsubscribe from redis channels cleanly: %s", err)
}

View File

@@ -45,21 +45,21 @@ func ValidateRmsg(cfg *config.MyConfig, j RMsg, msg string) (RMsg, error) {
return j, emsg
}
// j.Misc.Answer может и не быть, тогда ответа на такое сообщение не будет
// j.Misc.Botnick тоже можно не передавать, тогда будет записана пустая строка
// j.Misc.Csign если нам его не передали, возьмём значение из конфига
// j.Misc.Answer может и не быть, тогда ответа на такое сообщение не будет.
// j.Misc.Botnick тоже можно не передавать, тогда будет записана пустая строка.
// j.Misc.Csign если нам его не передали, возьмём значение из конфига.
if exist := j.Misc.Csign; exist == "" {
j.Misc.Csign = cfg.Csign
}
// j.Misc.Fwdcnt если нам его не передали, то будет 0
// j.Misc.Fwdcnt если нам его не передали, то будет 0.
if exist := j.Misc.Fwdcnt; exist == 0 {
j.Misc.Fwdcnt = 1
}
// j.Misc.GoodMorning может быть быть 1 или 0, по-умолчанию 0
// j.Misc.Msgformat может быть быть 1 или 0, по-умолчанию 0
// j.Misc.Username можно не передавать, тогда будет пустая строка
// j.Misc.GoodMorning может быть быть 1 или 0, по-умолчанию 0.
// j.Misc.Msgformat может быть быть 1 или 0, по-умолчанию 0.
// j.Misc.Username можно не передавать, тогда будет пустая строка.
return j, nil
}

View File

@@ -33,7 +33,7 @@ var (
// Config структурка, содержащая данные конфига.
Config *config.MyConfig
// ctx контекст редиски.
// Ctx контекст редиски.
Ctx = context.Background()
// Subscriber объектик сабскрайбера редиски.

View File

@@ -1,43 +1,45 @@
package webapp
// RMsg структурка, описывающая входящее сообщение из pubsub-канала redis-ки.
type RMsg struct {
From string `json:"from,omitempty"`
Chatid string `json:"chatid,omitempty"`
Userid string `json:"userid,omitempty"`
Threadid string `json:"threadid,omitempty"`
Message string `json:"message,omitempty"`
Plugin string `json:"plugin,omitempty"`
Mode string `json:"mode,omitempty"`
Misc struct {
Answer int64 `json:"answer,omitempty"`
Botnick string `json:"bot_nick,omitempty"`
Csign string `json:"csign,omitempty"`
Fwdcnt int64 `json:"fwd_cnt,omitempty"`
GoodMorning int64 `json:"good_morning,omitempty"`
Msgformat int64 `json:"msg_format,omitempty"`
Username string `json:"username,omitempty"`
} `json:"Misc"`
}
type (
// RMsg структурка, описывающая входящее сообщение из pubsub-канала redis-ки.
RMsg struct {
From string `json:"from,omitempty"`
Chatid string `json:"chatid,omitempty"`
Userid string `json:"userid,omitempty"`
Threadid string `json:"threadid,omitempty"`
Message string `json:"message,omitempty"`
Plugin string `json:"plugin,omitempty"`
Mode string `json:"mode,omitempty"`
Misc struct { //nolint: govet // Мне так удобнее.
Answer int64 `json:"answer,omitempty"`
Botnick string `json:"bot_nick,omitempty"`
Csign string `json:"csign,omitempty"`
Fwdcnt int64 `json:"fwd_cnt,omitempty"`
GoodMorning int64 `json:"good_morning,omitempty"`
Msgformat int64 `json:"msg_format,omitempty"`
Username string `json:"username,omitempty"`
} `json:"Misc"`
}
// SMsg структурка, описывающая исходящее сообщение в pubsub-канал redis-ки.
type SMsg struct {
From string `json:"from"`
Chatid string `json:"chatid"`
Userid string `json:"userid"`
Threadid string `json:"threadid"`
Message string `json:"message"`
Plugin string `json:"plugin"`
Mode string `json:"mode"`
Misc struct {
Answer int64 `json:"answer"`
Botnick string `json:"bot_nick"`
Csign string `json:"csign"`
Fwdcnt int64 `json:"fwd_cnt"`
GoodMorning int64 `json:"good_morning"`
Msgformat int64 `json:"msg_format"`
Username string `json:"username"`
} `json:"misc"`
}
// SMsg структурка, описывающая исходящее сообщение в pubsub-канал redis-ки.
SMsg struct {
From string `json:"from"`
Chatid string `json:"chatid"`
Userid string `json:"userid"`
Threadid string `json:"threadid"`
Message string `json:"message"`
Plugin string `json:"plugin"`
Mode string `json:"mode"`
Misc struct { //nolint: govet // Мне так удобнее.
Answer int64 `json:"answer"`
Botnick string `json:"bot_nick"`
Csign string `json:"csign"`
Fwdcnt int64 `json:"fwd_cnt"`
GoodMorning int64 `json:"good_morning"`
Msgformat int64 `json:"msg_format"`
Username string `json:"username"`
} `json:"misc"`
}
)
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */

View File

@@ -1,3 +1,4 @@
// Package xkcdru provides api to xkcd.ru.
package xkcdru
import (
@@ -25,7 +26,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
}
)
req, err := http.NewRequest(http.MethodGet, url, nil) //nolint: noctx
req, err := http.NewRequest(http.MethodGet, url, http.NoBody) //nolint: noctx
if err != nil {
return "", err
@@ -48,7 +49,7 @@ func APIClient(cfg *config.MyConfig) (string, error) {
}
}(resp.Body)
// Response body нам не интересен, нам интересен статус и нам интересен заголовок Location
// Response body нам не интересен, нам интересен статус и нам интересен заголовок Location.
if resp.StatusCode == 302 {
location := resp.Header.Get("Location")