diff --git a/.gitignore b/.gitignore index 33fcbc6..60c0b97 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /flickr_init /flickr_populate /flickr_test +/.golangci.bck.yml diff --git a/.golangci.yml b/.golangci.yml index 4e47bf3..b1f27e6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,50 +1,35 @@ +version: "2" run: - tests: false - skip-dirs: - - data - - vendor + go: "1.22" modules-download-mode: readonly + tests: false allow-parallel-runners: true - go: '1.22' -output: - sort-results: true -linters-settings: - gosec: - excludes: - # Оно не используется для повышения безопасности или каких-то таких секъюрных вещей, оно используется для выборки - # произвольного элемента из массива, так что ИДИ НАХУЙ с этой проверкой и не еби мозги липовой безопасностью. - - G404 # Insecure random number source (rand) - # Мне не нужны шашечки, мне ехать надо. - - G402 # TLS InsecureSkipVerify set true linters: - disable-all: true + default: none enable: - - errcheck - - gosimple - - govet - - ineffassign - - staticcheck - - unused - asasalint - asciicheck - bodyclose - containedctx - - contextcheck + - copyloopvar - decorder - durationcheck + - errcheck - errchkjson - errorlint - exhaustive - - exportloopref - ginkgolinter - gocheckcompilerdirectives - gocritic - godot - - gofmt - - goimports + - goheader - gosec + - govet - grouper + - iface - importas + - inamedparam + - ineffassign - ireturn - makezero - nakedret @@ -54,13 +39,43 @@ linters: - noctx - nolintlint - nonamedreturns + - perfsprint - predeclared - promlinter - reassign - revive - rowserrcheck - - stylecheck + - sloglint + - staticcheck - unconvert + - unparam + - unused - wastedassign - whitespace - - wsl + - wsl_v5 + settings: + gosec: + excludes: + - G404 + - G402 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/cmd/aleesa-webapp-go/main.go b/cmd/aleesa-webapp-go/main.go index 8ec40eb..17d3e9d 100644 --- a/cmd/aleesa-webapp-go/main.go +++ b/cmd/aleesa-webapp-go/main.go @@ -1,34 +1,18 @@ package main import ( - "context" "fmt" "os" "os/signal" "syscall" "time" - "aleesa-webapp-go/internal/config" "aleesa-webapp-go/internal/log" "aleesa-webapp-go/internal/webapp" "github.com/go-redis/redis/v8" ) -var ( - // ctx контекст редиски. - Ctx = context.Background() - - // Subscriber объектик сабскрайбера редиски. - Subscriber *redis.PubSub - - // Shutdown ставится в true, если мы получили сигнал на выключение. - Shutdown = false - - // SigChan канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов. - SigChan = make(chan os.Signal, 1) -) - // Основная функция программы, не добавить и не убавить. func main() { var ( @@ -57,74 +41,30 @@ func main() { // Иницализируем клиента Редиски. webapp.Config.RedisClient = redis.NewClient(&redis.Options{ Addr: fmt.Sprintf("%s:%d", webapp.Config.Server, webapp.Config.Port), - }).WithContext(Ctx).WithTimeout(time.Duration(webapp.Config.Timeout) * time.Second) + }).WithContext(webapp.Ctx).WithTimeout(time.Duration(webapp.Config.Timeout) * time.Second) // Обозначим, что хотим после соединения подписаться на события из канала config.Channel. - Subscriber = webapp.Config.RedisClient.Subscribe(Ctx, webapp.Config.Channel) + webapp.Subscriber = webapp.Config.RedisClient.Subscribe(webapp.Ctx, webapp.Config.Channel) // Самое время поставить трапы на сигналы. - signal.Notify(SigChan, + signal.Notify(webapp.SigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) // Запустим обработчик сигналов. - go SigHandler(webapp.Config) + go webapp.SigHandler(webapp.Config) // Начнём выгребать события из редиски (длина конвеера/буфера канала по-умолчанию - 100 сообщений). - ch := Subscriber.Channel() + ch := webapp.Subscriber.Channel() log.Info("Service started.") for msg := range ch { - if !Shutdown { - webapp.MsgParser(webapp.Config, Ctx, msg.Payload) + if !webapp.Shutdown { + webapp.MsgParser(webapp.Config, webapp.Ctx, msg.Payload) } } } -// SigHandler хэндлер сигналов закрывает все бд и сваливает из приложения. -func SigHandler(cfg *config.MyConfig) { - // TODO: утащить отсюда в модули. - - var err error - - for { - var s = <-SigChan - switch s { - case syscall.SIGINT: - log.Info("Got SIGINT, quitting") - case syscall.SIGTERM: - log.Info("Got SIGTERM, quitting") - 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) - } - - if err = Subscriber.Close(); err != nil { - log.Errorf("Unable to close redis connection cleanly: %s", err) - } - - if len(cfg.PcacheDB) > 0 { - log.Debug("Closing persistent cache db") - - for _, db := range cfg.PcacheDB { - _ = db.Close() - } - } - - os.Exit(0) - } -} - /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/cmd/flickr_init/main.go b/cmd/flickr_init/main.go index effbfe9..bce9661 100644 --- a/cmd/flickr_init/main.go +++ b/cmd/flickr_init/main.go @@ -11,11 +11,11 @@ func main() { if err := webapp.ReadConfig(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) - } else { - if err := flickr.APIClientInit(webapp.Config); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } + } + + if err := flickr.APIClientInit(webapp.Config); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } } diff --git a/cmd/flickr_populate/main.go b/cmd/flickr_populate/main.go index a1b626b..e847689 100644 --- a/cmd/flickr_populate/main.go +++ b/cmd/flickr_populate/main.go @@ -11,11 +11,11 @@ func main() { if err := webapp.ReadConfig(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) - } else { - if err := flickr.Populate(webapp.Config); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } + } + + if err := flickr.Populate(webapp.Config); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } } diff --git a/cmd/flickr_test/main.go b/cmd/flickr_test/main.go index 0aee041..5f5de87 100644 --- a/cmd/flickr_test/main.go +++ b/cmd/flickr_test/main.go @@ -11,16 +11,16 @@ func main() { if err := webapp.ReadConfig(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) - } else { - result, err := flickr.SearchByTags(webapp.Config, []string{"snail", "slug"}) - - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - - fmt.Println(result) } + + result, err := flickr.SearchByTags(webapp.Config, []string{"snail", "slug"}) + + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + fmt.Println(result) } /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/anekdotru/anekdotru.go b/internal/anekdotru/anekdotru.go index aec0e6e..4089507 100644 --- a/internal/anekdotru/anekdotru.go +++ b/internal/anekdotru/anekdotru.go @@ -13,6 +13,7 @@ import ( "github.com/carlmjohnson/requests" ) +// APIClient клиент сервиса anekdot.ru. func APIClient(cfg *config.MyConfig) (string, error) { var ( ctx = context.Background() diff --git a/internal/bunicomic/bunicomic.go b/internal/bunicomic/bunicomic.go index 9f837e0..128b904 100644 --- a/internal/bunicomic/bunicomic.go +++ b/internal/bunicomic/bunicomic.go @@ -3,6 +3,7 @@ package bunicomic import ( "aleesa-webapp-go/internal/config" "context" + "errors" "fmt" "math/rand/v2" "regexp" @@ -12,6 +13,7 @@ import ( "golang.org/x/net/html" ) +// APIClient клиент сервиса bunicomic.com. func APIClient(cfg *config.MyConfig) (string, error) { var ( ctx = context.Background() @@ -76,7 +78,7 @@ func APIClient(cfg *config.MyConfig) (string, error) { } // Картинки не выпарсилось, это точно ошибка. - err = fmt.Errorf("unable to get link to buni comic strip image") + err = errors.New("unable to get link to buni comic strip image") return "", err } diff --git a/internal/flickr/flickr.go b/internal/flickr/flickr.go index 1b33872..bcf47a4 100644 --- a/internal/flickr/flickr.go +++ b/internal/flickr/flickr.go @@ -2,7 +2,9 @@ package flickr import ( "aleesa-webapp-go/internal/config" - "aleesa-webapp-go/internal/pcachedb" //nolint: gosec // Just go fuck yourself if you don't see difference between hashing and cryptography. + "aleesa-webapp-go/internal/pcachedb" + "errors" + "strconv" "context" "fmt" @@ -151,6 +153,7 @@ func SearchByTags(cfg *config.MyConfig, tags []string) (string, error) { // которую он рассчитан. Но нам повезло, что можно просто вытащить подписанный url и заправить его в свой // http-клиент. url := c.GetUrl() + var response SearchResult err := requests. @@ -160,7 +163,7 @@ func SearchByTags(cfg *config.MyConfig, tags []string) (string, error) { Fetch(ctx) if response.Stat == "fail" { - return "", fmt.Errorf("search request returns no results and fails") + return "", errors.New("search request returns no results and fails") } // Ну, вот мы и получили ответ на вопрос - а сколько же у нас результатов поиска всего, по заданному критерию. @@ -171,7 +174,7 @@ func SearchByTags(cfg *config.MyConfig, tags []string) (string, error) { } randomPage := rand.Int64N(response.Photos.Pages) - c.Args.Set("page", fmt.Sprintf("%d", randomPage)) + c.Args.Set("page", strconv.FormatInt(randomPage, 10)) // Подписываем запрос oauth-говном. c.OAuthSign() @@ -189,7 +192,7 @@ func SearchByTags(cfg *config.MyConfig, tags []string) (string, error) { } if response.Stat == "fail" { - return "", fmt.Errorf("search request returns no results and fails") + return "", errors.New("search request returns no results and fails") } // Частенько выборка не очень уникальна, много дублей. Чтобы почистить выборку, неплохо бы выбирать только те фотки, diff --git a/internal/log/log.go b/internal/log/log.go index 567334a..0c77912 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -29,13 +29,12 @@ func Init(level string, fileDescriptor *os.File) { default: loglevel = slog.LevelInfo - } opts := &slog.HandlerOptions{ // Use the ReplaceAttr function on the handler options // to be able to replace any single attribute in the log output - ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { //nolint: revive // check that we are handling the time key if a.Key != slog.TimeKey { return a diff --git a/internal/monkeyuser/monkeyuser.go b/internal/monkeyuser/monkeyuser.go index 9d31138..68b0c36 100644 --- a/internal/monkeyuser/monkeyuser.go +++ b/internal/monkeyuser/monkeyuser.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "math/rand/v2" "strings" @@ -19,6 +20,7 @@ type MonkeyUsers []struct { URL string `json:"url"` } +// APIClient клиент сервиса monkeyuser.com. func APIClient(cfg *config.MyConfig) (string, error) { var ( ctx = context.Background() @@ -27,7 +29,7 @@ func APIClient(cfg *config.MyConfig) (string, error) { body string userAgent = cfg.UserAgents[rand.IntN(len(cfg.UserAgents))] baseURL = "https://www.monkeyuser.com" - indexURL = fmt.Sprintf("%s/index.json", baseURL) + indexURL = baseURL + "/index.json" ) err := requests. @@ -87,7 +89,7 @@ func APIClient(cfg *config.MyConfig) (string, error) { link = func(n *html.Node) { if n.Type == html.ElementNode && n.Data == "img" { - for _, a_href := range n.Attr { //nolint: revive,stylecheck + for _, a_href := range n.Attr { //nolint: revive // Если у нас class == "lazyload small-image", то после него ключ data-src должен содержать // искомый relative url if a_href.Key == "class" && a_href.Val == "logo" { @@ -116,7 +118,7 @@ func APIClient(cfg *config.MyConfig) (string, error) { return fmt.Sprintf("%s%s", baseURL, hyperlink), nil } - err = fmt.Errorf("no links found on monkeyusers.com") + err = errors.New("no links found on monkeyusers.com") return "", err } diff --git a/internal/oboobs/oboobs.go b/internal/oboobs/oboobs.go index 4454b71..632a504 100644 --- a/internal/oboobs/oboobs.go +++ b/internal/oboobs/oboobs.go @@ -18,6 +18,7 @@ type Oboobs struct { Preview string `json:"preview"` } +// APIClient клиент сервиса oboobs.ru. func APIClient(cfg *config.MyConfig) (string, error) { var ( ctx = context.Background() @@ -26,23 +27,15 @@ func APIClient(cfg *config.MyConfig) (string, error) { boobs []Oboobs ) - err := requests. - URL(url). - UserAgent(userAgent). - ToJSON(&boobs). - Fetch(ctx) - - if err != nil { + if err := requests.URL(url).UserAgent(userAgent).ToJSON(&boobs).Fetch(ctx); err != nil { return "", fmt.Errorf("unable to GET %s: %w", url, err) } if len(boobs) == 0 { - err = fmt.Errorf("empty json array returned from %s", url) - - return "", err + return "", fmt.Errorf("empty json array returned from %s", url) } - answer := fmt.Sprintf("https://media.oboobs.ru/%s", boobs[0].Preview) + answer := "https://media.oboobs.ru/%s" + boobs[0].Preview return answer, nil } diff --git a/internal/obutts/obutts.go b/internal/obutts/obutts.go index 153e52d..f210b58 100644 --- a/internal/obutts/obutts.go +++ b/internal/obutts/obutts.go @@ -18,6 +18,7 @@ type Obutts struct { Preview string `json:"preview"` } +// APIClient клиент сервиса obutts.ru. func APIClient(cfg *config.MyConfig) (string, error) { var ( ctx = context.Background() @@ -42,7 +43,7 @@ func APIClient(cfg *config.MyConfig) (string, error) { return "", err } - answer := fmt.Sprintf("http://media.obutts.ru/%s", butts[0].Preview) + answer := "http://media.obutts.ru/" + butts[0].Preview return answer, nil } diff --git a/internal/openweathermap/openweathermap.go b/internal/openweathermap/openweathermap.go index 9dcc138..cafd63d 100644 --- a/internal/openweathermap/openweathermap.go +++ b/internal/openweathermap/openweathermap.go @@ -228,17 +228,17 @@ func OwmAPIClient(cfg *config.MyConfig, item OwmItem) (string, error) { // UpdateOwmCache обновляет кэш. func UpdateOwmCache(cfg *config.MyConfig, tsNowUnix int64, entry string, value string) error { - key := fmt.Sprintf("%s+timestamp", entry) + key := entry + "+timestamp" // "Кэшируем" на 3 часа. ts := tsNowUnix + int64((3 * time.Hour).Seconds()) - timestamp := fmt.Sprintf("%d", ts) + timestamp := strconv.FormatInt(ts, 10) if err := pcachedb.SaveKeyWithValue(cfg, "cache", key, timestamp); err != nil { return fmt.Errorf("OWM-Cache: unable to save timestamp to cache: %w", err) } - key = fmt.Sprintf("%s+value", entry) + key = entry + "+value" if err := pcachedb.SaveKeyWithValue(cfg, "cache", key, value); err != nil { return fmt.Errorf("OWM-Cache: unable to save answer to cache: %w", err) @@ -358,8 +358,7 @@ func QueryOwmCache(cfg *config.MyConfig, item OwmItem) (string, error) { return answer, err } -// Записывает в кэш, кто каким городом интересовался в последний раз, когда спрашивал о погоде. -// STUB +// UpdateOwmUserCache записывает в кэш, кто каким городом интересовался в последний раз, когда спрашивал о погоде. func UpdateOwmUserCache(cfg *config.MyConfig, chatid string, entry string, value string) error { var err error diff --git a/internal/prazdnikisegodnyaru/prazdnikisegodnyaru.go b/internal/prazdnikisegodnyaru/prazdnikisegodnyaru.go index 323432a..8152075 100644 --- a/internal/prazdnikisegodnyaru/prazdnikisegodnyaru.go +++ b/internal/prazdnikisegodnyaru/prazdnikisegodnyaru.go @@ -16,8 +16,10 @@ import ( "golang.org/x/net/html" ) +// PsrClient обёртка для клиента сервиса prazdniki-segodnya.ru, работающая с кэшем. Запросы надо делать именно через неё. func PsrClient(cfg *config.MyConfig) (string, error) { tsNow := time.Now() + // Вычислим expiration time для MSK+0 в формате unix timestamp loc, _ := time.LoadLocation("Europe/Moscow") year, month, day := tsNow.In(loc).Date() @@ -91,6 +93,7 @@ func PsrClient(cfg *config.MyConfig) (string, error) { return pcachedb.GetValue(cfg, "cache", "prazdnikisegodhyaru_value"), nil } +// PsrAPIClient клиент сервиса prazdniki-segodnya.ru. func PsrAPIClient(cfg *config.MyConfig) (string, error) { var ( ctx = context.Background() @@ -102,15 +105,8 @@ func PsrAPIClient(cfg *config.MyConfig) (string, error) { _, _, day := time.Now().Date() userAgent := cfg.UserAgents[day] - err := requests. - URL(url). - UserAgent(userAgent). - Header("User-Agent", "ru-RU"). - Header("Accept-Charset", "utf-8"). - ToString(&respBody). - Fetch(ctx) - - if err != nil { + if err := requests.URL(url).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) } @@ -152,17 +148,16 @@ func PsrAPIClient(cfg *config.MyConfig) (string, error) { } if len(holidays) == 0 { - err = errors.New("unable to parse response from prazdniki-segodnya.ru: no holidays found") - - return "", err + return "", errors.New("unable to parse response from prazdniki-segodnya.ru: no holidays found") } return "* " + strings.Join(holidays, "\n* "), nil } +// UpdatePsrCache сохраняет в кэш ответ от сервиса prazdniki-segodnya.ru и временной штамп. func UpdatePsrCache(cfg *config.MyConfig, eTime int64, value string) error { key := "prazdnikisegodhyaru_timestamp" - timestamp := fmt.Sprintf("%d", eTime) + timestamp := strconv.FormatInt(eTime, 10) if err := pcachedb.SaveKeyWithValue(cfg, "cache", key, timestamp); err != nil { return fmt.Errorf("PSR-Cache: unable to save timestamp to cache: %w", err) diff --git a/internal/randomfox/randomfox.go b/internal/randomfox/randomfox.go index d0e6ef2..09eb8cb 100644 --- a/internal/randomfox/randomfox.go +++ b/internal/randomfox/randomfox.go @@ -16,6 +16,7 @@ type RandomFox struct { Link string `json:"link"` } +// APIClient клиент сервиса randomfox.ca. func APIClient(cfg *config.MyConfig) (string, error) { var ( ctx = context.Background() diff --git a/internal/thecatapi/thecatapi.go b/internal/thecatapi/thecatapi.go index 3397398..dfe41c2 100644 --- a/internal/thecatapi/thecatapi.go +++ b/internal/thecatapi/thecatapi.go @@ -20,6 +20,7 @@ type TheCatAPIinnerStruct struct { // TheCatAPI структурка, описывающая массив, возвращаемый на запрос урла картинки из The Cat API. type TheCatAPI []TheCatAPIinnerStruct +// APIClient клиент сервиса thecatapi.com. func APIClient(cfg *config.MyConfig) (string, error) { var ( ctx = context.Background() diff --git a/internal/webapp/MsgParser.go b/internal/webapp/MsgParser.go index 22123a1..f951a09 100644 --- a/internal/webapp/MsgParser.go +++ b/internal/webapp/MsgParser.go @@ -23,7 +23,7 @@ import ( ) // MsgParser горутинка, которая парсит json-чики прилетевшие из REDIS-ки. -func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { +func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { //nolint: revive var ( sendTo string answer string @@ -419,7 +419,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { } 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 = fmt.Sprintf("Я не знаю, какая погода в %s", city) + j.Message = "Я не знаю, какая погода в " + city } else { j.Message = answer } @@ -433,14 +433,14 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { 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 = fmt.Sprintf("Я не знаю, какая погода в %s", city) + j.Message = "Я не знаю, какая погода в " + city } else { j.Message = answer } } 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 = fmt.Sprintf("Я не знаю, какая погода в %s", city) + j.Message = "Я не знаю, какая погода в " + city } else { j.Message = answer } @@ -469,6 +469,7 @@ func MsgParser(cfg *config.MyConfig, ctx context.Context, msg string) { // Настало время формировать json и засылать его в дальше var message SMsg + message.From = j.From message.Userid = j.Userid message.Chatid = j.Chatid diff --git a/internal/webapp/ReadConfig.go b/internal/webapp/ReadConfig.go index d11f07f..96c8475 100644 --- a/internal/webapp/ReadConfig.go +++ b/internal/webapp/ReadConfig.go @@ -4,6 +4,7 @@ import ( "aleesa-webapp-go/internal/config" "aleesa-webapp-go/internal/log" "encoding/json" + "errors" "fmt" "os" "unsafe" @@ -131,7 +132,7 @@ func ReadConfig() error { // sampleConfig.OpenWeatherMap.Country is false by default // Теперь надо бы попробовать загрузить список User Agent-ов, с которыми ходить в разные апишки. - uaFile := fmt.Sprintf("%s/useragents.txt", sampleConfig.DataDir) + uaFile := sampleConfig.DataDir + "/useragents.txt" uaList, err := ReadLines(uaFile) if err != nil { @@ -149,7 +150,7 @@ func ReadConfig() error { return nil } - return fmt.Errorf("config was not loaded! Refusing to start") + return errors.New("config was not loaded! Refusing to start") } /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/webapp/SigHandler.go b/internal/webapp/SigHandler.go new file mode 100644 index 0000000..a86c55c --- /dev/null +++ b/internal/webapp/SigHandler.go @@ -0,0 +1,58 @@ +package webapp + +import ( + "aleesa-webapp-go/internal/config" + "aleesa-webapp-go/internal/log" + "os" + "syscall" +) + +// SigHandler хэндлер сигналов закрывает все бд и сваливает из приложения. +func SigHandler(cfg *config.MyConfig) { + var err error + + for { + var s = <-SigChan + switch s { + case syscall.SIGINT: + log.Info("Got SIGINT, quitting") + case syscall.SIGTERM: + log.Info("Got SIGTERM, quitting") + 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) + } + + if err = Subscriber.Close(); err != nil { + log.Errorf("Unable to close redis connection cleanly: %s", err) + } + + if len(cfg.PcacheDB) > 0 { + log.Debug("Closing persistent cache db") + + for dbName, db := range cfg.PcacheDB { + if err := db.Flush(); err != nil { + log.Errorf("Unable to flush %s db to disk: %s", dbName, err) + } + + if err := db.Close(); err != nil { + log.Errorf("Unable to close %s db to disk: %s", dbName, err) + } + } + } + + os.Exit(0) + } +} + +/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/webapp/defaults.go b/internal/webapp/defaults.go index e6187bd..f223d7f 100644 --- a/internal/webapp/defaults.go +++ b/internal/webapp/defaults.go @@ -6,6 +6,7 @@ import ( "path/filepath" ) +// DefaultConfigFileLocations выдаёт варианты путей, по которым может находиться конфиг-файл. func DefaultConfigFileLocations() ([]string, error) { var locations []string @@ -15,7 +16,7 @@ func DefaultConfigFileLocations() ([]string, error) { return locations, fmt.Errorf("unable to get my executable path: %w", err) } - configJSONPath := fmt.Sprintf("%s/data/config.json", filepath.Dir(executablePath)) + configJSONPath := filepath.Dir(executablePath) + "/data/config.json" locations = []string{ "~/.aleesa-webapp-go.json", diff --git a/internal/webapp/globals.go b/internal/webapp/globals.go index 20d18d2..16c011b 100644 --- a/internal/webapp/globals.go +++ b/internal/webapp/globals.go @@ -2,17 +2,48 @@ package webapp import ( "aleesa-webapp-go/internal/config" + "context" + "os" + + "github.com/go-redis/redis/v8" ) var ( - ForwardMax int64 = 5 + // ForwardMax максимальное количество перенаправлений между сервисами бота, по-умолчанию, 5. + ForwardMax int64 = 5 + + // ConfigFileSize максимальная длина конфиг-файла, предполагается что 64кб хватит всем. ConfigFileSize int64 = 65535 - DataDir = "data" - Host = "localhost" - Loglevel = "info" - RedisPort = 6379 - NetworkTimeout = 10 - Config *config.MyConfig + + // DataDir каталог с данными, в котором размещаются например, конфиг и кшт веб-клиентов, а также ключи flickr-а. + DataDir = "data" + + // Host хост, на котором работает сервер redis. + Host = "localhost" + + // Loglevel уровень логгирования, по-умолчанию, info. + Loglevel = "info" + + // RedisPort порт, на которм слушает сервер redis, по-умолчанию 6379. + RedisPort = 6379 + + // NetworkTimeout таймаут сетевого соединения. + NetworkTimeout = 10 + + // Config структурка, содержащая данные конфига. + Config *config.MyConfig + + // ctx контекст редиски. + Ctx = context.Background() + + // Subscriber объектик сабскрайбера редиски. + Subscriber *redis.PubSub + + // Shutdown ставится в true, если мы получили сигнал на выключение. + Shutdown = false + + // SigChan канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов. + SigChan = make(chan os.Signal, 1) ) /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/internal/xkcdru/xkcdru.go b/internal/xkcdru/xkcdru.go index d911e71..f660ba5 100644 --- a/internal/xkcdru/xkcdru.go +++ b/internal/xkcdru/xkcdru.go @@ -3,6 +3,7 @@ package xkcdru import ( "aleesa-webapp-go/internal/config" "aleesa-webapp-go/internal/log" + "errors" "fmt" "io" "math/rand/v2" @@ -10,6 +11,7 @@ import ( "time" ) +// APIClient клиент сервиса xkcd.ru. func APIClient(cfg *config.MyConfig) (string, error) { var ( err error @@ -32,6 +34,7 @@ func APIClient(cfg *config.MyConfig) (string, error) { req.Header.Set("User-Agent", cfg.UserAgents[rand.IntN(len(cfg.UserAgents))]) var resp *http.Response + resp, err = c.Do(req) //nolint: bodyclose // Посмотри ниже, блять, тело запроса закрывается. if err != nil { @@ -40,9 +43,7 @@ func APIClient(cfg *config.MyConfig) (string, error) { // Типа, надо закрывать Body в любом случае, как рекомендуют в документации https://pkg.go.dev/net/http . defer func(Body io.ReadCloser) { - err := Body.Close() - - if err != nil { + if err := Body.Close(); err != nil { log.Errorf("Unable to close response body for request to %s: %s", url, err) } }(resp.Body) @@ -52,7 +53,7 @@ func APIClient(cfg *config.MyConfig) (string, error) { location := resp.Header.Get("Location") if len(location) <= 3 { - err = fmt.Errorf("location header too short") + err = errors.New("location header too short") return "", err }