Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
20366ed3e4 | |||
1c52aae429 | |||
c0bd927903 | |||
3ca4448a7e | |||
8c42945e67 | |||
c15121a873 | |||
f954619223 | |||
d010926c07 | |||
7e3f6cf674 | |||
de5626814b | |||
923fdea2a1 | |||
58f77f22a4 | |||
a0d42bbbc3 | |||
7ed0e8fd88 | |||
ede215888a |
34
cache/cache.go
vendored
Normal file
34
cache/cache.go
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Provider interface {
|
||||||
|
Get(key string) ([]byte, error)
|
||||||
|
Set(key string, b []byte, d time.Duration) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func ForURI(uri string) Provider {
|
||||||
|
u, err := url.Parse(uri)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Scheme == "" && u.Path != "" {
|
||||||
|
u.Scheme = u.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
switch u.Scheme {
|
||||||
|
case "memcached":
|
||||||
|
return NewMemcached(u)
|
||||||
|
case "redis":
|
||||||
|
return NewRedisCache(u)
|
||||||
|
case "gcache":
|
||||||
|
return NewGcache(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &NullCache{}
|
||||||
|
}
|
47
cache/gcache.go
vendored
Normal file
47
cache/gcache.go
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bluele/gcache"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Gcache struct {
|
||||||
|
cache gcache.Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGcache(u *url.URL) *Gcache {
|
||||||
|
size := 128
|
||||||
|
|
||||||
|
q := u.Query()
|
||||||
|
|
||||||
|
if sizeStr := q.Get("size"); sizeStr != "" {
|
||||||
|
var err error
|
||||||
|
size, err = strconv.Atoi(sizeStr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
size = 128
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gc := gcache.New(size).
|
||||||
|
LRU().
|
||||||
|
Build()
|
||||||
|
|
||||||
|
return &Gcache{cache: gc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Gcache) Get(key string) ([]byte, error) {
|
||||||
|
res, err := c.cache.Get(key)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.([]byte), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Gcache) Set(key string, b []byte, d time.Duration) error {
|
||||||
|
return c.cache.SetWithExpire(key, b, d)
|
||||||
|
}
|
32
cache/memcached.go
vendored
Normal file
32
cache/memcached.go
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/bradfitz/gomemcache/memcache"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Memcached struct {
|
||||||
|
client *memcache.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMemcached(u *url.URL) *Memcached {
|
||||||
|
mc := memcache.New(strings.Split(u.Host, ",")...)
|
||||||
|
|
||||||
|
return &Memcached{client: mc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memcached) Get(key string) ([]byte, error) {
|
||||||
|
item, err := m.client.Get(key)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Memcached) Set(key string, b []byte, d time.Duration) error {
|
||||||
|
return m.client.Set(&memcache.Item{Key: key, Value: b, Expiration: int32(d.Seconds())})
|
||||||
|
}
|
14
cache/null.go
vendored
Normal file
14
cache/null.go
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type NullCache struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NullCache) Get(key string) ([]byte, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NullCache) Set(key string, b []byte, d time.Duration) error {
|
||||||
|
return nil
|
||||||
|
}
|
29
cache/redis.go
vendored
Normal file
29
cache/redis.go
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-redis/redis"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RedisCache struct {
|
||||||
|
client *redis.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRedisCache(uri *url.URL) *RedisCache {
|
||||||
|
client := redis.NewClient(&redis.Options{
|
||||||
|
Addr: uri.Host,
|
||||||
|
Password: "", // no password set
|
||||||
|
DB: 0, // use default DB
|
||||||
|
})
|
||||||
|
|
||||||
|
return &RedisCache{client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RedisCache) Get(key string) ([]byte, error) {
|
||||||
|
return c.client.Get(key).Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RedisCache) Set(key string, b []byte, d time.Duration) error {
|
||||||
|
return c.client.Set(key, b, d).Err()
|
||||||
|
}
|
166
endpoints.go
Normal file
166
endpoints.go
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func stats(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
|
data, err := statsResponse(w, ps, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
w.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func profile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
|
cacheKey := generateCacheKey(ps) + "-profile"
|
||||||
|
|
||||||
|
res, err := cacheProvider.Get(cacheKey)
|
||||||
|
|
||||||
|
if res != nil && err == nil {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache result for profile specifically
|
||||||
|
data, err := statsResponse(w, ps, profilePatch)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cacheTime > 0 {
|
||||||
|
cacheProvider.Set(cacheKey, data, cacheTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
w.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func heroes(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
|
names := strings.Split(ps.ByName("heroes"), ",")
|
||||||
|
|
||||||
|
if len(names) == 0 {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
writeError(w, errors.New("name list must contain at least one hero"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(names)
|
||||||
|
|
||||||
|
cacheKey := generateCacheKey(ps) + "-heroes-" + hex.EncodeToString(md5.New().Sum([]byte(strings.Join(names, ","))))
|
||||||
|
|
||||||
|
res, err := cacheProvider.Get(cacheKey)
|
||||||
|
|
||||||
|
if res != nil && err == nil {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(res)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nameMap := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
nameMap[name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
ops := make([]patchOperation, 0)
|
||||||
|
|
||||||
|
for _, heroName := range heroNames {
|
||||||
|
if _, exists := nameMap[heroName]; !exists {
|
||||||
|
ops = append(ops, patchOperation{
|
||||||
|
Op: OpRemove,
|
||||||
|
Path: "/quickPlayStats/topHeroes/" + heroName,
|
||||||
|
}, patchOperation{
|
||||||
|
Op: OpRemove,
|
||||||
|
Path: "/quickPlayStats/careerStats/" + heroName,
|
||||||
|
}, patchOperation{
|
||||||
|
Op: OpRemove,
|
||||||
|
Path: "/competitiveStats/topHeroes/" + heroName,
|
||||||
|
}, patchOperation{
|
||||||
|
Op: OpRemove,
|
||||||
|
Path: "/competitiveStats/careerStats/" + heroName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
patch, err := patchFromOperations(ops)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
writeError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a patch to remove all but specified heroes
|
||||||
|
data, err := statsResponse(w, ps, patch)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cacheTime > 0 {
|
||||||
|
cacheProvider.Set(cacheKey, data, cacheTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
w.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
type versionObject struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func versionHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(&versionObject{Version: Version}); err != nil {
|
||||||
|
writeError(w, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type statusObject struct {
|
||||||
|
ResponseCode int `json:"responseCode"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
status := &statusObject{}
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Head("https://playoverwatch.com")
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
status.ResponseCode = res.StatusCode
|
||||||
|
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
w.WriteHeader(res.StatusCode)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
status.Error = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method != http.MethodHead {
|
||||||
|
if err := json.NewEncoder(w).Encode(status); err != nil {
|
||||||
|
writeError(w, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
go.mod
16
go.mod
@ -3,12 +3,12 @@ module git.meow.tf/ow-api/ow-api
|
|||||||
go 1.12
|
go 1.12
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-redis/redis v6.14.2+incompatible
|
github.com/PuerkitoBio/goquery v1.5.1-0.20190109230704-3dcf72e6c17f
|
||||||
github.com/julienschmidt/httprouter v1.2.0
|
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833
|
||||||
github.com/onsi/ginkgo v1.8.0 // indirect
|
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
|
||||||
github.com/onsi/gomega v1.5.0 // indirect
|
github.com/go-redis/redis v6.15.6+incompatible
|
||||||
github.com/rs/cors v1.6.0
|
github.com/julienschmidt/httprouter v1.3.0
|
||||||
github.com/starboy/httpclient v0.0.0-20180910064439-d6d6e42b6080 // indirect
|
github.com/miekg/dns v1.1.26
|
||||||
github.com/tystuyfzand/ovrstat v0.0.0-20181127024708-dc4fc9f39eb7
|
github.com/rs/cors v1.7.0
|
||||||
s32x.com/ovrstat v0.0.0-20190506040748-69ba35e9ff58
|
s32x.com/ovrstat v0.0.0-20200131231416-4cb42edd331d
|
||||||
)
|
)
|
||||||
|
124
go.sum
124
go.sum
@ -2,60 +2,140 @@ github.com/PuerkitoBio/goquery v1.5.1-0.20190109230704-3dcf72e6c17f h1:cWOyRTtBc
|
|||||||
github.com/PuerkitoBio/goquery v1.5.1-0.20190109230704-3dcf72e6c17f/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
|
github.com/PuerkitoBio/goquery v1.5.1-0.20190109230704-3dcf72e6c17f/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
|
||||||
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
|
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
|
||||||
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||||
|
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
|
||||||
|
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||||
|
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833 h1:yCfXxYaelOyqnia8F/Yng47qhmfC9nKTRIbYRrRueq4=
|
||||||
|
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833/go.mod h1:8c4/i2VlovMO2gBnHGQPN5EJw+H0lx1u/5p+cgsXtCk=
|
||||||
|
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
|
||||||
|
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/go-redis/redis v6.14.2+incompatible h1:UE9pLhzmWf+xHNmZsoccjXosPicuiNaInPgym8nzfg0=
|
github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be8FqJQRhdQZ5Sg=
|
||||||
github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
github.com/go-redis/redis v6.15.6+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
|
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
github.com/labstack/echo/v4 v4.0.1-0.20190313005416-e3717be4beda/go.mod h1:tZv7nai5buKSg5h/8E6zz4LsD/Dqh9/91Mvs7Z5Zyno=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/labstack/echo/v4 v4.1.7-0.20190725203903-8cabd1e123d6 h1:ImU76UGnZN9+25dYuM+4uMhP0TFf/nT61rLy1RPPKUM=
|
||||||
|
github.com/labstack/echo/v4 v4.1.7-0.20190725203903-8cabd1e123d6/go.mod h1:kU/7PwzgNxZH4das4XNsSpBSOD09XIF5YEPzjpkGnGE=
|
||||||
|
github.com/labstack/echo/v4 v4.1.14 h1:h8XP66UfB3tUm+L3QPw7tmwAu3pJaA/nyfHPCcz46ic=
|
||||||
|
github.com/labstack/echo/v4 v4.1.14/go.mod h1:Q5KZ1vD3V5FEzjM79hjwVrC3ABr7F5IdM23bXQMRDGg=
|
||||||
|
github.com/labstack/gommon v0.2.9 h1:heVeuAYtevIQVYkGj6A41dtfT91LrvFG220lavpWhrU=
|
||||||
|
github.com/labstack/gommon v0.2.9/go.mod h1:E8ZTmW9vw5az5/ZyHWCp0Lw4OH2ecsaBP1C/NKavGG4=
|
||||||
|
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
|
||||||
|
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||||
|
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||||
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||||
|
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
|
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||||
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU=
|
||||||
|
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
|
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
|
||||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
|
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
|
||||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI=
|
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||||
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||||
github.com/starboy/httpclient v0.0.0-20180910064439-d6d6e42b6080 h1:2MLHNkv6Jl3E58fFOlOeshNo6eqanZV+yGPbJukRQIQ=
|
github.com/seilem/ovrstat v0.0.0-20200123200456-7b7e24d39506 h1:xAdnQYUFimq8Uiz4Io7QlnNGV+4BtagWGC+DDxVG3Fo=
|
||||||
github.com/starboy/httpclient v0.0.0-20180910064439-d6d6e42b6080/go.mod h1:ZaOOtAQcXELCP9jugRrAv1qg3ctLlGnz++lH0KpIZ24=
|
github.com/seilem/ovrstat v0.0.0-20200123200456-7b7e24d39506/go.mod h1:UzsLSEoY8B4FByz4e+5GGKebJio+H+axo3RxLr7d7Mw=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/tystuyfzand/ovrstat v0.0.0-20181127024708-dc4fc9f39eb7 h1:ZHUhYZJiBOfJ9NPDT/YEKHOCc0tTtQiLGpTcW0M2mBM=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/tystuyfzand/ovrstat v0.0.0-20181127024708-dc4fc9f39eb7/go.mod h1:GgVOFCqOnwT77FsVfvySTbw6u3rMW9rqUxvY7nGgpjg=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
|
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
|
||||||
golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||||
|
github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4=
|
||||||
|
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M=
|
||||||
|
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||||
|
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d h1:9FCpayM9Egr1baVnV1SX0H87m+XB0B8S0hAMi99X/3U=
|
||||||
|
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U=
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U=
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190607181551-461777fb6f67/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
|
||||||
|
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
|
||||||
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
|
||||||
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc h1:WiYx1rIFmx8c0mXAFtv5D/mHyKe1+jmuP7PViuwqwuQ=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190609082536-301114b31cce/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
|
||||||
|
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSfrPzImPoVxuomtbT2nk=
|
||||||
|
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190608022120-eacb66d2a7c3/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
s32x.com/ovrstat v0.0.0-20190506040748-69ba35e9ff58 h1:1ioAQKrIwwtxMm4uZySpqj9imNuyCDbL7dk9ccxntmQ=
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
s32x.com/ovrstat v0.0.0-20190506040748-69ba35e9ff58/go.mod h1:ukrDi0b+Kdtd+dI4M0HILhkMGtMZ4v3l2C82meAMzyw=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
s32x.com/ovrstat v0.0.0-20191125185405-f48c0d280dc1 h1:q5zjrLdxx3oo3i49aBKMMdppFoLCEEq6cSD8lWNCoAY=
|
||||||
|
s32x.com/ovrstat v0.0.0-20191125185405-f48c0d280dc1/go.mod h1:UzsLSEoY8B4FByz4e+5GGKebJio+H+axo3RxLr7d7Mw=
|
||||||
|
s32x.com/ovrstat v0.0.0-20200131231416-4cb42edd331d h1:G3p3gNYpF4WsklRFb4U2FWgXhicc2RDWgL+zNc//JWc=
|
||||||
|
s32x.com/ovrstat v0.0.0-20200131231416-4cb42edd331d/go.mod h1:UzsLSEoY8B4FByz4e+5GGKebJio+H+axo3RxLr7d7Mw=
|
||||||
|
294
main.go
294
main.go
@ -4,30 +4,25 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
|
"git.meow.tf/ow-api/ow-api/cache"
|
||||||
"git.meow.tf/ow-api/ow-api/json-patch"
|
"git.meow.tf/ow-api/ow-api/json-patch"
|
||||||
"github.com/go-redis/redis"
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
"github.com/rs/cors"
|
"github.com/rs/cors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"s32x.com/ovrstat/ovrstat"
|
"s32x.com/ovrstat/ovrstat"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version = "2.0.10"
|
Version = "2.3.4"
|
||||||
|
|
||||||
OpAdd = "add"
|
OpAdd = "add"
|
||||||
OpRemove = "remove"
|
OpRemove = "remove"
|
||||||
)
|
)
|
||||||
|
|
||||||
type patchOperation struct {
|
|
||||||
Op string `json:"op"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
Value interface{} `json:"value,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type gamesStats struct {
|
type gamesStats struct {
|
||||||
Played int64 `json:"played"`
|
Played int64 `json:"played"`
|
||||||
Won int64 `json:"won"`
|
Won int64 `json:"won"`
|
||||||
@ -42,23 +37,27 @@ type awardsStats struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
flagBind = flag.String("bind-address", ":8080", "Address to bind to for http requests")
|
flagBind = flag.String("bind-address", ":8080", "Address to bind to for http requests")
|
||||||
|
flagCache = flag.String("cache", "redis://localhost:6379", "Cache uri or 'none' to disable")
|
||||||
|
flagCacheTime = flag.Int("cacheTime", 300, "Cache time in seconds")
|
||||||
|
|
||||||
client *redis.Client
|
cacheProvider cache.Provider
|
||||||
|
|
||||||
|
cacheTime time.Duration
|
||||||
|
|
||||||
profilePatch *jsonpatch.Patch
|
profilePatch *jsonpatch.Patch
|
||||||
|
|
||||||
heroNames []string
|
heroNames []string
|
||||||
|
|
||||||
|
platforms = []string{ovrstat.PlatformPC, ovrstat.PlatformXBL, ovrstat.PlatformPSN, ovrstat.PlatformNS}
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
loadHeroNames()
|
loadHeroNames()
|
||||||
|
|
||||||
client = redis.NewClient(&redis.Options{
|
cacheProvider = cache.ForURI(*flagCache)
|
||||||
Addr: "localhost:6379",
|
|
||||||
Password: "", // no password set
|
cacheTime = time.Duration(*flagCacheTime) * time.Second
|
||||||
DB: 0, // use default DB
|
|
||||||
})
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@ -77,21 +76,12 @@ func main() {
|
|||||||
|
|
||||||
router := httprouter.New()
|
router := httprouter.New()
|
||||||
|
|
||||||
// PC
|
router.HEAD("/status", statusHandler)
|
||||||
router.GET("/v1/stats/pc/:region/:tag/heroes/:heroes", heroes)
|
router.GET("/status", statusHandler)
|
||||||
router.GET("/v1/stats/pc/:region/:tag/profile", profile)
|
|
||||||
router.GET("/v1/stats/pc/:region/:tag/complete", stats)
|
|
||||||
|
|
||||||
// Console
|
registerVersionOne(router)
|
||||||
router.GET("/v1/stats/psn/:tag/heroes/:heroes", injectPlatform("psn", heroes))
|
|
||||||
router.GET("/v1/stats/psn/:tag/profile", injectPlatform("psn", profile))
|
|
||||||
router.GET("/v1/stats/psn/:tag/complete", injectPlatform("psn", stats))
|
|
||||||
router.GET("/v1/stats/xbl/:tag/heroes/:heroes", injectPlatform("xbl", heroes))
|
|
||||||
router.GET("/v1/stats/xbl/:tag/profile", injectPlatform("xbl", profile))
|
|
||||||
router.GET("/v1/stats/xbl/:tag/complete", injectPlatform("xbl", stats))
|
|
||||||
|
|
||||||
// Version
|
registerVersionTwo(router)
|
||||||
router.GET("/v1/version", versionHandler)
|
|
||||||
|
|
||||||
c := cors.New(cors.Options{
|
c := cors.New(cors.Options{
|
||||||
AllowedOrigins: []string{"*"},
|
AllowedOrigins: []string{"*"},
|
||||||
@ -106,6 +96,31 @@ func main() {
|
|||||||
log.Fatal(http.ListenAndServe(*flagBind, c.Handler(router)))
|
log.Fatal(http.ListenAndServe(*flagBind, c.Handler(router)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registerVersionOne(router *httprouter.Router) {
|
||||||
|
for _, platform := range platforms {
|
||||||
|
router.GET("/v1/stats/"+platform+"/:region/:tag/heroes/:heroes", injectPlatform(platform, heroes))
|
||||||
|
router.GET("/v1/stats/"+platform+"/:region/:tag/profile", injectPlatform(platform, profile))
|
||||||
|
router.GET("/v1/stats/"+platform+"/:region/:tag/complete", injectPlatform(platform, stats))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version
|
||||||
|
router.GET("/v1/version", versionHandler)
|
||||||
|
|
||||||
|
router.HEAD("/v1/status", statusHandler)
|
||||||
|
router.GET("/v1/status", statusHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerVersionTwo(router *httprouter.Router) {
|
||||||
|
for _, platform := range platforms {
|
||||||
|
router.GET("/v2/stats/"+platform+"/:tag/heroes/:heroes", injectPlatform(platform, heroes))
|
||||||
|
router.GET("/v2/stats/"+platform+"/:tag/profile", injectPlatform(platform, profile))
|
||||||
|
router.GET("/v2/stats/"+platform+"/:tag/complete", injectPlatform(platform, stats))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version
|
||||||
|
router.GET("/v2/version", versionHandler)
|
||||||
|
}
|
||||||
|
|
||||||
func loadHeroNames() {
|
func loadHeroNames() {
|
||||||
stats, err := ovrstat.PCStats("cats-11481")
|
stats, err := ovrstat.PCStats("cats-11481")
|
||||||
|
|
||||||
@ -138,6 +153,10 @@ func injectPlatform(platform string, handler httprouter.Handle) httprouter.Handl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
tagRegexp = regexp.MustCompile("-(\\d+)$")
|
||||||
|
)
|
||||||
|
|
||||||
func statsResponse(w http.ResponseWriter, ps httprouter.Params, patch *jsonpatch.Patch) ([]byte, error) {
|
func statsResponse(w http.ResponseWriter, ps httprouter.Params, patch *jsonpatch.Patch) ([]byte, error) {
|
||||||
var stats *ovrstat.PlayerStats
|
var stats *ovrstat.PlayerStats
|
||||||
var err error
|
var err error
|
||||||
@ -148,12 +167,19 @@ func statsResponse(w http.ResponseWriter, ps httprouter.Params, patch *jsonpatch
|
|||||||
|
|
||||||
cacheKey := generateCacheKey(ps)
|
cacheKey := generateCacheKey(ps)
|
||||||
|
|
||||||
if region := ps.ByName("region"); region != "" {
|
platform := ps.ByName("platform")
|
||||||
|
|
||||||
|
switch platform {
|
||||||
|
case ovrstat.PlatformPC:
|
||||||
|
if !tagRegexp.MatchString(tag) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return nil, errors.New("bad tag")
|
||||||
|
}
|
||||||
stats, err = ovrstat.PCStats(tag)
|
stats, err = ovrstat.PCStats(tag)
|
||||||
} else if platform := ps.ByName("platform"); platform != "" {
|
case ovrstat.PlatformPSN, ovrstat.PlatformXBL, ovrstat.PlatformNS:
|
||||||
stats, err = ovrstat.ConsoleStats(platform, tag)
|
stats, err = ovrstat.ConsoleStats(platform, tag)
|
||||||
} else {
|
default:
|
||||||
return nil, errors.New("unknown region/platform")
|
return nil, errors.New("unknown platform")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -162,7 +188,7 @@ func statsResponse(w http.ResponseWriter, ps httprouter.Params, patch *jsonpatch
|
|||||||
|
|
||||||
// Caching of full response for modification
|
// Caching of full response for modification
|
||||||
|
|
||||||
res, err := client.Get(cacheKey).Bytes()
|
res, err := cacheProvider.Get(cacheKey)
|
||||||
|
|
||||||
if res != nil && err == nil {
|
if res != nil && err == nil {
|
||||||
if patch != nil {
|
if patch != nil {
|
||||||
@ -224,6 +250,35 @@ func statsResponse(w http.ResponseWriter, ps httprouter.Params, patch *jsonpatch
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rating := 0
|
||||||
|
var ratingIcon string
|
||||||
|
|
||||||
|
if len(stats.Ratings) > 0 {
|
||||||
|
totalRating := 0
|
||||||
|
iconUrl := ""
|
||||||
|
|
||||||
|
for _, rating := range stats.Ratings {
|
||||||
|
totalRating += rating.Level
|
||||||
|
iconUrl = rating.RankIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
rating = int(totalRating / len(stats.Ratings))
|
||||||
|
|
||||||
|
urlBase := iconUrl[0 : strings.Index(iconUrl, "rank-icons/")+11]
|
||||||
|
|
||||||
|
ratingIcon = urlBase + iconFor(rating)
|
||||||
|
}
|
||||||
|
|
||||||
|
extra = append(extra, patchOperation{
|
||||||
|
Op: OpAdd,
|
||||||
|
Path: "/rating",
|
||||||
|
Value: rating,
|
||||||
|
}, patchOperation{
|
||||||
|
Op: OpAdd,
|
||||||
|
Path: "/ratingIcon",
|
||||||
|
Value: ratingIcon,
|
||||||
|
})
|
||||||
|
|
||||||
b, err := json.Marshal(stats)
|
b, err := json.Marshal(stats)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -245,7 +300,9 @@ func statsResponse(w http.ResponseWriter, ps httprouter.Params, patch *jsonpatch
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache response
|
// Cache response
|
||||||
client.Set(cacheKey, b, 10*time.Minute)
|
if cacheTime > 0 {
|
||||||
|
cacheProvider.Set(cacheKey, b, cacheTime)
|
||||||
|
}
|
||||||
|
|
||||||
if patch != nil {
|
if patch != nil {
|
||||||
// Apply filter patch
|
// Apply filter patch
|
||||||
@ -255,163 +312,24 @@ func statsResponse(w http.ResponseWriter, ps httprouter.Params, patch *jsonpatch
|
|||||||
return b, err
|
return b, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func stats(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
func iconFor(rating int) string {
|
||||||
data, err := statsResponse(w, ps, nil)
|
if rating >= 4000 {
|
||||||
|
return "rank-GrandmasterTier.png"
|
||||||
if err != nil {
|
} else if rating >= 3500 {
|
||||||
writeError(w, err.Error())
|
return "rank-MasterTier.png"
|
||||||
return
|
} else if rating >= 3000 {
|
||||||
|
return "rank-DiamondTier.png"
|
||||||
|
} else if rating >= 2500 {
|
||||||
|
return "rank-PlatinumTier.png"
|
||||||
|
} else if rating >= 2000 {
|
||||||
|
return "rank-GoldTier.png"
|
||||||
|
} else if rating >= 1500 {
|
||||||
|
return "rank-SilverTier.png"
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
return "rank-BronzeTier.png"
|
||||||
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func profile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
cacheKey := generateCacheKey(ps) + "-profile"
|
|
||||||
|
|
||||||
// Check cache for -profile to prevent jsonpatch calls
|
|
||||||
res, err := client.Get(cacheKey).Bytes()
|
|
||||||
|
|
||||||
if res != nil && err == nil {
|
|
||||||
w.Write(res)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache result for profile specifically
|
|
||||||
data, err := statsResponse(w, ps, profilePatch)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client.Set(cacheKey, data, 10*time.Minute)
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func heroes(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
names := strings.Split(ps.ByName("heroes"), ",")
|
|
||||||
|
|
||||||
if len(names) == 0 {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
writeError(w, "Name list must contain at least one hero")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nameMap := make(map[string]bool)
|
|
||||||
|
|
||||||
for _, name := range names {
|
|
||||||
nameMap[name] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
ops := make([]patchOperation, 0)
|
|
||||||
|
|
||||||
for _, heroName := range heroNames {
|
|
||||||
if _, exists := nameMap[heroName]; !exists {
|
|
||||||
ops = append(ops, patchOperation{
|
|
||||||
Op: OpRemove,
|
|
||||||
Path: "/quickPlayStats/topHeroes/" + heroName,
|
|
||||||
}, patchOperation{
|
|
||||||
Op: OpRemove,
|
|
||||||
Path: "/quickPlayStats/careerStats/" + heroName,
|
|
||||||
}, patchOperation{
|
|
||||||
Op: OpRemove,
|
|
||||||
Path: "/competitiveStats/topHeroes/" + heroName,
|
|
||||||
}, patchOperation{
|
|
||||||
Op: OpRemove,
|
|
||||||
Path: "/competitiveStats/careerStats/" + heroName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
patch, err := patchFromOperations(ops)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
writeError(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a patch to remove all but specified heroes
|
|
||||||
data, err := statsResponse(w, ps, patch)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
w.Write(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func valueOrDefault(m map[string]interface{}, key string, d int64) int64 {
|
|
||||||
if v, ok := m[key]; ok {
|
|
||||||
switch v.(type) {
|
|
||||||
case int64:
|
|
||||||
return v.(int64)
|
|
||||||
case int:
|
|
||||||
return int64(v.(int))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
func patchFromOperations(ops []patchOperation) (*jsonpatch.Patch, error) {
|
|
||||||
patchBytes, err := json.Marshal(ops)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
patch, err := jsonpatch.DecodePatch(patchBytes)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &patch, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type versionObject struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func versionHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(&versionObject{Version: Version}); err != nil {
|
|
||||||
writeError(w, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type errorObject struct {
|
|
||||||
Error string `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeError(w http.ResponseWriter, err string) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(&errorObject{Error: err}); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateCacheKey(ps httprouter.Params) string {
|
func generateCacheKey(ps httprouter.Params) string {
|
||||||
var cacheKey string
|
return ps.ByName("platform") + "-" + ps.ByName("tag")
|
||||||
|
|
||||||
tag := ps.ByName("tag")
|
|
||||||
|
|
||||||
if region := ps.ByName("region"); region != "" {
|
|
||||||
cacheKey = "pc-" + region + "-" + tag
|
|
||||||
} else if platform := ps.ByName("platform"); platform != "" {
|
|
||||||
cacheKey = platform + "-" + tag
|
|
||||||
}
|
|
||||||
|
|
||||||
return cacheKey
|
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,12 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/tystuyfzand/ovrstat/ovrstat"
|
"s32x.com/ovrstat/ovrstat"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_Stats(t *testing.T) {
|
func Test_Stats(t *testing.T) {
|
||||||
stats, err := ovrstat.PCStats("us", "cats-11481")
|
stats, err := ovrstat.PCStats("cats-11481")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
268
pro/main.go
Normal file
268
pro/main.go
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"s32x.com/ovrstat/ovrstat"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cloudfrontUrl, err := lookupCloudfrontURL()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Unable to find url:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("URL:", cloudfrontUrl)
|
||||||
|
|
||||||
|
ips, err := lookupIpv6Dns(cloudfrontUrl)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Unable to find ips:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("IPs:", ips)
|
||||||
|
|
||||||
|
localIps, err := checkIpv6Local()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Unable to get local ips")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Local IPs:", localIps)
|
||||||
|
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
|
localAddr, err := net.ResolveIPAddr("ip", "")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// You also need to do this to make it work and not give you a
|
||||||
|
// "mismatched local address type ip"
|
||||||
|
// This will make the ResolveIPAddr a TCPAddr without needing to
|
||||||
|
// say what SRC port number to use.
|
||||||
|
localTCPAddr := net.TCPAddr{
|
||||||
|
IP: localAddr.IP,
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultDialer := &net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
dialer := &net.Dialer{
|
||||||
|
LocalAddr: &localTCPAddr,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
transport := &http.Transport{
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
oldNetwork := network
|
||||||
|
oldAddr := addr
|
||||||
|
|
||||||
|
if addr == "playoverwatch.com:443" {
|
||||||
|
network = "tcp6"
|
||||||
|
addr = "[" + ips[r.Intn(len(ips))] + "]:443"
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Dial using", addr)
|
||||||
|
|
||||||
|
c, err := dialer.DialContext(ctx, network, addr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Fallback")
|
||||||
|
c, err = defaultDialer.DialContext(ctx, oldNetwork, oldAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, err
|
||||||
|
},
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
ServerName: "playoverwatch.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
http.DefaultClient = &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := ovrstat.PCStats("cats-11481")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln("Error retrieving:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println(stats.Name+" is level", stats.Level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupCloudfrontURL() (string, error) {
|
||||||
|
res, err := http.Get("https://playoverwatch.com/en-us/")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
doc, err := goquery.NewDocumentFromReader(res.Body)
|
||||||
|
|
||||||
|
links := doc.Find("link").Map(func(i int, s *goquery.Selection) string {
|
||||||
|
v, exists := s.Attr("href")
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return v
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, link := range links {
|
||||||
|
u, err := url.Parse(link)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(u.Host, "cloudfront.net") {
|
||||||
|
return u.Host, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("no cloudfront url")
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupIpv6Dns(host string) ([]string, error) {
|
||||||
|
c := &dns.Client{
|
||||||
|
Net: "udp",
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := new(dns.Msg)
|
||||||
|
msg.Id = dns.Id()
|
||||||
|
msg.RecursionDesired = true
|
||||||
|
msg.Question = make([]dns.Question, 1)
|
||||||
|
msg.Question[0] = dns.Question{Name: dns.Fqdn(host), Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}
|
||||||
|
|
||||||
|
in, _, err := c.Exchange(msg, "8.8.8.8:53")
|
||||||
|
|
||||||
|
result := make([]string, 0)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if in != nil && in.Rcode != dns.RcodeSuccess {
|
||||||
|
return result, errors.New(dns.RcodeToString[in.Rcode])
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, record := range in.Answer {
|
||||||
|
if t, ok := record.(*dns.AAAA); ok {
|
||||||
|
result = append(result, t.AAAA.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkIpv6Local() ([]string, error) {
|
||||||
|
ifaces, err := net.Interfaces()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := make([]string, 0)
|
||||||
|
|
||||||
|
for _, i := range ifaces {
|
||||||
|
addrs, err := i.Addrs()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
var ip net.IP
|
||||||
|
|
||||||
|
switch v := addr.(type) {
|
||||||
|
case *net.IPNet:
|
||||||
|
ip = v.IP
|
||||||
|
case *net.IPAddr:
|
||||||
|
ip = v.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Check", ip.String())
|
||||||
|
|
||||||
|
if ip == nil || ip.To4() == nil || isPrivateIP(ip) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Checking", ip.String())
|
||||||
|
|
||||||
|
if ipnet, ok := addr.(*net.IPNet); ok {
|
||||||
|
for ip := ipnet.IP.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {
|
||||||
|
ret = append(ret, ip.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, ip.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func inc(ip net.IP) {
|
||||||
|
for j := len(ip) - 1; j >= 0; j-- {
|
||||||
|
ip[j]++
|
||||||
|
if ip[j] > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var privateIPBlocks []*net.IPNet
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for _, cidr := range []string{
|
||||||
|
"127.0.0.0/8", // IPv4 loopback
|
||||||
|
"10.0.0.0/8", // RFC1918
|
||||||
|
"172.16.0.0/12", // RFC1918
|
||||||
|
"192.168.0.0/16", // RFC1918
|
||||||
|
"169.254.0.0/16", // RFC3927 link-local
|
||||||
|
"::1/128", // IPv6 loopback
|
||||||
|
"fe80::/10", // IPv6 link-local
|
||||||
|
"fc00::/7", // IPv6 unique local addr
|
||||||
|
} {
|
||||||
|
_, block, err := net.ParseCIDR(cidr)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("parse error on %q: %v", cidr, err))
|
||||||
|
}
|
||||||
|
privateIPBlocks = append(privateIPBlocks, block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPrivateIP(ip net.IP) bool {
|
||||||
|
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, block := range privateIPBlocks {
|
||||||
|
if block.Contains(ip) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
58
util.go
Normal file
58
util.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
jsonpatch "git.meow.tf/ow-api/ow-api/json-patch"
|
||||||
|
"net/http"
|
||||||
|
"s32x.com/ovrstat/ovrstat"
|
||||||
|
)
|
||||||
|
|
||||||
|
func valueOrDefault(m map[string]interface{}, key string, d int64) int64 {
|
||||||
|
if v, ok := m[key]; ok {
|
||||||
|
switch v.(type) {
|
||||||
|
case int64:
|
||||||
|
return v.(int64)
|
||||||
|
case int:
|
||||||
|
return int64(v.(int))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
type patchOperation struct {
|
||||||
|
Op string `json:"op"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func patchFromOperations(ops []patchOperation) (*jsonpatch.Patch, error) {
|
||||||
|
patchBytes, err := json.Marshal(ops)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
patch, err := jsonpatch.DecodePatch(patchBytes)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &patch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorObject struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeError(w http.ResponseWriter, err error) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err == ovrstat.ErrPlayerNotFound {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(&errorObject{Error: err.Error()}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
22
validator/error.go
Normal file
22
validator/error.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package validator
|
||||||
|
|
||||||
|
func NewSelectorError(message, parent, selector string) *parentSelectorError {
|
||||||
|
return &parentSelectorError{message, parent, selector}
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorString is a trivial implementation of error.
|
||||||
|
type parentSelectorError struct {
|
||||||
|
message, parent, selector string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *parentSelectorError) Error() string {
|
||||||
|
str := e.message + ": "
|
||||||
|
|
||||||
|
if e.parent != "" {
|
||||||
|
str += e.parent + ","
|
||||||
|
}
|
||||||
|
|
||||||
|
str += e.selector
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
196
validator/validator.go
Normal file
196
validator/validator.go
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
baseURL = "https://playoverwatch.com/en-us/career"
|
||||||
|
|
||||||
|
apiURL = "https://playoverwatch.com/en-us/search/account-by-name/"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Platform struct {
|
||||||
|
Platform string `json:"platform"`
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
URLName string `json:"urlName"`
|
||||||
|
PlayerLevel int `json:"playerLevel"`
|
||||||
|
Portrait string `json:"portrait"`
|
||||||
|
IsPublic bool `json:"isPublic"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
careerInitRegexp = regexp.MustCompile(`window\.app\.career\.init\((\d+)\,`)
|
||||||
|
|
||||||
|
// Response/server errors
|
||||||
|
errInvalidResponse = errors.New("error querying the response")
|
||||||
|
errUnexpectedStatus = errors.New("unexpected status code")
|
||||||
|
errRequestBlocked = errors.New("request blocked")
|
||||||
|
errNoApiResponse = errors.New("unable to contact api endpoint")
|
||||||
|
errApiInvalidJson = errors.New("invalid json response")
|
||||||
|
errApiUnexpectedStatus = errors.New("unexpected status code from api")
|
||||||
|
|
||||||
|
// Invalid/missing elements
|
||||||
|
errNoMasthead = errors.New("unable to find masthead")
|
||||||
|
errNoCareerInit = errors.New("no career init call found")
|
||||||
|
|
||||||
|
// Element definitions
|
||||||
|
mastheadElements = []string{
|
||||||
|
"img.player-portrait",
|
||||||
|
"div.player-level div.u-vertical-center",
|
||||||
|
"div.player-level",
|
||||||
|
"div.player-rank",
|
||||||
|
"div.EndorsementIcon-tooltip div.u-center",
|
||||||
|
"div.EndorsementIcon",
|
||||||
|
"div.masthead p.masthead-detail.h4 span",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func ValidateEndpoint() error {
|
||||||
|
url := baseURL + "/pc/cats-11481"
|
||||||
|
|
||||||
|
res, err := http.Get(url)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errInvalidResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode == http.StatusForbidden {
|
||||||
|
return errRequestBlocked
|
||||||
|
} else if res.StatusCode != http.StatusOK {
|
||||||
|
return errUnexpectedStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
pd, err := goquery.NewDocumentFromReader(res.Body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errInvalidResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
masthead := pd.Find("div.masthead")
|
||||||
|
|
||||||
|
if masthead.Length() < 1 {
|
||||||
|
return errNoMasthead
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate masthead elements
|
||||||
|
if err := validateElementsExist(masthead.First(), "div.masthead", mastheadElements...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate API response
|
||||||
|
if err := validateApi(strings.Replace(url[strings.LastIndex(url, "/")+1:], "-", "%23", -1)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate specific elements
|
||||||
|
if err := validateElementsExist(pd.Selection, "", "div#quickplay", "div#competitive"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateDetailedStats(pd.Find("div#quickplay").First(), "div#quickplay"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateDetailedStats(pd.Find("div#competitive").First(), "div#competitive"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateDetailedStats(s *goquery.Selection, parent string) error {
|
||||||
|
if err := validateElementsExist(s, parent, "div.progress-category", "div.js-stats"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Validate hero stats
|
||||||
|
err = validateHeroStats(s.Find("div.progress-category").Parent(), parent)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("No hero stats: " + err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return validateCareerStats(s.Find("div.js-stats").Parent(), parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateHeroStats(s *goquery.Selection, parent string) error {
|
||||||
|
selectors := []string{
|
||||||
|
"div.progress-category", // Top level
|
||||||
|
"div.progress-category div.ProgressBar", // ProgressBar
|
||||||
|
"div.progress-category div.ProgressBar div.ProgressBar-title", // ProgressBar Title
|
||||||
|
"div.progress-category div.ProgressBar div.ProgressBar-description", // ProgressBar Description
|
||||||
|
}
|
||||||
|
|
||||||
|
return validateElementsExist(s, parent, selectors...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCareerStats(careerStatsSelector *goquery.Selection, parent string) error {
|
||||||
|
if err := validateElementsExist(careerStatsSelector, parent, "select option"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
selectors := []string{
|
||||||
|
"div.row div.js-stats", // Top level
|
||||||
|
"div.row div.js-stats div.column", // stat boxes
|
||||||
|
"div.row div.js-stats div.column .stat-title", // stat boxes
|
||||||
|
"div.row div.js-stats div.column table.DataTable", // data table
|
||||||
|
"div.row div.js-stats div.column table.DataTable tbody", // data table tbody
|
||||||
|
"div.row div.js-stats div.column table.DataTable tbody tr", // data table tbody tr
|
||||||
|
"div.row div.js-stats div.column table.DataTable tbody tr td", // data table tbody tr td
|
||||||
|
}
|
||||||
|
|
||||||
|
return validateElementsExist(careerStatsSelector, parent, selectors...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateApi(code string) error {
|
||||||
|
var platforms []Platform
|
||||||
|
|
||||||
|
apires, err := http.Get(apiURL + code)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errNoApiResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
defer apires.Body.Close()
|
||||||
|
|
||||||
|
if apires.StatusCode != http.StatusOK {
|
||||||
|
return errApiUnexpectedStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(apires.Body).Decode(&platforms); err != nil {
|
||||||
|
return errApiInvalidJson
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateElementsExist(s *goquery.Selection, parent string, elements ...string) error {
|
||||||
|
var e *goquery.Selection
|
||||||
|
|
||||||
|
for _, selector := range elements {
|
||||||
|
e = s.Find(selector)
|
||||||
|
|
||||||
|
if e.Length() < 1 {
|
||||||
|
return errors.New("unable to find element: " + parent + " " + selector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
11
validator/validator_test.go
Normal file
11
validator/validator_test.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package validator
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func Test_Validator(t *testing.T) {
|
||||||
|
err := ValidateEndpoint()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user