1
0
Fork 0
mirror of https://github.com/Luzifer/badge-gen.git synced 2024-12-20 16:41:16 +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 (
"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 (
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")
}
serviceHandlers[service] = f
return nil
panic("duplicate service handler")
}
func main() {
rconfig.Parse(&cfg)
serviceHandlers[service] = f
}
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)
}
}

21
cache/cache.go vendored
View file

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

13
cache/inMemCache.go vendored
View file

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

View file

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

4
go.mod
View file

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

View file

@ -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</text>") {
t.Error("Did not found node with text 'API'")
}
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'")
}
assert.Contains(t, badge, ">API</text>")
assert.Contains(t, badge, "<path fill=\"#4c1\"")
assert.Contains(t, badge, ">Documentation</text>")
}
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)
assert.Equal(t, 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) {
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), "<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), "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"133\" height=\"20\">")
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) {
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), "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"133\" height=\"20\">") {
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), "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"133\" height=\"20\">")
assert.NotContains(t, string(p), "#4c1", "default color should not be set")
assert.Contains(t, string(p), "#572", "given color should be set")
}
}
}

View file

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

View file

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

View file

@ -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,17 +289,14 @@ 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 {
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
// #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"))
}
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",
)
}

View file

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

View file

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

View file

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

View file

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