Compare commits

...

6 Commits

Author SHA1 Message Date
eleksir
11ea7b8398 Select correct lang in request, use timeout for request, fix sighandler panic 2026-05-03 16:41:32 +03:00
eleksir
7d3c083cea Fix counting errors. 2026-03-31 22:25:35 +03:00
eleksir
dbfbd6865a Fix off-by-one bug in celebratoday.go 2026-03-31 22:12:01 +03:00
eleksir
27ce4d3c78 Add .gitignore, rename config.json to config_example.json 2026-03-21 12:18:39 +03:00
eleksir
693996a262 Try harder to get holidays 2026-03-21 12:14:26 +03:00
eleksir
7da7f38e13 Add AcceptedAnswer.Text entity to list of holidays, fix some comments 2026-03-20 00:09:48 +03:00
9 changed files with 67 additions and 13 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/tg-holy-bot
/data/config.json

View File

@@ -1,7 +1,7 @@
## tg-holy-bot ## tg-holy-bot
Телеграммный бот для канала с праздниками. Раз в сутки забирает список праздников с к сайта celebratoday.com и Телеграммный бот для канала с праздниками. Раз в сутки забирает список праздников с к сайта celebratoday.com и
показывает его на канальчике в телеграме. показывает его на канальчике в телеграме. Создан исключительно в обучающих целях, как пример приложения на языке go.
## Как это собрать? ## Как это собрать?

View File

@@ -61,7 +61,7 @@ func main() {
log.Infof("Stop cron") log.Infof("Stop cron")
// Jobs usually runs in a fraction of second, don't bother waiting for their completion. // Кроновые задачки выполняются за доли сеекунды, не будем заморачиваться ожиданием их завершения.
_ = c.Stop() _ = c.Stop()
log.Infof("Shutdown bot") log.Infof("Shutdown bot")

View File

@@ -24,12 +24,14 @@ func APIClient() (string, error) {
// Формальная попытка притвориться браузером. // Формальная попытка притвориться браузером.
_, _, day := time.Now().Date() _, _, day := time.Now().Date()
userAgent := Config.UserAgents[day] userAgent := Config.UserAgents[day-1]
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if err := requests.URL(url).UserAgent(userAgent). 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", "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("Accept-Language", "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7").
Header("User-Agent", "ru-RU").
Header("Accept-Charset", "utf-8").ToString(&respBody).Fetch(ctx); err != nil { Header("Accept-Charset", "utf-8").ToString(&respBody).Fetch(ctx); err != nil {
return "", fmt.Errorf("Unable to GET %s: %w", url, err) //nolint: staticcheck // Looks like golangci-lint ignoring settings about fucking ST1005 check. We want readable errors despite what wants author of linter or assholes from go dev team. return "", fmt.Errorf("Unable to GET %s: %w", url, err) //nolint: staticcheck // Looks like golangci-lint ignoring settings about fucking ST1005 check. We want readable errors despite what wants author of linter or assholes from go dev team.
} }
@@ -70,6 +72,8 @@ func parser(data string) ([]string, error) {
// А теперь пробежимся по полученной структуре и вытянем из неё все строки с праздниками. // А теперь пробежимся по полученной структуре и вытянем из неё все строки с праздниками.
for _, ass := range s { for _, ass := range s {
for _, entity := range ass.MainEntity { for _, entity := range ass.MainEntity {
holidays = append(holidays, entity.AcceptedAnswer.Text)
for _, sugestedAnswer := range entity.SuggestedAnswer { for _, sugestedAnswer := range entity.SuggestedAnswer {
holidays = append(holidays, sugestedAnswer.Text) holidays = append(holidays, sugestedAnswer.Text)
} }

View File

@@ -2,19 +2,57 @@ package holy
import ( import (
"tg-holy-bot/internal/log" "tg-holy-bot/internal/log"
"time"
) )
// CronTask sends generated hiliday list to the channel. const maxRetryAttempts = 15
// CronTask sends generated holiday list to the channel.
func CronTask() { func CronTask() {
var (
err error
errcnt int
phrase string
timeout time.Duration
)
log.Info("Cron Task triggered") log.Info("Cron Task triggered")
phrase, err := APIClient() for {
phrase, err = APIClient()
if err != nil { if err != nil {
log.Errorf("Unable to get holidays: %s", err) errcnt++
log.Errorf("Unable to get holidays (try %d/%d): %s", errcnt, maxRetryAttempts, err)
switch errcnt {
case 1, 2:
timeout = 1 * time.Second
case 3, 4, 5:
timeout = 5 * time.Second
case 6, 7, 8:
timeout = 1 * time.Minute
case 9, 10, 11:
timeout = 5 * time.Minute
case 12, 13, 14:
timeout = 1 * time.Hour
default:
log.Error("Giveup trying to get holidays")
return return
} }
} else {
break
}
time.Sleep(timeout)
}
if _, err = Bot.SendMessage(phrase, Config.ChannelID, nil); err != nil { if _, err = Bot.SendMessage(phrase, Config.ChannelID, nil); err != nil {
log.Errorf("Unable to send message to channel: %s", err) log.Errorf("Unable to send message to channel: %s", err)

View File

@@ -3,6 +3,7 @@ package holy
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"os" "os"
"tg-holy-bot/internal/log" "tg-holy-bot/internal/log"
@@ -45,7 +46,7 @@ func ReadConfig() error {
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку. // Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку // Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
// сериализуем в json, а потом json преврщааем в стркутурку. Не очень эффективно, но он и не часто требуется. // сериализуем в json, а потом json преврщааем в стркутурку. Не очень эффективно, но оно и не часто требуется.
var ( var (
sampleConfig *MyConfig sampleConfig *MyConfig
tmp map[string]any tmp map[string]any
@@ -103,6 +104,13 @@ func ReadConfig() error {
return errors.New("Amount of user_agent strings in config must be exactly 31! Refusing to start") //nolint: staticcheck // Looks like golangci-lint ignoring settings about fucking ST1005 check. We want readable errors despite what wants author of linter or assholes from go dev team. return errors.New("Amount of user_agent strings in config must be exactly 31! Refusing to start") //nolint: staticcheck // Looks like golangci-lint ignoring settings about fucking ST1005 check. We want readable errors despite what wants author of linter or assholes from go dev team.
} }
// Validate that all user agent strings are non-empty
for i, userAgent := range sampleConfig.UserAgents {
if userAgent == "" {
return fmt.Errorf("User agent string at index %d is empty! Refusing to start", i) //nolint: staticcheck // Looks like golangci-lint ignoring settings about fucking ST1005 check. We want readable errors despite what wants author of linter or assholes from go dev team.
}
}
log.Infof("Using %s as config file", location) log.Infof("Using %s as config file", location)
Config = sampleConfig Config = sampleConfig

View File

@@ -24,8 +24,10 @@ func SigHandler() {
} }
Shutdown = true Shutdown = true
close(Run) close(Run)
// Ensure we exit cleanly after handling the signal
return
} }
} }

View File

@@ -1,7 +1,7 @@
package holy package holy
type ( type (
// Holy структура, описывающая искомый json-чик. // Holy структура, описывающая искомый json-чик с праздниками.
Holy struct { Holy struct {
AtContext string `json:"@context"` AtContext string `json:"@context"`
AtType string `json:"@type"` AtType string `json:"@type"`