239 lines
6.7 KiB
Go
239 lines
6.7 KiB
Go
// 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
|
|
|
|
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: */
|