From dc3fc39a2c34bacd5eae2afcd698e087d148fbe7 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Fri, 8 Sep 2023 14:23:22 +0200 Subject: [PATCH] Fix linter errors Signed-off-by: Knut Ahlers --- app.go | 126 +++++++++++++++++++++++++++++------------- cache/cache.go | 21 +++---- cache/inMemCache.go | 13 +++-- font.go | 3 +- go.mod | 4 ++ main_test.go | 127 +++++++++++++++---------------------------- metric.go | 2 +- service_aur.go | 39 +++++++------ service_github.go | 107 +++++++++++++++++++----------------- service_liberapay.go | 37 ++++++++----- service_static.go | 14 ++--- service_travis.go | 35 +++++++----- service_twitch.go | 63 ++++++++++----------- 13 files changed, 318 insertions(+), 273 deletions(-) diff --git a/app.go b/app.go index 837246c..f7921c0 100644 --- a/app.go +++ b/app.go @@ -2,8 +2,7 @@ package main import ( "bytes" - "crypto/sha1" - "errors" + "crypto/sha256" "fmt" "net/http" "net/url" @@ -14,7 +13,8 @@ import ( "time" "github.com/gorilla/mux" - log "github.com/sirupsen/logrus" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/tdewolff/minify" "github.com/tdewolff/minify/svg" "golang.org/x/net/context" @@ -26,12 +26,26 @@ import ( ) const ( - xSpacing = 8 - defaultColor = "4c1" + badgeGenerationTimeout = 1500 * time.Millisecond + xSpacing = 8 + defaultColor = "4c1" +) + +const ( + colorNameBlue = "blue" + colorNameBrightGreen = "brightgreen" + colorNameGray = "gray" + colorNameGreen = "green" + colorNameLightGray = "lightgray" + colorNameOrange = "orange" + colorNameRed = "red" + colorNameYellow = "yellow" + colorNameYellowGreen = "yellowgreen" ) var ( cfg = struct { + LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"` Port int64 `env:"PORT"` Listen string `flag:"listen" default:":3000" description:"Port/IP to listen on"` Cache string `flag:"cache" default:"mem://" description:"Where to cache query results from thirdparty APIs"` @@ -42,17 +56,15 @@ var ( version = "dev" colorList = map[string]string{ - "brightgreen": "4c1", - "green": "97CA00", - "yellow": "dfb317", - "yellowgreen": "a4a61d", - "orange": "fe7d37", - "red": "e05d44", - "blue": "007ec6", - "grey": "555", - "gray": "555", - "lightgrey": "9f9f9f", - "lightgray": "9f9f9f", + colorNameBlue: "007ec6", + colorNameBrightGreen: "4c1", + colorNameGray: "555", + colorNameGreen: "97CA00", + colorNameLightGray: "9f9f9f", + colorNameOrange: "fe7d37", + colorNameRed: "e05d44", + colorNameYellow: "dfb317", + colorNameYellowGreen: "a4a61d", } cacheStore cache.Cache @@ -84,26 +96,44 @@ type serviceHandler interface { Handle(ctx context.Context, params []string) (title, text, color string, err error) } -func registerServiceHandler(service string, f serviceHandler) error { +func registerServiceHandler(service string, f serviceHandler) { if _, ok := serviceHandlers[service]; ok { - return errors.New("Duplicate service handler") + panic("duplicate service handler") } + serviceHandlers[service] = f - return nil } -func main() { - rconfig.Parse(&cfg) +func initApp() error { + rconfig.AutoEnv(true) + if err := rconfig.ParseAndValidate(&cfg); err != nil { + return errors.Wrap(err, "parsing commandline options") + } + + l, err := logrus.ParseLevel(cfg.LogLevel) + if err != nil { + return errors.Wrap(err, "parsing log level") + } + logrus.SetLevel(l) + if cfg.Port != 0 { cfg.Listen = fmt.Sprintf(":%d", cfg.Port) } - log.Infof("badge-gen %s started...", version) + return nil +} +func main() { var err error + if err = initApp(); err != nil { + logrus.WithError(err).Fatal("initializing app") + } + + logrus.Infof("badge-gen %s started...", version) + cacheStore, err = cache.GetCacheByURI(cfg.Cache) if err != nil { - log.WithError(err).Fatal("Unable to open cache") + logrus.WithError(err).Fatal("Unable to open cache") } f, err := os.Open(cfg.ConfStorage) @@ -112,17 +142,19 @@ func main() { yamlDecoder := yaml.NewDecoder(f) yamlDecoder.SetStrict(true) if err = yamlDecoder.Decode(&configStore); err != nil { - log.WithError(err).Fatal("Unable to parse config") + logrus.WithError(err).Fatal("Unable to parse config") } - log.Printf("Loaded %d value pairs into configuration store", len(configStore)) + logrus.Printf("Loaded %d value pairs into configuration store", len(configStore)) - f.Close() + if err = f.Close(); err != nil { + logrus.WithError(err).Error("closing config file (leaked fd)") + } case os.IsNotExist(err): // Do nothing default: - log.WithError(err).Fatal("Unable to open config") + logrus.WithError(err).Fatal("Unable to open config") } r := mux.NewRouter().UseEncodedPath() @@ -130,7 +162,15 @@ func main() { r.HandleFunc("/{service}/{parameters:.*}", generateServiceBadge).Methods("GET") r.HandleFunc("/", handleDemoPage) - http.ListenAndServe(cfg.Listen, r) + server := &http.Server{ + Addr: cfg.Listen, + Handler: r, + ReadHeaderTimeout: time.Second, + } + + if err = server.ListenAndServe(); err != nil { + logrus.WithError(err).Fatal("HTTP server exited unexpectedly") + } } func generateServiceBadge(res http.ResponseWriter, r *http.Request) { @@ -148,7 +188,7 @@ func generateServiceBadge(res http.ResponseWriter, r *http.Request) { al := accessLogger.New(res) - ctx, cancel := context.WithTimeout(context.Background(), 1500*time.Millisecond) + ctx, cancel := context.WithTimeout(r.Context(), badgeGenerationTimeout) defer cancel() handler, ok := serviceHandlers[service] @@ -188,7 +228,7 @@ func generateBadge(res http.ResponseWriter, r *http.Request) { } func renderBadgeToResponse(res http.ResponseWriter, r *http.Request, title, text, color string) { - cacheKey := fmt.Sprintf("%x", sha1.Sum([]byte(fmt.Sprintf("%s::::%s::::%s", title, text, color)))) + cacheKey := fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%s::::%s::::%s", title, text, color)))) storedTag, _ := cacheStore.Get("eTag", cacheKey) res.Header().Add("Cache-Control", "no-cache") @@ -200,7 +240,7 @@ func renderBadgeToResponse(res http.ResponseWriter, r *http.Request, title, text } badge, eTag := createBadge(title, text, color) - cacheStore.Set("eTag", cacheKey, eTag, time.Hour) + _ = cacheStore.Set("eTag", cacheKey, eTag, time.Hour) res.Header().Add("ETag", eTag) res.Header().Add("Content-Type", "image/svg+xml") @@ -210,7 +250,9 @@ func renderBadgeToResponse(res http.ResponseWriter, r *http.Request, title, text badge, _ = m.Bytes("image/svg+xml", badge) - res.Write(badge) + if _, err := res.Write(badge); err != nil { + logrus.WithError(err).Error("writing badge") + } } func createBadge(title, text, color string) ([]byte, string) { @@ -219,7 +261,7 @@ func createBadge(title, text, color string) ([]byte, string) { titleW, _ := calculateTextWidth(title) textW, _ := calculateTextWidth(text) - width := titleW + textW + 4*xSpacing + width := titleW + textW + 4*xSpacing //nolint:gomnd t, _ := assets.ReadFile("assets/badgeTemplate.tpl") tpl, _ := template.New("svg").Parse(string(t)) @@ -228,7 +270,7 @@ func createBadge(title, text, color string) ([]byte, string) { color = c } - tpl.Execute(&buf, map[string]interface{}{ + _ = tpl.Execute(&buf, map[string]any{ "Width": width, "TitleWidth": titleW + 2*xSpacing, "Title": title, @@ -242,10 +284,10 @@ func createBadge(title, text, color string) ([]byte, string) { } func generateETag(in []byte) string { - return fmt.Sprintf("%x", sha1.Sum(in)) + return fmt.Sprintf("%x", sha256.Sum256(in)) } -func handleDemoPage(res http.ResponseWriter, r *http.Request) { +func handleDemoPage(res http.ResponseWriter, _ *http.Request) { t, _ := assets.ReadFile("assets/demoPage.tpl.html") tpl, _ := template.New("demoPage").Parse(string(t)) @@ -265,8 +307,16 @@ func handleDemoPage(res http.ResponseWriter, r *http.Request) { sort.Sort(examples) - tpl.Execute(res, map[string]interface{}{ + if err := tpl.Execute(res, map[string]interface{}{ "Examples": examples, "Version": version, - }) + }); err != nil { + logrus.WithError(err).Error("rendering demo page") + } +} + +func logErr(err error, text string) { + if err != nil { + logrus.WithError(err).Error(text) + } } diff --git a/cache/cache.go b/cache/cache.go index ad4c675..953c28e 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -1,33 +1,34 @@ +// Package cache contains caching implementation for retrieved data package cache import ( - "errors" "net/url" "time" + + "github.com/pkg/errors" ) -type KeyNotFoundError struct{} - -func (k KeyNotFoundError) Error() string { - return "Requested key was not found in database" -} +// ErrKeyNotFound signalized the key is not present in the cache +var ErrKeyNotFound = errors.New("requested key was not found in database") +// Cache describes an interface used to store generated data type Cache interface { Get(namespace, key string) (value string, err error) Set(namespace, key, value string, ttl time.Duration) (err error) Delete(namespace, key string) (err error) } +// GetCacheByURI instantiates a new Cache by the given URI string func GetCacheByURI(uri string) (Cache, error) { - url, err := url.Parse(uri) + u, err := url.Parse(uri) if err != nil { - return nil, err + return nil, errors.Wrap(err, "parsing uri") } - switch url.Scheme { + switch u.Scheme { case "mem": return NewInMemCache(), nil default: - return nil, errors.New("Invalid cache scheme: " + url.Scheme) + return nil, errors.New("Invalid cache scheme: " + u.Scheme) } } diff --git a/cache/inMemCache.go b/cache/inMemCache.go index 447c5fa..b5b47c8 100644 --- a/cache/inMemCache.go +++ b/cache/inMemCache.go @@ -10,29 +10,33 @@ type inMemCacheEntry struct { Expires time.Time } +// InMemCache implements the Cache interface for storage in memory type InMemCache struct { cache map[string]inMemCacheEntry lock sync.RWMutex } +// NewInMemCache creates a new InMemCache func NewInMemCache() *InMemCache { return &InMemCache{ cache: map[string]inMemCacheEntry{}, } } -func (i InMemCache) Get(namespace, key string) (value string, err error) { +// Get retrieves stored data +func (i *InMemCache) Get(namespace, key string) (value string, err error) { i.lock.RLock() defer i.lock.RUnlock() e, ok := i.cache[namespace+"::"+key] if !ok || e.Expires.Before(time.Now()) { - return "", KeyNotFoundError{} + return "", ErrKeyNotFound } return e.Value, nil } -func (i InMemCache) Set(namespace, key, value string, ttl time.Duration) (err error) { +// Set stores data +func (i *InMemCache) Set(namespace, key, value string, ttl time.Duration) (err error) { i.lock.Lock() defer i.lock.Unlock() @@ -44,7 +48,8 @@ func (i InMemCache) Set(namespace, key, value string, ttl time.Duration) (err er return nil } -func (i InMemCache) Delete(namespace, key string) (err error) { +// Delete deletes data +func (i *InMemCache) Delete(namespace, key string) (err error) { i.lock.Lock() defer i.lock.Unlock() diff --git a/font.go b/font.go index b3a1467..8f268d7 100644 --- a/font.go +++ b/font.go @@ -7,6 +7,7 @@ package main import ( "github.com/golang/freetype/truetype" + "github.com/pkg/errors" "golang.org/x/image/math/fixed" ) @@ -18,7 +19,7 @@ func calculateTextWidth(text string) (int, error) { binFont, _ := assets.ReadFile("assets/DejaVuSans.ttf") font, err := truetype.Parse(binFont) if err != nil { - return 0, err + return 0, errors.Wrap(err, "parsing truetype font") } scale := fontSize / float64(font.FUnitsPerEm()) diff --git a/go.mod b/go.mod index f47932d..d76443c 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/gorilla/mux v1.8.0 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.8.4 github.com/tdewolff/minify v2.3.6+incompatible golang.org/x/image v0.12.0 golang.org/x/net v0.15.0 @@ -16,9 +17,12 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tdewolff/parse v2.3.4+incompatible // indirect github.com/tdewolff/test v1.0.6 // indirect golang.org/x/sys v0.12.0 // indirect gopkg.in/validator.v2 v2.0.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/main_test.go b/main_test.go index 2c32173..9c7b4b7 100644 --- a/main_test.go +++ b/main_test.go @@ -1,132 +1,93 @@ package main import ( - "io/ioutil" + "io" "net/http" "net/http/httptest" - "net/url" - "strings" + "os" "testing" + + "github.com/Luzifer/badge-gen/cache" + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" ) +func testGenerateMux() *mux.Router { + m := mux.NewRouter() + m.HandleFunc("/v1/badge", generateBadge).Methods("GET") + m.HandleFunc("/{service}/{parameters:.*}", generateServiceBadge).Methods("GET") + m.HandleFunc("/", handleDemoPage) + return m +} + +func TestMain(m *testing.M) { + cacheStore = cache.NewInMemCache() + os.Exit(m.Run()) +} + func TestCreateBadge(t *testing.T) { - badge := string(createBadge("API", "Documentation", "4c1")) + badgeData, _ := createBadge("API", "Documentation", "4c1") + badge := string(badgeData) - if !strings.Contains(badge, ">API") { - t.Error("Did not found node with text 'API'") - } - - if !strings.Contains(badge, "Documentation") { - t.Error("Did not found node with text 'Documentation'") - } + assert.Contains(t, badge, ">API") + assert.Contains(t, badge, "Documentation") } func TestHttpResponseMissingParameters(t *testing.T) { resp := httptest.NewRecorder() - uri := "/v1/badge" - - req, err := http.NewRequest("GET", uri, nil) + req, err := http.NewRequest("GET", "/v1/badge", nil) if err != nil { t.Fatal(err) } - generateMux().ServeHTTP(resp, req) - if p, err := ioutil.ReadAll(resp.Body); err != nil { + testGenerateMux().ServeHTTP(resp, req) + if p, err := io.ReadAll(resp.Body); err != nil { t.Fail() } else { - if resp.Code != http.StatusInternalServerError { - t.Errorf("Response code should be %d, is %d", http.StatusInternalServerError, resp.Code) - } - - if string(p) != "You must specify parameters 'title' and 'text'.\n" { - t.Error("Response message did not match test") - } + assert.Equal(t, http.StatusInternalServerError, resp.Code) + assert.Contains(t, string(p), "You must specify parameters 'title' and 'text'.") } - } func TestHttpResponseWithoutColor(t *testing.T) { resp := httptest.NewRecorder() - uri := "/v1/badge?" - params := url.Values{ - "title": []string{"API"}, - "text": []string{"Documentation"}, - } - - req, err := http.NewRequest("GET", uri+params.Encode(), nil) + req, err := http.NewRequest("GET", "/static/API/Documentation", nil) if err != nil { t.Fatal(err) } - generateMux().ServeHTTP(resp, req) - if p, err := ioutil.ReadAll(resp.Body); err != nil { + testGenerateMux().ServeHTTP(resp, req) + if p, err := io.ReadAll(resp.Body); err != nil { t.Fail() } else { - if resp.Code != http.StatusOK { - t.Errorf("Response code should be %d, is %d", http.StatusInternalServerError, resp.Code) - } - - if resp.Header().Get("Content-Type") != "image/svg+xml" { - t.Errorf("Response had wrong Content-Type: %s", resp.Header().Get("Content-Type")) - } - + assert.Equal(t, http.StatusOK, resp.Code) + assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type")) // Check whether there is a SVG in the response, format checks are in other checks - if !strings.Contains(string(p), "") { - t.Error("Response message did not match test") - } - - if !strings.Contains(string(p), "#4c1") { - t.Error("Default color was not set") - } + assert.Contains(t, string(p), "") + assert.Contains(t, string(p), "#4c1", "default color should be set") } - } func TestHttpResponseWithColor(t *testing.T) { resp := httptest.NewRecorder() - uri := "/v1/badge?" - params := url.Values{ - "title": []string{"API"}, - "text": []string{"Documentation"}, - "color": []string{"572"}, - } - - req, err := http.NewRequest("GET", uri+params.Encode(), nil) + req, err := http.NewRequest("GET", "/static/API/Documentation/572", nil) //nolint:noctx // fine for an internal test if err != nil { t.Fatal(err) } - generateMux().ServeHTTP(resp, req) - if p, err := ioutil.ReadAll(resp.Body); err != nil { + testGenerateMux().ServeHTTP(resp, req) + if p, err := io.ReadAll(resp.Body); err != nil { t.Fail() } else { - if resp.Code != http.StatusOK { - t.Errorf("Response code should be %d, is %d", http.StatusInternalServerError, resp.Code) - } - - if resp.Header().Get("Content-Type") != "image/svg+xml" { - t.Errorf("Response had wrong Content-Type: %s", resp.Header().Get("Content-Type")) - } - + assert.Equal(t, http.StatusOK, resp.Code) + assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type")) // Check whether there is a SVG in the response, format checks are in other checks - if !strings.Contains(string(p), "") { - t.Error("Response message did not match test") - } - - if strings.Contains(string(p), "#4c1") { - t.Error("Default color is present with color given") - } - - if !strings.Contains(string(p), "#572") { - t.Error("Given color is not present in SVG") - } + assert.Contains(t, string(p), "") + assert.NotContains(t, string(p), "#4c1", "default color should not be set") + assert.Contains(t, string(p), "#572", "given color should be set") } - } diff --git a/metric.go b/metric.go index b677602..49fae00 100644 --- a/metric.go +++ b/metric.go @@ -8,7 +8,7 @@ import ( func metricFormat(in int64) string { siUnits := []string{"k", "M", "G", "T", "P", "E"} for i := len(siUnits) - 1; i >= 0; i-- { - p := int64(math.Pow(1000, float64(i+1))) + p := int64(math.Pow(1000, float64(i+1))) //nolint:gomnd // Makes no sense to extract if in >= p { return fmt.Sprintf("%d%s", in/p, siUnits[i]) } diff --git a/service_aur.go b/service_aur.go index 7b13594..1b4ad47 100644 --- a/service_aur.go +++ b/service_aur.go @@ -9,9 +9,12 @@ import ( "time" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "golang.org/x/net/context" ) +const aurCacheDuration = 10 * time.Minute + func init() { registerServiceHandler("aur", aurServiceHandler{}) } @@ -44,7 +47,7 @@ type aurInfoResult struct { } `json:"results"` } -func (a aurServiceHandler) GetDocumentation() serviceHandlerDocumentationList { +func (aurServiceHandler) GetDocumentation() serviceHandlerDocumentationList { return serviceHandlerDocumentationList{ { ServiceName: "AUR package version", @@ -72,12 +75,12 @@ func (a aurServiceHandler) GetDocumentation() serviceHandlerDocumentationList { func (aurServiceHandler) IsEnabled() bool { return true } func (a aurServiceHandler) Handle(ctx context.Context, params []string) (title, text, color string, err error) { - if len(params) < 2 { + if len(params) < 2 { //nolint:gomnd return title, text, color, errors.New("No service-command / parameters were given") } switch params[0] { - case "license": + case "license": //nolint:goconst return a.handleAURLicense(ctx, params[1:]) case "updated": return a.handleAURUpdated(ctx, params[1:]) @@ -102,10 +105,10 @@ func (a aurServiceHandler) handleAURLicense(ctx context.Context, params []string text = strings.Join(info.Results[0].License, ", ") - cacheStore.Set("aur_license", title, text, 10*time.Minute) + logErr(cacheStore.Set("aur_license", title, text, aurCacheDuration), "writing AUR license to cache") } - return "license", text, "blue", nil + return "license", text, colorNameBlue, nil } func (a aurServiceHandler) handleAURVersion(ctx context.Context, params []string) (title, text, color string, err error) { @@ -120,10 +123,10 @@ func (a aurServiceHandler) handleAURVersion(ctx context.Context, params []string text = info.Results[0].Version - cacheStore.Set("aur_version", title, text, 10*time.Minute) + logErr(cacheStore.Set("aur_version", title, text, aurCacheDuration), "writing AUR version to cache") } - return title, text, "blue", nil + return title, text, colorNameBlue, nil } func (a aurServiceHandler) handleAURUpdated(ctx context.Context, params []string) (title, text, color string, err error) { @@ -140,13 +143,13 @@ func (a aurServiceHandler) handleAURUpdated(ctx context.Context, params []string text = update.Format("2006-01-02 15:04:05") if info.Results[0].OutOfDate > 0 { - text = text + " (outdated)" + text += " (outdated)" } - cacheStore.Set("aur_updated", title, text, 10*time.Minute) + logErr(cacheStore.Set("aur_updated", title, text, aurCacheDuration), "writing AUR updated to cache") } - color = "blue" + color = colorNameBlue if strings.Contains(text, "outdated") { color = "red" } @@ -166,26 +169,30 @@ func (a aurServiceHandler) handleAURVotes(ctx context.Context, params []string) text = strconv.Itoa(info.Results[0].NumVotes) + " votes" - cacheStore.Set("aur_votes", title, text, 10*time.Minute) + logErr(cacheStore.Set("aur_votes", title, text, aurCacheDuration), "writing AUR votes to cache") } - return title, text, "brightgreen", nil + return title, text, colorNameBrightGreen, nil } -func (a aurServiceHandler) fetchAURInfo(ctx context.Context, pkg string) (*aurInfoResult, error) { +func (aurServiceHandler) fetchAURInfo(ctx context.Context, pkg string) (*aurInfoResult, error) { params := url.Values{ "v": []string{"5"}, "type": []string{"info"}, "arg": []string{pkg}, } - url := "https://aur.archlinux.org/rpc/?" + params.Encode() + u := "https://aur.archlinux.org/rpc/?" + params.Encode() - req, _ := http.NewRequest("GET", url, nil) + req, _ := http.NewRequest("GET", u, nil) resp, err := http.DefaultClient.Do(req.WithContext(ctx)) if err != nil { return nil, errors.Wrap(err, "Failed to fetch AUR info") } - defer resp.Body.Close() + defer func() { + if err := resp.Body.Close(); err != nil { + logrus.WithError(err).Error("closing response body (leaked fd)") + } + }() out := &aurInfoResult{} if err := json.NewDecoder(resp.Body).Decode(out); err != nil { diff --git a/service_github.go b/service_github.go index b066e4b..84031b6 100644 --- a/service_github.go +++ b/service_github.go @@ -2,15 +2,18 @@ package main import ( "encoding/json" - "errors" "net/http" "regexp" "strings" "time" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" "golang.org/x/net/context" ) +const githubCacheDuration = 10 * time.Minute + func init() { registerServiceHandler("github", githubServiceHandler{}) } @@ -31,7 +34,7 @@ type githubRepo struct { type githubServiceHandler struct{} -func (g githubServiceHandler) GetDocumentation() serviceHandlerDocumentationList { +func (githubServiceHandler) GetDocumentation() serviceHandlerDocumentationList { return serviceHandlerDocumentationList{ { ServiceName: "GitHub repo license", @@ -74,9 +77,9 @@ func (g githubServiceHandler) GetDocumentation() serviceHandlerDocumentationList func (githubServiceHandler) IsEnabled() bool { return true } func (g githubServiceHandler) Handle(ctx context.Context, params []string) (title, text, color string, err error) { - if len(params) < 2 { - err = errors.New("No service-command / parameters were given") - return + if len(params) < 2 { //nolint:gomnd + err = errors.New("no service-command / parameters were given") + return title, text, color, err } switch params[0] { @@ -86,15 +89,15 @@ func (g githubServiceHandler) Handle(ctx context.Context, params []string) (titl title, text, color, err = g.handleLatestTag(ctx, params[1:]) case "latest-release": title, text, color, err = g.handleLatestRelease(ctx, params[1:]) - case "downloads": + case "downloads": //nolint:goconst title, text, color, err = g.handleDownloads(ctx, params[1:]) case "stars": title, text, color, err = g.handleStargazers(ctx, params[1:]) default: - err = errors.New("An unknown service command was called") + err = errors.New("an unknown service command was called") } - return + return title, text, color, err } func (g githubServiceHandler) handleStargazers(ctx context.Context, params []string) (title, text, color string, err error) { @@ -106,31 +109,31 @@ func (g githubServiceHandler) handleStargazers(ctx context.Context, params []str r := githubRepo{} if err = g.fetchAPI(ctx, path, nil, &r); err != nil { - return + return title, text, color, err } text = metricFormat(r.StargazersCount) - cacheStore.Set("github_repo_stargazers", path, text, 10*time.Minute) + logErr(cacheStore.Set("github_repo_stargazers", path, text, githubCacheDuration), "writing Github repo stargazers to cache") } title = "stars" - color = "brightgreen" - return + color = colorNameBrightGreen + return title, text, color, err } func (g githubServiceHandler) handleDownloads(ctx context.Context, params []string) (title, text, color string, err error) { switch len(params) { - case 2: + case 2: //nolint:gomnd title, text, color, err = g.handleRepoDownloads(ctx, params) - case 3: + case 3: //nolint:gomnd params = append(params, "total") fallthrough - case 4: + case 4: //nolint:gomnd title, text, color, err = g.handleReleaseDownloads(ctx, params) default: err = errors.New("Unsupported number of arguments") } - return + return title, text, color, err } func (g githubServiceHandler) handleReleaseDownloads(ctx context.Context, params []string) (title, text, color string, err error) { @@ -145,24 +148,24 @@ func (g githubServiceHandler) handleReleaseDownloads(ctx context.Context, params r := githubRelease{} if err = g.fetchAPI(ctx, path, nil, &r); err != nil { - return + return title, text, color, err } var sum int64 for _, rel := range r.Assets { if params[3] == "total" || rel.Name == params[3] { - sum = sum + rel.Downloads + sum += rel.Downloads } } text = metricFormat(sum) - cacheStore.Set("github_release_downloads", path, text, 10*time.Minute) + logErr(cacheStore.Set("github_release_downloads", path, text, githubCacheDuration), "writing Github release downloads to cache") } title = "downloads" - color = "brightgreen" - return + color = colorNameBrightGreen + return title, text, color, err } func (g githubServiceHandler) handleRepoDownloads(ctx context.Context, params []string) (title, text, color string, err error) { @@ -173,26 +176,26 @@ func (g githubServiceHandler) handleRepoDownloads(ctx context.Context, params [] if err != nil { r := []githubRelease{} - // TODO: This does not respect pagination! + // NOTE: This does not respect pagination! if err = g.fetchAPI(ctx, path, nil, &r); err != nil { - return + return title, text, color, err } var sum int64 for _, rel := range r { for _, rea := range rel.Assets { - sum = sum + rea.Downloads + sum += rea.Downloads } } text = metricFormat(sum) - cacheStore.Set("github_repo_downloads", path, text, 10*time.Minute) + logErr(cacheStore.Set("github_repo_downloads", path, text, githubCacheDuration), "writing Github repo downloads to cache") } title = "downloads" - color = "brightgreen" - return + color = colorNameBrightGreen + return title, text, color, err } func (g githubServiceHandler) handleLatestRelease(ctx context.Context, params []string) (title, text, color string, err error) { @@ -204,24 +207,24 @@ func (g githubServiceHandler) handleLatestRelease(ctx context.Context, params [] r := githubRelease{} if err = g.fetchAPI(ctx, path, nil, &r); err != nil { - return + return title, text, color, err } text = r.TagName if text == "" { - text = "None" + text = "None" //nolint:goconst } - cacheStore.Set("github_latest_release", path, text, 10*time.Minute) + logErr(cacheStore.Set("github_latest_release", path, text, githubCacheDuration), "writing Github last release to cache") } title = "release" - color = "blue" + color = colorNameBlue if regexp.MustCompile(`^v?0\.`).MatchString(text) { color = "orange" } - return + return title, text, color, err } func (g githubServiceHandler) handleLatestTag(ctx context.Context, params []string) (title, text, color string, err error) { @@ -235,7 +238,7 @@ func (g githubServiceHandler) handleLatestTag(ctx context.Context, params []stri }{} if err = g.fetchAPI(ctx, path, nil, &r); err != nil { - return + return title, text, color, err } if len(r) > 0 { @@ -243,17 +246,17 @@ func (g githubServiceHandler) handleLatestTag(ctx context.Context, params []stri } else { text = "None" } - cacheStore.Set("github_latest_tag", path, text, 10*time.Minute) + logErr(cacheStore.Set("github_latest_tag", path, text, githubCacheDuration), "writing Github last tag to cache") } title = "tag" - color = "blue" + color = colorNameBlue if regexp.MustCompile(`^v?0\.`).MatchString(text) { color = "orange" } - return + return title, text, color, err } func (g githubServiceHandler) handleLicense(ctx context.Context, params []string) (title, text, color string, err error) { @@ -272,11 +275,11 @@ func (g githubServiceHandler) handleLicense(ctx context.Context, params []string "Accept": "application/vnd.github.drax-preview+json", } if err = g.fetchAPI(ctx, path, headers, &r); err != nil { - return + return title, text, color, err } text = r.License.Name - cacheStore.Set("github_license", path, text, 10*time.Minute) + logErr(cacheStore.Set("github_license", path, text, githubCacheDuration), "writing Github license to cache") } title = "license" @@ -286,16 +289,13 @@ func (g githubServiceHandler) handleLicense(ctx context.Context, params []string text = "None" } - return + return title, text, color, err } -func (g githubServiceHandler) fetchAPI(ctx context.Context, path string, headers map[string]string, out interface{}) error { - req, _ := http.NewRequest("GET", "https://api.github.com/"+path, nil) - - if headers != nil { - for k, v := range headers { - req.Header.Set(k, v) - } +func (githubServiceHandler) fetchAPI(ctx context.Context, path string, headers map[string]string, out interface{}) error { + req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/"+path, nil) + for k, v := range headers { + req.Header.Set(k, v) } // #configStore github.username - string - Username for Github auth to increase API requests @@ -304,11 +304,18 @@ func (g githubServiceHandler) fetchAPI(ctx context.Context, path string, headers req.SetBasicAuth(configStore.Str("github.username"), configStore.Str("github.personal_token")) } - resp, err := http.DefaultClient.Do(req.WithContext(ctx)) + resp, err := http.DefaultClient.Do(req) if err != nil { - return err + return errors.Wrap(err, "executing HTTP request") } - defer resp.Body.Close() + defer func() { + if err := resp.Body.Close(); err != nil { + logrus.WithError(err).Error("closing request body (leaked fd)") + } + }() - return json.NewDecoder(resp.Body).Decode(out) + return errors.Wrap( + json.NewDecoder(resp.Body).Decode(out), + "decoding JSON response", + ) } diff --git a/service_liberapay.go b/service_liberapay.go index 02a129b..ba86df2 100644 --- a/service_liberapay.go +++ b/service_liberapay.go @@ -2,16 +2,19 @@ package main import ( "encoding/json" - "errors" "fmt" "net/http" "strings" "time" "github.com/Luzifer/go_helpers/v2/str" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" "golang.org/x/net/context" ) +const liberapayCacheDuration = 60 * time.Minute + func init() { registerServiceHandler("liberapay", liberapayServiceHandler{}) } @@ -29,7 +32,7 @@ type liberapayPublicProfile struct { type liberapayServiceHandler struct{} -func (s liberapayServiceHandler) GetDocumentation() serviceHandlerDocumentationList { +func (liberapayServiceHandler) GetDocumentation() serviceHandlerDocumentationList { return serviceHandlerDocumentationList{ { ServiceName: "LiberaPay Amount Receiving", @@ -46,36 +49,40 @@ func (s liberapayServiceHandler) GetDocumentation() serviceHandlerDocumentationL func (liberapayServiceHandler) IsEnabled() bool { return true } -func (s liberapayServiceHandler) Handle(ctx context.Context, params []string) (title, text, color string, err error) { - if len(params) < 2 { - err = errors.New("You need to provide user and payment direction") - return +func (liberapayServiceHandler) Handle(ctx context.Context, params []string) (title, text, color string, err error) { + if len(params) < 2 { //nolint:gomnd + err = errors.New("you need to provide user and payment direction") + return title, text, color, err } if !str.StringInSlice(params[1], []string{"receiving", "giving"}) { err = fmt.Errorf("%q is an invalid payment direction", params[1]) - return + return title, text, color, err } title = params[1] - color = "brightgreen" + color = colorNameBrightGreen cacheKey := strings.Join([]string{params[0], params[1]}, ":") text, err = cacheStore.Get("liberapay", cacheKey) if err != nil { - req, _ := http.NewRequest("GET", fmt.Sprintf("https://liberapay.com/%s/public.json", params[0]), nil) + req, _ := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://liberapay.com/%s/public.json", params[0]), nil) var resp *http.Response - resp, err = http.DefaultClient.Do(req.WithContext(ctx)) + resp, err = http.DefaultClient.Do(req) if err != nil { - return + return title, text, color, errors.Wrap(err, "executing request") } - defer resp.Body.Close() + defer func() { + if err := resp.Body.Close(); err != nil { + logrus.WithError(err).Error("closing response body (leaked fd)") + } + }() r := liberapayPublicProfile{} if err = json.NewDecoder(resp.Body).Decode(&r); err != nil { - return + return title, text, color, errors.Wrap(err, "decoding JSON response") } switch params[1] { @@ -93,8 +100,8 @@ func (s liberapayServiceHandler) Handle(ctx context.Context, params []string) (t } } - cacheStore.Set("liberapay", cacheKey, text, 60*time.Minute) + logErr(cacheStore.Set("liberapay", cacheKey, text, liberapayCacheDuration), "writing liberapay result to cache") } - return + return title, text, color, nil } diff --git a/service_static.go b/service_static.go index 69386c3..877e1ad 100644 --- a/service_static.go +++ b/service_static.go @@ -12,7 +12,7 @@ func init() { type staticServiceHandler struct{} -func (s staticServiceHandler) GetDocumentation() serviceHandlerDocumentationList { +func (staticServiceHandler) GetDocumentation() serviceHandlerDocumentationList { return serviceHandlerDocumentationList{{ ServiceName: "Static Badge", DemoPath: "/static/API/Documentation/4c1", @@ -22,18 +22,18 @@ func (s staticServiceHandler) GetDocumentation() serviceHandlerDocumentationList func (staticServiceHandler) IsEnabled() bool { return true } -func (s staticServiceHandler) Handle(ctx context.Context, params []string) (title, text, color string, err error) { - if len(params) < 2 { - err = errors.New("You need to provide title and text") - return +func (staticServiceHandler) Handle(_ context.Context, params []string) (title, text, color string, err error) { + if len(params) < 2 { //nolint:gomnd + err = errors.New("you need to provide title and text") + return title, text, color, err } - if len(params) < 3 { + if len(params) < 3 { //nolint:gomnd params = append(params, defaultColor) } title = params[0] text = params[1] color = params[2] - return + return title, text, color, err } diff --git a/service_travis.go b/service_travis.go index 362485a..bc4fb51 100644 --- a/service_travis.go +++ b/service_travis.go @@ -2,21 +2,24 @@ package main import ( "encoding/json" - "errors" "net/http" "strings" "time" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" "golang.org/x/net/context" ) +const travisCacheDuration = 5 * time.Minute + func init() { registerServiceHandler("travis", travisServiceHandler{}) } type travisServiceHandler struct{} -func (t travisServiceHandler) GetDocumentation() serviceHandlerDocumentationList { +func (travisServiceHandler) GetDocumentation() serviceHandlerDocumentationList { return serviceHandlerDocumentationList{{ ServiceName: "Travis-CI", DemoPath: "/travis/Luzifer/password", @@ -26,13 +29,13 @@ func (t travisServiceHandler) GetDocumentation() serviceHandlerDocumentationList func (travisServiceHandler) IsEnabled() bool { return true } -func (t travisServiceHandler) Handle(ctx context.Context, params []string) (title, text, color string, err error) { - if len(params) < 2 { - err = errors.New("You need to provide user and repo") - return +func (travisServiceHandler) Handle(ctx context.Context, params []string) (title, text, color string, err error) { + if len(params) < 2 { //nolint:gomnd + err = errors.New("you need to provide user and repo") + return title, text, color, err } - if len(params) < 3 { + if len(params) < 3 { //nolint:gomnd params = append(params, "master") } @@ -43,12 +46,16 @@ func (t travisServiceHandler) Handle(ctx context.Context, params []string) (titl if err != nil { var resp *http.Response - req, _ := http.NewRequest("GET", "https://api.travis-ci.org/"+path, nil) - resp, err = http.DefaultClient.Do(req.WithContext(ctx)) + req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.travis-ci.org/"+path, nil) + resp, err = http.DefaultClient.Do(req) if err != nil { - return + return title, text, color, errors.Wrap(err, "executing request") } - defer resp.Body.Close() + defer func() { + if err := resp.Body.Close(); err != nil { + logrus.WithError(err).Error("closing request body (leaked fd)") + } + }() r := struct { File string `json:"file"` @@ -58,10 +65,10 @@ func (t travisServiceHandler) Handle(ctx context.Context, params []string) (titl }{} if err = json.NewDecoder(resp.Body).Decode(&r); err != nil { - return + return title, text, color, errors.Wrap(err, "decoding JSON response") } state = r.Branch.State - cacheStore.Set("travis", path, state, 5*time.Minute) + logErr(cacheStore.Set("travis", path, state, travisCacheDuration), "writing Travis status to cache") } title = "travis" @@ -76,5 +83,5 @@ func (t travisServiceHandler) Handle(ctx context.Context, params []string) (titl "canceled": "9f9f9f", }[text] - return + return title, text, color, nil } diff --git a/service_twitch.go b/service_twitch.go index 697cede..dbdab25 100644 --- a/service_twitch.go +++ b/service_twitch.go @@ -10,6 +10,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "golang.org/x/net/context" ) @@ -29,7 +30,7 @@ type twitchServiceHandler struct { accessTokenExpiry time.Time } -func (t twitchServiceHandler) GetDocumentation() serviceHandlerDocumentationList { +func (twitchServiceHandler) GetDocumentation() serviceHandlerDocumentationList { return serviceHandlerDocumentationList{ { ServiceName: "Twitch views", @@ -44,9 +45,9 @@ func (twitchServiceHandler) IsEnabled() bool { } func (t *twitchServiceHandler) Handle(ctx context.Context, params []string) (title, text, color string, err error) { - if len(params) < 2 { + if len(params) < 2 { //nolint:gomnd err = errors.New("No service-command / parameters were given") - return + return title, text, color, err } switch params[0] { @@ -56,7 +57,7 @@ func (t *twitchServiceHandler) Handle(ctx context.Context, params []string) (tit err = errors.New("An unknown service command was called") } - return + return title, text, color, err } func (t *twitchServiceHandler) handleViews(ctx context.Context, params []string) (title, text, color string, err error) { @@ -72,7 +73,7 @@ func (t *twitchServiceHandler) handleViews(ctx context.Context, params []string) field = "id" } - if err := t.doTwitchRequest(http.MethodGet, fmt.Sprintf("https://api.twitch.tv/helix/users?%s=%s", field, params[0]), nil, &respData); err != nil { + if err := t.doTwitchRequest(ctx, http.MethodGet, fmt.Sprintf("https://api.twitch.tv/helix/users?%s=%s", field, params[0]), nil, &respData); err != nil { return "", "", "", errors.Wrap(err, "requesting user list") } @@ -84,10 +85,10 @@ func (t *twitchServiceHandler) handleViews(ctx context.Context, params []string) title = "views" color = "9146FF" - return + return title, text, color, err } -func (t *twitchServiceHandler) getAccessToken() (string, error) { +func (t *twitchServiceHandler) getAccessToken(ctx context.Context) (string, error) { if time.Now().Before(t.accessTokenExpiry) && t.accessToken != "" { return t.accessToken, nil } @@ -97,11 +98,20 @@ func (t *twitchServiceHandler) getAccessToken() (string, error) { params.Set("client_secret", configStore.Str(configKeyTwitchClientSecret)) params.Set("grant_type", "client_credentials") - resp, err := http.Post(fmt.Sprintf("https://id.twitch.tv/oauth2/token?%s", params.Encode()), "application/json", nil) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://id.twitch.tv/oauth2/token?%s", params.Encode()), nil) if err != nil { - return "", errors.Wrap(err, "request access token") + return "", errors.Wrap(err, "creating access token request") } - defer resp.Body.Close() + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", errors.Wrap(err, "executing access token request") + } + defer func() { + if err := resp.Body.Close(); err != nil { + logrus.WithError(err).Error("closing response body (leaked fd)") + } + }() var respData struct { AccessToken string `json:"access_token"` @@ -112,37 +122,18 @@ func (t *twitchServiceHandler) getAccessToken() (string, error) { } t.accessToken = respData.AccessToken - t.accessTokenExpiry = time.Now().Add(time.Duration(time.Duration(respData.ExpiresIn)) * time.Second) + t.accessTokenExpiry = time.Now().Add(time.Duration(respData.ExpiresIn) * time.Second) return t.accessToken, nil } -func (t *twitchServiceHandler) getIDForUser(login string) (string, error) { - var respData struct { - Data []struct { - ID string `json:"id"` - Login string `json:"login"` - } `json:"data"` - } - - if err := t.doTwitchRequest(http.MethodGet, fmt.Sprintf("https://api.twitch.tv/helix/users?login=%s", login), nil, &respData); err != nil { - return "", errors.Wrap(err, "requesting user list") - } - - if len(respData.Data) != 1 { - return "", errors.New("unexpected number of users returned") - } - - return respData.Data[0].ID, nil -} - -func (t *twitchServiceHandler) doTwitchRequest(method, url string, body io.Reader, out interface{}) error { - at, err := t.getAccessToken() +func (t *twitchServiceHandler) doTwitchRequest(ctx context.Context, method, reqURL string, body io.Reader, out any) error { + at, err := t.getAccessToken(ctx) if err != nil { return errors.Wrap(err, "getting access token") } - req, err := http.NewRequest(method, url, body) + req, err := http.NewRequestWithContext(ctx, method, reqURL, body) if err != nil { return errors.Wrap(err, "creating request") } @@ -153,7 +144,11 @@ func (t *twitchServiceHandler) doTwitchRequest(method, url string, body io.Reade if err != nil { return errors.Wrap(err, "executing request") } - defer resp.Body.Close() + defer func() { + if err := resp.Body.Close(); err != nil { + logrus.WithError(err).Error("closing response body (leaked fd)") + } + }() if err = json.NewDecoder(resp.Body).Decode(out); err != nil { return errors.Wrap(err, "reading response")