Small underhood changes, linter fixes

This commit is contained in:
eleksir
2025-10-20 21:07:10 +03:00
parent 6c00224f64
commit ff57b0af7d
23 changed files with 218 additions and 172 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@
/flickr_init
/flickr_populate
/flickr_test
/.golangci.bck.yml

View File

@@ -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$

View File

@@ -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: */

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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: */

View File

@@ -13,6 +13,7 @@ import (
"github.com/carlmjohnson/requests"
)
// APIClient клиент сервиса anekdot.ru.
func APIClient(cfg *config.MyConfig) (string, error) {
var (
ctx = context.Background()

View File

@@ -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
}

View File

@@ -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")
}
// Частенько выборка не очень уникальна, много дублей. Чтобы почистить выборку, неплохо бы выбирать только те фотки,

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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: */

View File

@@ -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: */

View File

@@ -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",

View File

@@ -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: */

View File

@@ -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
}