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
Телеграммный бот для канала с праздниками. Раз в сутки забирает список праздников с к сайта celebratoday.com и
показывает его на канальчике в телеграме.
показывает его на канальчике в телеграме. Создан исключительно в обучающих целях, как пример приложения на языке go.
## Как это собрать?

View File

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

View File

@@ -24,12 +24,14 @@ func APIClient() (string, error) {
// Формальная попытка притвориться браузером.
_, _, 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).
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-Language", "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7").
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.
}
@@ -70,6 +72,8 @@ func parser(data string) ([]string, error) {
// А теперь пробежимся по полученной структуре и вытянем из неё все строки с праздниками.
for _, ass := range s {
for _, entity := range ass.MainEntity {
holidays = append(holidays, entity.AcceptedAnswer.Text)
for _, sugestedAnswer := range entity.SuggestedAnswer {
holidays = append(holidays, sugestedAnswer.Text)
}

View File

@@ -2,18 +2,56 @@ package holy
import (
"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() {
var (
err error
errcnt int
phrase string
timeout time.Duration
)
log.Info("Cron Task triggered")
phrase, err := APIClient()
for {
phrase, err = APIClient()
if err != nil {
log.Errorf("Unable to get holidays: %s", err)
if err != nil {
errcnt++
return
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
}
} else {
break
}
time.Sleep(timeout)
}
if _, err = Bot.SendMessage(phrase, Config.ChannelID, nil); err != nil {

View File

@@ -3,6 +3,7 @@ package holy
import (
"encoding/json"
"errors"
"fmt"
"os"
"tg-holy-bot/internal/log"
@@ -45,7 +46,7 @@ func ReadConfig() error {
// Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку.
// Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку
// сериализуем в json, а потом json преврщааем в стркутурку. Не очень эффективно, но он и не часто требуется.
// сериализуем в json, а потом json преврщааем в стркутурку. Не очень эффективно, но оно и не часто требуется.
var (
sampleConfig *MyConfig
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.
}
// 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)
Config = sampleConfig

View File

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

View File

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