1
0
Fork 0
mirror of https://github.com/Luzifer/badge-gen.git synced 2024-12-29 21:01:15 +00:00

Fix linter errors

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2023-09-08 14:23:22 +02:00
parent 73b722d998
commit dc3fc39a2c
Signed by: luzifer
GPG key ID: D91C3E91E4CAD6F5
13 changed files with 318 additions and 273 deletions

126
app.go
View file

@ -2,8 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"crypto/sha1" "crypto/sha256"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
@ -14,7 +13,8 @@ import (
"time" "time"
"github.com/gorilla/mux" "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"
"github.com/tdewolff/minify/svg" "github.com/tdewolff/minify/svg"
"golang.org/x/net/context" "golang.org/x/net/context"
@ -26,12 +26,26 @@ import (
) )
const ( const (
badgeGenerationTimeout = 1500 * time.Millisecond
xSpacing = 8 xSpacing = 8
defaultColor = "4c1" defaultColor = "4c1"
) )
const (
colorNameBlue = "blue"
colorNameBrightGreen = "brightgreen"
colorNameGray = "gray"
colorNameGreen = "green"
colorNameLightGray = "lightgray"
colorNameOrange = "orange"
colorNameRed = "red"
colorNameYellow = "yellow"
colorNameYellowGreen = "yellowgreen"
)
var ( var (
cfg = struct { cfg = struct {
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
Port int64 `env:"PORT"` Port int64 `env:"PORT"`
Listen string `flag:"listen" default:":3000" description:"Port/IP to listen on"` 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"` Cache string `flag:"cache" default:"mem://" description:"Where to cache query results from thirdparty APIs"`
@ -42,17 +56,15 @@ var (
version = "dev" version = "dev"
colorList = map[string]string{ colorList = map[string]string{
"brightgreen": "4c1", colorNameBlue: "007ec6",
"green": "97CA00", colorNameBrightGreen: "4c1",
"yellow": "dfb317", colorNameGray: "555",
"yellowgreen": "a4a61d", colorNameGreen: "97CA00",
"orange": "fe7d37", colorNameLightGray: "9f9f9f",
"red": "e05d44", colorNameOrange: "fe7d37",
"blue": "007ec6", colorNameRed: "e05d44",
"grey": "555", colorNameYellow: "dfb317",
"gray": "555", colorNameYellowGreen: "a4a61d",
"lightgrey": "9f9f9f",
"lightgray": "9f9f9f",
} }
cacheStore cache.Cache cacheStore cache.Cache
@ -84,26 +96,44 @@ type serviceHandler interface {
Handle(ctx context.Context, params []string) (title, text, color string, err error) 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 { if _, ok := serviceHandlers[service]; ok {
return errors.New("Duplicate service handler") panic("duplicate service handler")
}
serviceHandlers[service] = f
return nil
} }
func main() { serviceHandlers[service] = f
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 { if cfg.Port != 0 {
cfg.Listen = fmt.Sprintf(":%d", cfg.Port) cfg.Listen = fmt.Sprintf(":%d", cfg.Port)
} }
log.Infof("badge-gen %s started...", version) return nil
}
func main() {
var err error 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) cacheStore, err = cache.GetCacheByURI(cfg.Cache)
if err != nil { 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) f, err := os.Open(cfg.ConfStorage)
@ -112,17 +142,19 @@ func main() {
yamlDecoder := yaml.NewDecoder(f) yamlDecoder := yaml.NewDecoder(f)
yamlDecoder.SetStrict(true) yamlDecoder.SetStrict(true)
if err = yamlDecoder.Decode(&configStore); err != nil { 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): case os.IsNotExist(err):
// Do nothing // Do nothing
default: default:
log.WithError(err).Fatal("Unable to open config") logrus.WithError(err).Fatal("Unable to open config")
} }
r := mux.NewRouter().UseEncodedPath() r := mux.NewRouter().UseEncodedPath()
@ -130,7 +162,15 @@ func main() {
r.HandleFunc("/{service}/{parameters:.*}", generateServiceBadge).Methods("GET") r.HandleFunc("/{service}/{parameters:.*}", generateServiceBadge).Methods("GET")
r.HandleFunc("/", handleDemoPage) 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) { func generateServiceBadge(res http.ResponseWriter, r *http.Request) {
@ -148,7 +188,7 @@ func generateServiceBadge(res http.ResponseWriter, r *http.Request) {
al := accessLogger.New(res) al := accessLogger.New(res)
ctx, cancel := context.WithTimeout(context.Background(), 1500*time.Millisecond) ctx, cancel := context.WithTimeout(r.Context(), badgeGenerationTimeout)
defer cancel() defer cancel()
handler, ok := serviceHandlers[service] 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) { 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) storedTag, _ := cacheStore.Get("eTag", cacheKey)
res.Header().Add("Cache-Control", "no-cache") 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) 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("ETag", eTag)
res.Header().Add("Content-Type", "image/svg+xml") 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) 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) { func createBadge(title, text, color string) ([]byte, string) {
@ -219,7 +261,7 @@ func createBadge(title, text, color string) ([]byte, string) {
titleW, _ := calculateTextWidth(title) titleW, _ := calculateTextWidth(title)
textW, _ := calculateTextWidth(text) textW, _ := calculateTextWidth(text)
width := titleW + textW + 4*xSpacing width := titleW + textW + 4*xSpacing //nolint:gomnd
t, _ := assets.ReadFile("assets/badgeTemplate.tpl") t, _ := assets.ReadFile("assets/badgeTemplate.tpl")
tpl, _ := template.New("svg").Parse(string(t)) tpl, _ := template.New("svg").Parse(string(t))
@ -228,7 +270,7 @@ func createBadge(title, text, color string) ([]byte, string) {
color = c color = c
} }
tpl.Execute(&buf, map[string]interface{}{ _ = tpl.Execute(&buf, map[string]any{
"Width": width, "Width": width,
"TitleWidth": titleW + 2*xSpacing, "TitleWidth": titleW + 2*xSpacing,
"Title": title, "Title": title,
@ -242,10 +284,10 @@ func createBadge(title, text, color string) ([]byte, string) {
} }
func generateETag(in []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") t, _ := assets.ReadFile("assets/demoPage.tpl.html")
tpl, _ := template.New("demoPage").Parse(string(t)) tpl, _ := template.New("demoPage").Parse(string(t))
@ -265,8 +307,16 @@ func handleDemoPage(res http.ResponseWriter, r *http.Request) {
sort.Sort(examples) sort.Sort(examples)
tpl.Execute(res, map[string]interface{}{ if err := tpl.Execute(res, map[string]interface{}{
"Examples": examples, "Examples": examples,
"Version": version, "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)
}
} }

21
cache/cache.go vendored
View file

@ -1,33 +1,34 @@
// Package cache contains caching implementation for retrieved data
package cache package cache
import ( import (
"errors"
"net/url" "net/url"
"time" "time"
"github.com/pkg/errors"
) )
type KeyNotFoundError struct{} // ErrKeyNotFound signalized the key is not present in the cache
var ErrKeyNotFound = errors.New("requested key was not found in database")
func (k KeyNotFoundError) Error() string {
return "Requested key was not found in database"
}
// Cache describes an interface used to store generated data
type Cache interface { type Cache interface {
Get(namespace, key string) (value string, err error) Get(namespace, key string) (value string, err error)
Set(namespace, key, value string, ttl time.Duration) (err error) Set(namespace, key, value string, ttl time.Duration) (err error)
Delete(namespace, key string) (err error) Delete(namespace, key string) (err error)
} }
// GetCacheByURI instantiates a new Cache by the given URI string
func GetCacheByURI(uri string) (Cache, error) { func GetCacheByURI(uri string) (Cache, error) {
url, err := url.Parse(uri) u, err := url.Parse(uri)
if err != nil { if err != nil {
return nil, err return nil, errors.Wrap(err, "parsing uri")
} }
switch url.Scheme { switch u.Scheme {
case "mem": case "mem":
return NewInMemCache(), nil return NewInMemCache(), nil
default: default:
return nil, errors.New("Invalid cache scheme: " + url.Scheme) return nil, errors.New("Invalid cache scheme: " + u.Scheme)
} }
} }

13
cache/inMemCache.go vendored
View file

@ -10,29 +10,33 @@ type inMemCacheEntry struct {
Expires time.Time Expires time.Time
} }
// InMemCache implements the Cache interface for storage in memory
type InMemCache struct { type InMemCache struct {
cache map[string]inMemCacheEntry cache map[string]inMemCacheEntry
lock sync.RWMutex lock sync.RWMutex
} }
// NewInMemCache creates a new InMemCache
func NewInMemCache() *InMemCache { func NewInMemCache() *InMemCache {
return &InMemCache{ return &InMemCache{
cache: map[string]inMemCacheEntry{}, 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() i.lock.RLock()
defer i.lock.RUnlock() defer i.lock.RUnlock()
e, ok := i.cache[namespace+"::"+key] e, ok := i.cache[namespace+"::"+key]
if !ok || e.Expires.Before(time.Now()) { if !ok || e.Expires.Before(time.Now()) {
return "", KeyNotFoundError{} return "", ErrKeyNotFound
} }
return e.Value, nil 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() i.lock.Lock()
defer i.lock.Unlock() defer i.lock.Unlock()
@ -44,7 +48,8 @@ func (i InMemCache) Set(namespace, key, value string, ttl time.Duration) (err er
return nil 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() i.lock.Lock()
defer i.lock.Unlock() defer i.lock.Unlock()

View file

@ -7,6 +7,7 @@ package main
import ( import (
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/pkg/errors"
"golang.org/x/image/math/fixed" "golang.org/x/image/math/fixed"
) )
@ -18,7 +19,7 @@ func calculateTextWidth(text string) (int, error) {
binFont, _ := assets.ReadFile("assets/DejaVuSans.ttf") binFont, _ := assets.ReadFile("assets/DejaVuSans.ttf")
font, err := truetype.Parse(binFont) font, err := truetype.Parse(binFont)
if err != nil { if err != nil {
return 0, err return 0, errors.Wrap(err, "parsing truetype font")
} }
scale := fontSize / float64(font.FUnitsPerEm()) scale := fontSize / float64(font.FUnitsPerEm())

4
go.mod
View file

@ -9,6 +9,7 @@ require (
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.4
github.com/tdewolff/minify v2.3.6+incompatible github.com/tdewolff/minify v2.3.6+incompatible
golang.org/x/image v0.12.0 golang.org/x/image v0.12.0
golang.org/x/net v0.15.0 golang.org/x/net v0.15.0
@ -16,9 +17,12 @@ require (
) )
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/spf13/pflag v1.0.5 // indirect
github.com/tdewolff/parse v2.3.4+incompatible // indirect github.com/tdewolff/parse v2.3.4+incompatible // indirect
github.com/tdewolff/test v1.0.6 // indirect github.com/tdewolff/test v1.0.6 // indirect
golang.org/x/sys v0.12.0 // indirect golang.org/x/sys v0.12.0 // indirect
gopkg.in/validator.v2 v2.0.1 // indirect gopkg.in/validator.v2 v2.0.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View file

@ -1,132 +1,93 @@
package main package main
import ( import (
"io/ioutil" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "os"
"strings"
"testing" "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) { func TestCreateBadge(t *testing.T) {
badge := string(createBadge("API", "Documentation", "4c1")) badgeData, _ := createBadge("API", "Documentation", "4c1")
badge := string(badgeData)
if !strings.Contains(badge, ">API</text>") { assert.Contains(t, badge, ">API</text>")
t.Error("Did not found node with text 'API'") assert.Contains(t, badge, "<path fill=\"#4c1\"")
} assert.Contains(t, badge, ">Documentation</text>")
if !strings.Contains(badge, "<path fill=\"#4c1\"") {
t.Error("Did not find color coding for path")
}
if !strings.Contains(badge, ">Documentation</text>") {
t.Error("Did not found node with text 'Documentation'")
}
} }
func TestHttpResponseMissingParameters(t *testing.T) { func TestHttpResponseMissingParameters(t *testing.T) {
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
uri := "/v1/badge" req, err := http.NewRequest("GET", "/v1/badge", nil)
req, err := http.NewRequest("GET", uri, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
generateMux().ServeHTTP(resp, req) testGenerateMux().ServeHTTP(resp, req)
if p, err := ioutil.ReadAll(resp.Body); err != nil { if p, err := io.ReadAll(resp.Body); err != nil {
t.Fail() t.Fail()
} else { } else {
if resp.Code != http.StatusInternalServerError { assert.Equal(t, http.StatusInternalServerError, resp.Code)
t.Errorf("Response code should be %d, is %d", http.StatusInternalServerError, resp.Code) assert.Contains(t, string(p), "You must specify parameters 'title' and 'text'.")
} }
if string(p) != "You must specify parameters 'title' and 'text'.\n" {
t.Error("Response message did not match test")
}
}
} }
func TestHttpResponseWithoutColor(t *testing.T) { func TestHttpResponseWithoutColor(t *testing.T) {
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
uri := "/v1/badge?" req, err := http.NewRequest("GET", "/static/API/Documentation", nil)
params := url.Values{
"title": []string{"API"},
"text": []string{"Documentation"},
}
req, err := http.NewRequest("GET", uri+params.Encode(), nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
generateMux().ServeHTTP(resp, req) testGenerateMux().ServeHTTP(resp, req)
if p, err := ioutil.ReadAll(resp.Body); err != nil { if p, err := io.ReadAll(resp.Body); err != nil {
t.Fail() t.Fail()
} else { } else {
if resp.Code != http.StatusOK { assert.Equal(t, http.StatusOK, resp.Code)
t.Errorf("Response code should be %d, is %d", http.StatusInternalServerError, resp.Code) assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type"))
}
if resp.Header().Get("Content-Type") != "image/svg+xml" {
t.Errorf("Response had wrong Content-Type: %s", resp.Header().Get("Content-Type"))
}
// Check whether there is a SVG in the response, format checks are in other checks // Check whether there is a SVG in the response, format checks are in other checks
if !strings.Contains(string(p), "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"133\" height=\"20\">") { assert.Contains(t, string(p), "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"133\" height=\"20\">")
t.Error("Response message did not match test") assert.Contains(t, string(p), "#4c1", "default color should be set")
} }
if !strings.Contains(string(p), "#4c1") {
t.Error("Default color was not set")
}
}
} }
func TestHttpResponseWithColor(t *testing.T) { func TestHttpResponseWithColor(t *testing.T) {
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
uri := "/v1/badge?" req, err := http.NewRequest("GET", "/static/API/Documentation/572", nil) //nolint:noctx // fine for an internal test
params := url.Values{
"title": []string{"API"},
"text": []string{"Documentation"},
"color": []string{"572"},
}
req, err := http.NewRequest("GET", uri+params.Encode(), nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
generateMux().ServeHTTP(resp, req) testGenerateMux().ServeHTTP(resp, req)
if p, err := ioutil.ReadAll(resp.Body); err != nil { if p, err := io.ReadAll(resp.Body); err != nil {
t.Fail() t.Fail()
} else { } else {
if resp.Code != http.StatusOK { assert.Equal(t, http.StatusOK, resp.Code)
t.Errorf("Response code should be %d, is %d", http.StatusInternalServerError, resp.Code) assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type"))
}
if resp.Header().Get("Content-Type") != "image/svg+xml" {
t.Errorf("Response had wrong Content-Type: %s", resp.Header().Get("Content-Type"))
}
// Check whether there is a SVG in the response, format checks are in other checks // Check whether there is a SVG in the response, format checks are in other checks
if !strings.Contains(string(p), "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"133\" height=\"20\">") { assert.Contains(t, string(p), "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"133\" height=\"20\">")
t.Error("Response message did not match test") assert.NotContains(t, string(p), "#4c1", "default color should not be set")
} assert.Contains(t, string(p), "#572", "given color should be set")
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")
} }
} }
}

View file

@ -8,7 +8,7 @@ import (
func metricFormat(in int64) string { func metricFormat(in int64) string {
siUnits := []string{"k", "M", "G", "T", "P", "E"} siUnits := []string{"k", "M", "G", "T", "P", "E"}
for i := len(siUnits) - 1; i >= 0; i-- { 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 { if in >= p {
return fmt.Sprintf("%d%s", in/p, siUnits[i]) return fmt.Sprintf("%d%s", in/p, siUnits[i])
} }

View file

@ -9,9 +9,12 @@ import (
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
const aurCacheDuration = 10 * time.Minute
func init() { func init() {
registerServiceHandler("aur", aurServiceHandler{}) registerServiceHandler("aur", aurServiceHandler{})
} }
@ -44,7 +47,7 @@ type aurInfoResult struct {
} `json:"results"` } `json:"results"`
} }
func (a aurServiceHandler) GetDocumentation() serviceHandlerDocumentationList { func (aurServiceHandler) GetDocumentation() serviceHandlerDocumentationList {
return serviceHandlerDocumentationList{ return serviceHandlerDocumentationList{
{ {
ServiceName: "AUR package version", ServiceName: "AUR package version",
@ -72,12 +75,12 @@ func (a aurServiceHandler) GetDocumentation() serviceHandlerDocumentationList {
func (aurServiceHandler) IsEnabled() bool { return true } func (aurServiceHandler) IsEnabled() bool { return true }
func (a aurServiceHandler) Handle(ctx context.Context, params []string) (title, text, color string, err error) { 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") return title, text, color, errors.New("No service-command / parameters were given")
} }
switch params[0] { switch params[0] {
case "license": case "license": //nolint:goconst
return a.handleAURLicense(ctx, params[1:]) return a.handleAURLicense(ctx, params[1:])
case "updated": case "updated":
return a.handleAURUpdated(ctx, params[1:]) 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, ", ") 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) { 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 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) { 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") text = update.Format("2006-01-02 15:04:05")
if info.Results[0].OutOfDate > 0 { 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") { if strings.Contains(text, "outdated") {
color = "red" color = "red"
} }
@ -166,26 +169,30 @@ func (a aurServiceHandler) handleAURVotes(ctx context.Context, params []string)
text = strconv.Itoa(info.Results[0].NumVotes) + " votes" 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{ params := url.Values{
"v": []string{"5"}, "v": []string{"5"},
"type": []string{"info"}, "type": []string{"info"},
"arg": []string{pkg}, "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)) resp, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil { if err != nil {
return nil, errors.Wrap(err, "Failed to fetch AUR info") 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{} out := &aurInfoResult{}
if err := json.NewDecoder(resp.Body).Decode(out); err != nil { if err := json.NewDecoder(resp.Body).Decode(out); err != nil {

View file

@ -2,15 +2,18 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
const githubCacheDuration = 10 * time.Minute
func init() { func init() {
registerServiceHandler("github", githubServiceHandler{}) registerServiceHandler("github", githubServiceHandler{})
} }
@ -31,7 +34,7 @@ type githubRepo struct {
type githubServiceHandler struct{} type githubServiceHandler struct{}
func (g githubServiceHandler) GetDocumentation() serviceHandlerDocumentationList { func (githubServiceHandler) GetDocumentation() serviceHandlerDocumentationList {
return serviceHandlerDocumentationList{ return serviceHandlerDocumentationList{
{ {
ServiceName: "GitHub repo license", ServiceName: "GitHub repo license",
@ -74,9 +77,9 @@ func (g githubServiceHandler) GetDocumentation() serviceHandlerDocumentationList
func (githubServiceHandler) IsEnabled() bool { return true } func (githubServiceHandler) IsEnabled() bool { return true }
func (g githubServiceHandler) Handle(ctx context.Context, params []string) (title, text, color string, err error) { func (g githubServiceHandler) 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") err = errors.New("no service-command / parameters were given")
return return title, text, color, err
} }
switch params[0] { 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:]) title, text, color, err = g.handleLatestTag(ctx, params[1:])
case "latest-release": case "latest-release":
title, text, color, err = g.handleLatestRelease(ctx, params[1:]) title, text, color, err = g.handleLatestRelease(ctx, params[1:])
case "downloads": case "downloads": //nolint:goconst
title, text, color, err = g.handleDownloads(ctx, params[1:]) title, text, color, err = g.handleDownloads(ctx, params[1:])
case "stars": case "stars":
title, text, color, err = g.handleStargazers(ctx, params[1:]) title, text, color, err = g.handleStargazers(ctx, params[1:])
default: 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) { 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{} r := githubRepo{}
if err = g.fetchAPI(ctx, path, nil, &r); err != nil { if err = g.fetchAPI(ctx, path, nil, &r); err != nil {
return return title, text, color, err
} }
text = metricFormat(r.StargazersCount) 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" title = "stars"
color = "brightgreen" color = colorNameBrightGreen
return return title, text, color, err
} }
func (g githubServiceHandler) handleDownloads(ctx context.Context, params []string) (title, text, color string, err error) { func (g githubServiceHandler) handleDownloads(ctx context.Context, params []string) (title, text, color string, err error) {
switch len(params) { switch len(params) {
case 2: case 2: //nolint:gomnd
title, text, color, err = g.handleRepoDownloads(ctx, params) title, text, color, err = g.handleRepoDownloads(ctx, params)
case 3: case 3: //nolint:gomnd
params = append(params, "total") params = append(params, "total")
fallthrough fallthrough
case 4: case 4: //nolint:gomnd
title, text, color, err = g.handleReleaseDownloads(ctx, params) title, text, color, err = g.handleReleaseDownloads(ctx, params)
default: default:
err = errors.New("Unsupported number of arguments") 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) { 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{} r := githubRelease{}
if err = g.fetchAPI(ctx, path, nil, &r); err != nil { if err = g.fetchAPI(ctx, path, nil, &r); err != nil {
return return title, text, color, err
} }
var sum int64 var sum int64
for _, rel := range r.Assets { for _, rel := range r.Assets {
if params[3] == "total" || rel.Name == params[3] { if params[3] == "total" || rel.Name == params[3] {
sum = sum + rel.Downloads sum += rel.Downloads
} }
} }
text = metricFormat(sum) 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" title = "downloads"
color = "brightgreen" color = colorNameBrightGreen
return return title, text, color, err
} }
func (g githubServiceHandler) handleRepoDownloads(ctx context.Context, params []string) (title, text, color string, err error) { 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 { if err != nil {
r := []githubRelease{} r := []githubRelease{}
// TODO: This does not respect pagination! // NOTE: This does not respect pagination!
if err = g.fetchAPI(ctx, path, nil, &r); err != nil { if err = g.fetchAPI(ctx, path, nil, &r); err != nil {
return return title, text, color, err
} }
var sum int64 var sum int64
for _, rel := range r { for _, rel := range r {
for _, rea := range rel.Assets { for _, rea := range rel.Assets {
sum = sum + rea.Downloads sum += rea.Downloads
} }
} }
text = metricFormat(sum) 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" title = "downloads"
color = "brightgreen" color = colorNameBrightGreen
return return title, text, color, err
} }
func (g githubServiceHandler) handleLatestRelease(ctx context.Context, params []string) (title, text, color string, err error) { 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{} r := githubRelease{}
if err = g.fetchAPI(ctx, path, nil, &r); err != nil { if err = g.fetchAPI(ctx, path, nil, &r); err != nil {
return return title, text, color, err
} }
text = r.TagName text = r.TagName
if text == "" { 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" title = "release"
color = "blue" color = colorNameBlue
if regexp.MustCompile(`^v?0\.`).MatchString(text) { if regexp.MustCompile(`^v?0\.`).MatchString(text) {
color = "orange" color = "orange"
} }
return return title, text, color, err
} }
func (g githubServiceHandler) handleLatestTag(ctx context.Context, params []string) (title, text, color string, err error) { 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 { if err = g.fetchAPI(ctx, path, nil, &r); err != nil {
return return title, text, color, err
} }
if len(r) > 0 { if len(r) > 0 {
@ -243,17 +246,17 @@ func (g githubServiceHandler) handleLatestTag(ctx context.Context, params []stri
} else { } else {
text = "None" 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" title = "tag"
color = "blue" color = colorNameBlue
if regexp.MustCompile(`^v?0\.`).MatchString(text) { if regexp.MustCompile(`^v?0\.`).MatchString(text) {
color = "orange" color = "orange"
} }
return return title, text, color, err
} }
func (g githubServiceHandler) handleLicense(ctx context.Context, params []string) (title, text, color string, err error) { 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", "Accept": "application/vnd.github.drax-preview+json",
} }
if err = g.fetchAPI(ctx, path, headers, &r); err != nil { if err = g.fetchAPI(ctx, path, headers, &r); err != nil {
return return title, text, color, err
} }
text = r.License.Name 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" title = "license"
@ -286,17 +289,14 @@ func (g githubServiceHandler) handleLicense(ctx context.Context, params []string
text = "None" text = "None"
} }
return return title, text, color, err
} }
func (g githubServiceHandler) fetchAPI(ctx context.Context, path string, headers map[string]string, out interface{}) error { func (githubServiceHandler) fetchAPI(ctx context.Context, path string, headers map[string]string, out interface{}) error {
req, _ := http.NewRequest("GET", "https://api.github.com/"+path, nil) req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/"+path, nil)
if headers != nil {
for k, v := range headers { for k, v := range headers {
req.Header.Set(k, v) req.Header.Set(k, v)
} }
}
// #configStore github.username - string - Username for Github auth to increase API requests // #configStore github.username - string - Username for Github auth to increase API requests
// #configStore github.personal_token - string - Token for Github auth to increase API requests // #configStore github.personal_token - string - Token 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")) 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 { 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",
)
} }

View file

@ -2,16 +2,19 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/Luzifer/go_helpers/v2/str" "github.com/Luzifer/go_helpers/v2/str"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
const liberapayCacheDuration = 60 * time.Minute
func init() { func init() {
registerServiceHandler("liberapay", liberapayServiceHandler{}) registerServiceHandler("liberapay", liberapayServiceHandler{})
} }
@ -29,7 +32,7 @@ type liberapayPublicProfile struct {
type liberapayServiceHandler struct{} type liberapayServiceHandler struct{}
func (s liberapayServiceHandler) GetDocumentation() serviceHandlerDocumentationList { func (liberapayServiceHandler) GetDocumentation() serviceHandlerDocumentationList {
return serviceHandlerDocumentationList{ return serviceHandlerDocumentationList{
{ {
ServiceName: "LiberaPay Amount Receiving", ServiceName: "LiberaPay Amount Receiving",
@ -46,36 +49,40 @@ func (s liberapayServiceHandler) GetDocumentation() serviceHandlerDocumentationL
func (liberapayServiceHandler) IsEnabled() bool { return true } func (liberapayServiceHandler) IsEnabled() bool { return true }
func (s liberapayServiceHandler) Handle(ctx context.Context, params []string) (title, text, color string, err error) { func (liberapayServiceHandler) 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("You need to provide user and payment direction") err = errors.New("you need to provide user and payment direction")
return return title, text, color, err
} }
if !str.StringInSlice(params[1], []string{"receiving", "giving"}) { if !str.StringInSlice(params[1], []string{"receiving", "giving"}) {
err = fmt.Errorf("%q is an invalid payment direction", params[1]) err = fmt.Errorf("%q is an invalid payment direction", params[1])
return return title, text, color, err
} }
title = params[1] title = params[1]
color = "brightgreen" color = colorNameBrightGreen
cacheKey := strings.Join([]string{params[0], params[1]}, ":") cacheKey := strings.Join([]string{params[0], params[1]}, ":")
text, err = cacheStore.Get("liberapay", cacheKey) text, err = cacheStore.Get("liberapay", cacheKey)
if err != nil { 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 var resp *http.Response
resp, err = http.DefaultClient.Do(req.WithContext(ctx)) resp, err = http.DefaultClient.Do(req)
if err != nil { 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{} r := liberapayPublicProfile{}
if err = json.NewDecoder(resp.Body).Decode(&r); err != nil { if err = json.NewDecoder(resp.Body).Decode(&r); err != nil {
return return title, text, color, errors.Wrap(err, "decoding JSON response")
} }
switch params[1] { 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
} }

View file

@ -12,7 +12,7 @@ func init() {
type staticServiceHandler struct{} type staticServiceHandler struct{}
func (s staticServiceHandler) GetDocumentation() serviceHandlerDocumentationList { func (staticServiceHandler) GetDocumentation() serviceHandlerDocumentationList {
return serviceHandlerDocumentationList{{ return serviceHandlerDocumentationList{{
ServiceName: "Static Badge", ServiceName: "Static Badge",
DemoPath: "/static/API/Documentation/4c1", DemoPath: "/static/API/Documentation/4c1",
@ -22,18 +22,18 @@ func (s staticServiceHandler) GetDocumentation() serviceHandlerDocumentationList
func (staticServiceHandler) IsEnabled() bool { return true } func (staticServiceHandler) IsEnabled() bool { return true }
func (s staticServiceHandler) Handle(ctx context.Context, params []string) (title, text, color string, err error) { func (staticServiceHandler) Handle(_ context.Context, params []string) (title, text, color string, err error) {
if len(params) < 2 { if len(params) < 2 { //nolint:gomnd
err = errors.New("You need to provide title and text") err = errors.New("you need to provide title and text")
return return title, text, color, err
} }
if len(params) < 3 { if len(params) < 3 { //nolint:gomnd
params = append(params, defaultColor) params = append(params, defaultColor)
} }
title = params[0] title = params[0]
text = params[1] text = params[1]
color = params[2] color = params[2]
return return title, text, color, err
} }

View file

@ -2,21 +2,24 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
const travisCacheDuration = 5 * time.Minute
func init() { func init() {
registerServiceHandler("travis", travisServiceHandler{}) registerServiceHandler("travis", travisServiceHandler{})
} }
type travisServiceHandler struct{} type travisServiceHandler struct{}
func (t travisServiceHandler) GetDocumentation() serviceHandlerDocumentationList { func (travisServiceHandler) GetDocumentation() serviceHandlerDocumentationList {
return serviceHandlerDocumentationList{{ return serviceHandlerDocumentationList{{
ServiceName: "Travis-CI", ServiceName: "Travis-CI",
DemoPath: "/travis/Luzifer/password", DemoPath: "/travis/Luzifer/password",
@ -26,13 +29,13 @@ func (t travisServiceHandler) GetDocumentation() serviceHandlerDocumentationList
func (travisServiceHandler) IsEnabled() bool { return true } func (travisServiceHandler) IsEnabled() bool { return true }
func (t travisServiceHandler) Handle(ctx context.Context, params []string) (title, text, color string, err error) { func (travisServiceHandler) 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("You need to provide user and repo") err = errors.New("you need to provide user and repo")
return return title, text, color, err
} }
if len(params) < 3 { if len(params) < 3 { //nolint:gomnd
params = append(params, "master") params = append(params, "master")
} }
@ -43,12 +46,16 @@ func (t travisServiceHandler) Handle(ctx context.Context, params []string) (titl
if err != nil { if err != nil {
var resp *http.Response var resp *http.Response
req, _ := http.NewRequest("GET", "https://api.travis-ci.org/"+path, nil) req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.travis-ci.org/"+path, nil)
resp, err = http.DefaultClient.Do(req.WithContext(ctx)) resp, err = http.DefaultClient.Do(req)
if err != nil { 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 { r := struct {
File string `json:"file"` 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 { 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 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" title = "travis"
@ -76,5 +83,5 @@ func (t travisServiceHandler) Handle(ctx context.Context, params []string) (titl
"canceled": "9f9f9f", "canceled": "9f9f9f",
}[text] }[text]
return return title, text, color, nil
} }

View file

@ -10,6 +10,7 @@ import (
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
@ -29,7 +30,7 @@ type twitchServiceHandler struct {
accessTokenExpiry time.Time accessTokenExpiry time.Time
} }
func (t twitchServiceHandler) GetDocumentation() serviceHandlerDocumentationList { func (twitchServiceHandler) GetDocumentation() serviceHandlerDocumentationList {
return serviceHandlerDocumentationList{ return serviceHandlerDocumentationList{
{ {
ServiceName: "Twitch views", 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) { 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") err = errors.New("No service-command / parameters were given")
return return title, text, color, err
} }
switch params[0] { 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") 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) { 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" 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") return "", "", "", errors.Wrap(err, "requesting user list")
} }
@ -84,10 +85,10 @@ func (t *twitchServiceHandler) handleViews(ctx context.Context, params []string)
title = "views" title = "views"
color = "9146FF" 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 != "" { if time.Now().Before(t.accessTokenExpiry) && t.accessToken != "" {
return t.accessToken, nil return t.accessToken, nil
} }
@ -97,11 +98,20 @@ func (t *twitchServiceHandler) getAccessToken() (string, error) {
params.Set("client_secret", configStore.Str(configKeyTwitchClientSecret)) params.Set("client_secret", configStore.Str(configKeyTwitchClientSecret))
params.Set("grant_type", "client_credentials") 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 { 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 { var respData struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
@ -112,37 +122,18 @@ func (t *twitchServiceHandler) getAccessToken() (string, error) {
} }
t.accessToken = respData.AccessToken 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 return t.accessToken, nil
} }
func (t *twitchServiceHandler) getIDForUser(login string) (string, error) { func (t *twitchServiceHandler) doTwitchRequest(ctx context.Context, method, reqURL string, body io.Reader, out any) error {
var respData struct { at, err := t.getAccessToken(ctx)
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()
if err != nil { if err != nil {
return errors.Wrap(err, "getting access token") 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 { if err != nil {
return errors.Wrap(err, "creating request") return errors.Wrap(err, "creating request")
} }
@ -153,7 +144,11 @@ func (t *twitchServiceHandler) doTwitchRequest(method, url string, body io.Reade
if err != nil { if err != nil {
return errors.Wrap(err, "executing request") 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 { if err = json.NewDecoder(resp.Body).Decode(out); err != nil {
return errors.Wrap(err, "reading response") return errors.Wrap(err, "reading response")