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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
223
internal/celebratoday/celebratoday.go
Normal file
223
internal/celebratoday/celebratoday.go
Normal 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: */
|
||||
@@ -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: */
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ var (
|
||||
// Config структурка, содержащая данные конфига.
|
||||
Config *config.MyConfig
|
||||
|
||||
// ctx контекст редиски.
|
||||
// Ctx контекст редиски.
|
||||
Ctx = context.Background()
|
||||
|
||||
// Subscriber объектик сабскрайбера редиски.
|
||||
|
||||
@@ -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: */
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user