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:
parent
73b722d998
commit
dc3fc39a2c
13 changed files with 318 additions and 273 deletions
126
app.go
126
app.go
|
@ -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
21
cache/cache.go
vendored
|
@ -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
13
cache/inMemCache.go
vendored
|
@ -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()
|
||||||
|
|
||||||
|
|
3
font.go
3
font.go
|
@ -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
4
go.mod
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
127
main_test.go
127
main_test.go
|
@ -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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -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])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in a new issue