mirror of https://github.com/chubin/wttr.in
Merge branch 'master' of https://github.com/chubin/wttr.in
commit
dd87ab5076
@ -0,0 +1,30 @@
|
||||
run:
|
||||
skip-dirs:
|
||||
- pkg/curlator
|
||||
linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
- wsl
|
||||
- wrapcheck
|
||||
- varnamelen
|
||||
- gci
|
||||
- exhaustivestruct
|
||||
- exhaustruct
|
||||
- gomnd
|
||||
- gofmt
|
||||
|
||||
# to be fixed:
|
||||
- ireturn
|
||||
- gosec
|
||||
- noctx
|
||||
- interfacer
|
||||
|
||||
# deprecated:
|
||||
- scopelint
|
||||
- deadcode
|
||||
- varcheck
|
||||
- maligned
|
||||
- ifshort
|
||||
- nosnakecase
|
||||
- structcheck
|
||||
- golint
|
@ -0,0 +1,9 @@
|
||||
srv: srv.go internal/*/*.go internal/*/*/*.go
|
||||
go build -o srv -ldflags '-w -linkmode external -extldflags "-static"' ./
|
||||
#go build -o srv ./
|
||||
|
||||
go-test:
|
||||
go test ./...
|
||||
|
||||
lint:
|
||||
golangci-lint run ./...
|
After Width: | Height: | Size: 63 KiB |
@ -1,79 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron"
|
||||
)
|
||||
|
||||
var peakRequest30 sync.Map
|
||||
var peakRequest60 sync.Map
|
||||
|
||||
func initPeakHandling() {
|
||||
c := cron.New()
|
||||
// cronTime := fmt.Sprintf("%d,%d * * * *", 30-prefetchInterval/60, 60-prefetchInterval/60)
|
||||
c.AddFunc("24 * * * *", prefetchPeakRequests30)
|
||||
c.AddFunc("54 * * * *", prefetchPeakRequests60)
|
||||
c.Start()
|
||||
}
|
||||
|
||||
func savePeakRequest(cacheDigest string, r *http.Request) {
|
||||
_, min, _ := time.Now().Clock()
|
||||
if min == 30 {
|
||||
peakRequest30.Store(cacheDigest, *r)
|
||||
} else if min == 0 {
|
||||
peakRequest60.Store(cacheDigest, *r)
|
||||
}
|
||||
}
|
||||
|
||||
func prefetchRequest(r *http.Request) {
|
||||
processRequest(r)
|
||||
}
|
||||
|
||||
func syncMapLen(sm *sync.Map) int {
|
||||
count := 0
|
||||
|
||||
f := func(key, value interface{}) bool {
|
||||
|
||||
// Not really certain about this part, don't know for sure
|
||||
// if this is a good check for an entry's existence
|
||||
if key == "" {
|
||||
return false
|
||||
}
|
||||
count++
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
sm.Range(f)
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
func prefetchPeakRequests(peakRequestMap *sync.Map) {
|
||||
peakRequestLen := syncMapLen(peakRequestMap)
|
||||
log.Printf("PREFETCH: Prefetching %d requests\n", peakRequestLen)
|
||||
if peakRequestLen == 0 {
|
||||
return
|
||||
}
|
||||
sleepBetweenRequests := time.Duration(prefetchInterval*1000/peakRequestLen) * time.Millisecond
|
||||
peakRequestMap.Range(func(key interface{}, value interface{}) bool {
|
||||
go func(r http.Request) {
|
||||
prefetchRequest(&r)
|
||||
}(value.(http.Request))
|
||||
peakRequestMap.Delete(key)
|
||||
time.Sleep(sleepBetweenRequests)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func prefetchPeakRequests30() {
|
||||
prefetchPeakRequests(&peakRequest30)
|
||||
}
|
||||
|
||||
func prefetchPeakRequests60() {
|
||||
prefetchPeakRequests(&peakRequest60)
|
||||
}
|
@ -1,199 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func processRequest(r *http.Request) responseWithHeader {
|
||||
var response responseWithHeader
|
||||
|
||||
if response, ok := redirectInsecure(r); ok {
|
||||
return *response
|
||||
}
|
||||
|
||||
if dontCache(r) {
|
||||
return get(r)
|
||||
}
|
||||
|
||||
cacheDigest := getCacheDigest(r)
|
||||
|
||||
foundInCache := false
|
||||
|
||||
savePeakRequest(cacheDigest, r)
|
||||
|
||||
cacheBody, ok := lruCache.Get(cacheDigest)
|
||||
if ok {
|
||||
cacheEntry := cacheBody.(responseWithHeader)
|
||||
|
||||
// if after all attempts we still have no answer,
|
||||
// we try to make the query on our own
|
||||
for attempts := 0; attempts < 300; attempts++ {
|
||||
if !ok || !cacheEntry.InProgress {
|
||||
break
|
||||
}
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
cacheBody, ok = lruCache.Get(cacheDigest)
|
||||
cacheEntry = cacheBody.(responseWithHeader)
|
||||
}
|
||||
if cacheEntry.InProgress {
|
||||
log.Printf("TIMEOUT: %s\n", cacheDigest)
|
||||
}
|
||||
if ok && !cacheEntry.InProgress && cacheEntry.Expires.After(time.Now()) {
|
||||
response = cacheEntry
|
||||
foundInCache = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundInCache {
|
||||
lruCache.Add(cacheDigest, responseWithHeader{InProgress: true})
|
||||
response = get(r)
|
||||
if response.StatusCode == 200 || response.StatusCode == 304 || response.StatusCode == 404 {
|
||||
lruCache.Add(cacheDigest, response)
|
||||
} else {
|
||||
log.Printf("REMOVE: %d response for %s from cache\n", response.StatusCode, cacheDigest)
|
||||
lruCache.Remove(cacheDigest)
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func get(req *http.Request) responseWithHeader {
|
||||
|
||||
client := &http.Client{}
|
||||
|
||||
queryURL := fmt.Sprintf("http://%s%s", req.Host, req.RequestURI)
|
||||
|
||||
proxyReq, err := http.NewRequest(req.Method, queryURL, req.Body)
|
||||
if err != nil {
|
||||
log.Printf("Request: %s\n", err)
|
||||
}
|
||||
|
||||
// proxyReq.Header.Set("Host", req.Host)
|
||||
// proxyReq.Header.Set("X-Forwarded-For", req.RemoteAddr)
|
||||
|
||||
for header, values := range req.Header {
|
||||
for _, value := range values {
|
||||
proxyReq.Header.Add(header, value)
|
||||
}
|
||||
}
|
||||
|
||||
res, err := client.Do(proxyReq)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
return responseWithHeader{
|
||||
InProgress: false,
|
||||
Expires: time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second),
|
||||
Body: body,
|
||||
Header: res.Header,
|
||||
StatusCode: res.StatusCode,
|
||||
}
|
||||
}
|
||||
|
||||
// implementation of the cache.get_signature of original wttr.in
|
||||
func getCacheDigest(req *http.Request) string {
|
||||
|
||||
userAgent := req.Header.Get("User-Agent")
|
||||
|
||||
queryHost := req.Host
|
||||
queryString := req.RequestURI
|
||||
|
||||
clientIPAddress := readUserIP(req)
|
||||
|
||||
lang := req.Header.Get("Accept-Language")
|
||||
|
||||
return fmt.Sprintf("%s:%s%s:%s:%s", userAgent, queryHost, queryString, clientIPAddress, lang)
|
||||
}
|
||||
|
||||
// return true if request should not be cached
|
||||
func dontCache(req *http.Request) bool {
|
||||
|
||||
// dont cache cyclic requests
|
||||
loc := strings.Split(req.RequestURI, "?")[0]
|
||||
return strings.Contains(loc, ":")
|
||||
}
|
||||
|
||||
// redirectInsecure returns redirection response, and bool value, if redirection was needed,
|
||||
// if the query comes from a browser, and it is insecure.
|
||||
//
|
||||
// Insecure queries are marked by the frontend web server
|
||||
// with X-Forwarded-Proto header:
|
||||
//
|
||||
// proxy_set_header X-Forwarded-Proto $scheme;
|
||||
//
|
||||
//
|
||||
func redirectInsecure(req *http.Request) (*responseWithHeader, bool) {
|
||||
if isPlainTextAgent(req.Header.Get("User-Agent")) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if strings.ToLower(req.Header.Get("X-Forwarded-Proto")) == "https" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
target := "https://" + req.Host + req.URL.Path
|
||||
if len(req.URL.RawQuery) > 0 {
|
||||
target += "?" + req.URL.RawQuery
|
||||
}
|
||||
|
||||
body := []byte(fmt.Sprintf(`<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
|
||||
<TITLE>301 Moved</TITLE></HEAD><BODY>
|
||||
<H1>301 Moved</H1>
|
||||
The document has moved
|
||||
<A HREF="%s">here</A>.
|
||||
</BODY></HTML>
|
||||
`, target))
|
||||
|
||||
return &responseWithHeader{
|
||||
InProgress: false,
|
||||
Expires: time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second),
|
||||
Body: body,
|
||||
Header: http.Header{"Location": []string{target}},
|
||||
StatusCode: 301,
|
||||
}, true
|
||||
}
|
||||
|
||||
// isPlainTextAgent returns true if userAgent is a plain-text agent
|
||||
func isPlainTextAgent(userAgent string) bool {
|
||||
userAgentLower := strings.ToLower(userAgent)
|
||||
for _, signature := range plainTextAgents {
|
||||
if strings.Contains(userAgentLower, signature) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func readUserIP(r *http.Request) string {
|
||||
IPAddress := r.Header.Get("X-Real-Ip")
|
||||
if IPAddress == "" {
|
||||
IPAddress = r.Header.Get("X-Forwarded-For")
|
||||
}
|
||||
if IPAddress == "" {
|
||||
IPAddress = r.RemoteAddr
|
||||
var err error
|
||||
IPAddress, _, err = net.SplitHostPort(IPAddress)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: userip: %q is not IP:port\n", IPAddress)
|
||||
}
|
||||
}
|
||||
return IPAddress
|
||||
}
|
||||
|
||||
func randInt(min int, max int) int {
|
||||
return min + rand.Intn(max-min)
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
)
|
||||
|
||||
const serverPort = 8083
|
||||
const uplinkSrvAddr = "127.0.0.1:9002"
|
||||
const uplinkTimeout = 30
|
||||
const prefetchInterval = 300
|
||||
const lruCacheSize = 12800
|
||||
|
||||
// plainTextAgents contains signatures of the plain-text agents
|
||||
var plainTextAgents = []string{
|
||||
"curl",
|
||||
"httpie",
|
||||
"lwp-request",
|
||||
"wget",
|
||||
"python-httpx",
|
||||
"python-requests",
|
||||
"openbsd ftp",
|
||||
"powershell",
|
||||
"fetch",
|
||||
"aiohttp",
|
||||
"http_get",
|
||||
"xh",
|
||||
}
|
||||
|
||||
var lruCache *lru.Cache
|
||||
|
||||
type responseWithHeader struct {
|
||||
InProgress bool // true if the request is being processed
|
||||
Expires time.Time // expiration time of the cache entry
|
||||
|
||||
Body []byte
|
||||
Header http.Header
|
||||
StatusCode int // e.g. 200
|
||||
}
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
lruCache, err = lru.New(lruCacheSize)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: uplinkTimeout * time.Second,
|
||||
KeepAlive: uplinkTimeout * time.Second,
|
||||
DualStack: true,
|
||||
}
|
||||
|
||||
http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, uplinkSrvAddr)
|
||||
}
|
||||
|
||||
initPeakHandling()
|
||||
}
|
||||
|
||||
func copyHeader(dst, src http.Header) {
|
||||
for k, vv := range src {
|
||||
for _, v := range vv {
|
||||
dst.Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// printStat()
|
||||
response := processRequest(r)
|
||||
|
||||
copyHeader(w.Header(), response.Header)
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.WriteHeader(response.StatusCode)
|
||||
w.Write(response.Body)
|
||||
})
|
||||
|
||||
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", serverPort), nil))
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package main
|
||||
|
||||
// import (
|
||||
// "log"
|
||||
// "sync"
|
||||
// "time"
|
||||
// )
|
||||
//
|
||||
// type safeCounter struct {
|
||||
// v map[int]int
|
||||
// mux sync.Mutex
|
||||
// }
|
||||
//
|
||||
// func (c *safeCounter) inc(key int) {
|
||||
// c.mux.Lock()
|
||||
// c.v[key]++
|
||||
// c.mux.Unlock()
|
||||
// }
|
||||
//
|
||||
// // func (c *safeCounter) val(key int) int {
|
||||
// // c.mux.Lock()
|
||||
// // defer c.mux.Unlock()
|
||||
// // return c.v[key]
|
||||
// // }
|
||||
// //
|
||||
// // func (c *safeCounter) reset(key int) int {
|
||||
// // c.mux.Lock()
|
||||
// // defer c.mux.Unlock()
|
||||
// // result := c.v[key]
|
||||
// // c.v[key] = 0
|
||||
// // return result
|
||||
// // }
|
||||
//
|
||||
// var queriesPerMinute safeCounter
|
||||
//
|
||||
// func printStat() {
|
||||
// _, min, _ := time.Now().Clock()
|
||||
// queriesPerMinute.inc(min)
|
||||
// log.Printf("Processed %d requests\n", min)
|
||||
// }
|
@ -0,0 +1,26 @@
|
||||
module github.com/chubin/wttr.in
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/alecthomas/kong v0.7.1 // indirect
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20200910202707-1e08a3fab204 // indirect
|
||||
github.com/go-sql-driver/mysql v1.5.0 // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
|
||||
github.com/hashicorp/golang-lru v0.6.0
|
||||
github.com/itchyny/gojq v0.12.11 // indirect
|
||||
github.com/klauspost/lctime v0.1.0 // indirect
|
||||
github.com/lib/pq v1.8.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
||||
github.com/robfig/cron v1.2.0
|
||||
github.com/samonzeweb/godb v1.0.8 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/smartystreets/assertions v1.2.0 // indirect
|
||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||
github.com/stretchr/testify v1.8.1 // indirect
|
||||
github.com/zsefvlol/timezonemapper v1.0.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
@ -0,0 +1,80 @@
|
||||
github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA=
|
||||
github.com/alecthomas/kong v0.7.1 h1:azoTh0IOfwlAX3qN9sHWTxACE2oV8Bg2gAwBsMwDQY4=
|
||||
github.com/alecthomas/kong v0.7.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
|
||||
github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20200910202707-1e08a3fab204/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
|
||||
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/itchyny/gojq v0.12.11 h1:YhLueoHhHiN4mkfM+3AyJV6EPcCxKZsOnYf+aVSwaQw=
|
||||
github.com/itchyny/gojq v0.12.11/go.mod h1:o3FT8Gkbg/geT4pLI0tF3hvip5F3Y/uskjRz9OYa38g=
|
||||
github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE=
|
||||
github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/klauspost/lctime v0.1.0 h1:nINsuFc860M9cyYhT6vfg6U1USh7kiVBj/s/2b04U70=
|
||||
github.com/klauspost/lctime v0.1.0/go.mod h1:OwdMhr8tbQvusAsnilqkkgDQqivWlqyg0w5cfXkLiDk=
|
||||
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
||||
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
||||
github.com/samonzeweb/godb v1.0.8 h1:WRn6nq0FChYOzh+w8SgpXHUkEhL7W6ZqkCf5Ninx7Uc=
|
||||
github.com/samonzeweb/godb v1.0.8/go.mod h1:LNDt3CakfBwpRY4AD0y/QPTbj+jB6O17tSxQES0p47o=
|
||||
github.com/samonzeweb/godb v1.0.15 h1:HyNb8o1w109as9KWE8ih1YIBe8jC4luJ22f1XNacW38=
|
||||
github.com/samonzeweb/godb v1.0.15/go.mod h1:SxCHqyireDXNrZApknS9lGUEutA43x9eJF632ecbK5Q=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/zsefvlol/timezonemapper v1.0.0 h1:HXqkOzf01gXYh2nDQcDSROikFgMaximnhE8BY9SyF6E=
|
||||
github.com/zsefvlol/timezonemapper v1.0.0/go.mod h1:cVUCOLEmc/VvOMusEhpd2G/UBtadL26ZVz2syODXDoQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
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/sys v0.0.0-20190215142949-d0b11bdaac8a/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-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
@ -0,0 +1,179 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/chubin/wttr.in/internal/types"
|
||||
"github.com/chubin/wttr.in/internal/util"
|
||||
)
|
||||
|
||||
// Config of the program.
|
||||
type Config struct {
|
||||
Cache
|
||||
Geo
|
||||
Logging
|
||||
Server
|
||||
Uplink
|
||||
}
|
||||
|
||||
// Logging configuration.
|
||||
type Logging struct {
|
||||
// AccessLog path.
|
||||
AccessLog string `yaml:"accessLog,omitempty"`
|
||||
|
||||
// ErrorsLog path.
|
||||
ErrorsLog string `yaml:"errorsLog,omitempty"`
|
||||
|
||||
// Interval between access log flushes, in seconds.
|
||||
Interval int `yaml:"interval,omitempty"`
|
||||
}
|
||||
|
||||
// Server configuration.
|
||||
type Server struct {
|
||||
// PortHTTP is port where HTTP server must listen.
|
||||
// If 0, HTTP is disabled.
|
||||
PortHTTP int `yaml:"portHttp,omitempty"`
|
||||
|
||||
// PortHTTP is port where the HTTPS server must listen.
|
||||
// If 0, HTTPS is disabled.
|
||||
PortHTTPS int `yaml:"portHttps,omitempty"`
|
||||
|
||||
// TLSCertFile contains path to cert file for TLS Server.
|
||||
TLSCertFile string `yaml:"tlsCertFile,omitempty"`
|
||||
|
||||
// TLSCertFile contains path to key file for TLS Server.
|
||||
TLSKeyFile string `yaml:"tlsKeyFile,omitempty"`
|
||||
}
|
||||
|
||||
// Uplink configuration.
|
||||
type Uplink struct {
|
||||
// Address contains address of the uplink server in form IP:PORT.
|
||||
Address string `yaml:"address,omitempty"`
|
||||
|
||||
// Timeout for upstream queries.
|
||||
Timeout int `yaml:"timeout,omitempty"`
|
||||
|
||||
// PrefetchInterval contains time (in milliseconds) indicating,
|
||||
// how long the prefetch procedure should take.
|
||||
PrefetchInterval int `yaml:"prefetchInterval,omitempty"`
|
||||
}
|
||||
|
||||
// Cache configuration.
|
||||
type Cache struct {
|
||||
// Size of the main cache.
|
||||
Size int `yaml:"size,omitempty"`
|
||||
}
|
||||
|
||||
// Geo contains geolocation configuration.
|
||||
type Geo struct {
|
||||
// IPCache contains the path to the IP Geodata cache.
|
||||
IPCache string `yaml:"ipCache,omitempty"`
|
||||
|
||||
// IPCacheDB contains the path to the SQLite DB with the IP Geodata cache.
|
||||
IPCacheDB string `yaml:"ipCacheDb,omitempty"`
|
||||
|
||||
IPCacheType types.CacheType `yaml:"ipCacheType,omitempty"`
|
||||
|
||||
// LocationCache contains the path to the Location Geodata cache.
|
||||
LocationCache string `yaml:"locationCache,omitempty"`
|
||||
|
||||
// LocationCacheDB contains the path to the SQLite DB with the Location Geodata cache.
|
||||
LocationCacheDB string `yaml:"locationCacheDb,omitempty"`
|
||||
|
||||
LocationCacheType types.CacheType `yaml:"locationCacheType,omitempty"`
|
||||
|
||||
Nominatim []Nominatim
|
||||
}
|
||||
|
||||
type Nominatim struct {
|
||||
Name string
|
||||
|
||||
// Type describes the type of the location service.
|
||||
// Supported types: iq.
|
||||
Type string
|
||||
|
||||
URL string
|
||||
|
||||
Token string
|
||||
}
|
||||
|
||||
// Default contains the default configuration.
|
||||
func Default() *Config {
|
||||
return &Config{
|
||||
Cache{
|
||||
Size: 12800,
|
||||
},
|
||||
Geo{
|
||||
IPCache: "/wttr.in/cache/ip2l",
|
||||
IPCacheDB: "/wttr.in/cache/geoip.db",
|
||||
IPCacheType: types.CacheTypeDB,
|
||||
LocationCache: "/wttr.in/cache/loc",
|
||||
LocationCacheDB: "/wttr.in/cache/geoloc.db",
|
||||
LocationCacheType: types.CacheTypeDB,
|
||||
Nominatim: []Nominatim{
|
||||
{
|
||||
Name: "locationiq",
|
||||
Type: "iq",
|
||||
URL: "https://eu1.locationiq.com/v1/search",
|
||||
Token: os.Getenv("NOMINATIM_LOCATIONIQ"),
|
||||
},
|
||||
{
|
||||
Name: "opencage",
|
||||
Type: "opencage",
|
||||
URL: "https://api.opencagedata.com/geocode/v1/json",
|
||||
Token: os.Getenv("NOMINATIM_OPENCAGE"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Logging{
|
||||
AccessLog: "/wttr.in/log/access.log",
|
||||
ErrorsLog: "/wttr.in/log/errors.log",
|
||||
Interval: 300,
|
||||
},
|
||||
Server{
|
||||
PortHTTP: 8083,
|
||||
PortHTTPS: 8084,
|
||||
TLSCertFile: "/wttr.in/etc/fullchain.pem",
|
||||
TLSKeyFile: "/wttr.in/etc/privkey.pem",
|
||||
},
|
||||
Uplink{
|
||||
Address: "127.0.0.1:9002",
|
||||
Timeout: 30,
|
||||
PrefetchInterval: 300,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Load config from file.
|
||||
func Load(filename string) (*Config, error) {
|
||||
var (
|
||||
config Config
|
||||
data []byte
|
||||
err error
|
||||
)
|
||||
|
||||
data, err = os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = util.YamlUnmarshalStrict(data, &config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func (c *Config) Dump() []byte {
|
||||
data, err := yaml.Marshal(c)
|
||||
if err != nil {
|
||||
// should never happen.
|
||||
log.Fatalln("config.Dump():", err)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
@ -0,0 +1,774 @@
|
||||
package main
|
||||
|
||||
// Source: https://www.ditig.com/downloads/256-colors.json
|
||||
|
||||
var ansiColorsDB = [][3]float64{
|
||||
{
|
||||
0, 0, 0,
|
||||
},
|
||||
{
|
||||
128, 0, 0,
|
||||
},
|
||||
{
|
||||
0, 128, 0,
|
||||
},
|
||||
{
|
||||
128, 128, 0,
|
||||
},
|
||||
{
|
||||
0, 0, 128,
|
||||
},
|
||||
{
|
||||
128, 0, 128,
|
||||
},
|
||||
{
|
||||
0, 128, 128,
|
||||
},
|
||||
{
|
||||
192, 192, 192,
|
||||
},
|
||||
{
|
||||
128, 128, 128,
|
||||
},
|
||||
{
|
||||
255, 0, 0,
|
||||
},
|
||||
{
|
||||
0, 255, 0,
|
||||
},
|
||||
{
|
||||
255, 255, 0,
|
||||
},
|
||||
{
|
||||
0, 0, 255,
|
||||
},
|
||||
{
|
||||
255, 0, 255,
|
||||
},
|
||||
{
|
||||
0, 255, 255,
|
||||
},
|
||||
{
|
||||
255, 255, 255,
|
||||
},
|
||||
{
|
||||
0, 0, 0,
|
||||
},
|
||||
{
|
||||
0, 0, 95,
|
||||
},
|
||||
{
|
||||
0, 0, 135,
|
||||
},
|
||||
{
|
||||
0, 0, 175,
|
||||
},
|
||||
{
|
||||
0, 0, 215,
|
||||
},
|
||||
{
|
||||
0, 0, 255,
|
||||
},
|
||||
{
|
||||
0, 95, 0,
|
||||
},
|
||||
{
|
||||
0, 95, 95,
|
||||
},
|
||||
{
|
||||
0, 95, 135,
|
||||
},
|
||||
{
|
||||
0, 95, 175,
|
||||
},
|
||||
{
|
||||
0, 95, 215,
|
||||
},
|
||||
{
|
||||
0, 95, 255,
|
||||
},
|
||||
{
|
||||
0, 135, 0,
|
||||
},
|
||||
{
|
||||
0, 135, 95,
|
||||
},
|
||||
{
|
||||
0, 135, 135,
|
||||
},
|
||||
{
|
||||
0, 135, 175,
|
||||
},
|
||||
{
|
||||
0, 135, 215,
|
||||
},
|
||||
{
|
||||
0, 135, 255,
|
||||
},
|
||||
{
|
||||
0, 175, 0,
|
||||
},
|
||||
{
|
||||
0, 175, 95,
|
||||
},
|
||||
{
|
||||
0, 175, 135,
|
||||
},
|
||||
{
|
||||
0, 175, 175,
|
||||
},
|
||||
{
|
||||
0, 175, 215,
|
||||
},
|
||||
{
|
||||
0, 175, 255,
|
||||
},
|
||||
{
|
||||
0, 215, 0,
|
||||
},
|
||||
{
|
||||
0, 215, 95,
|
||||
},
|
||||
{
|
||||
0, 215, 135,
|
||||
},
|
||||
{
|
||||
0, 215, 175,
|
||||
},
|
||||
{
|
||||
0, 215, 215,
|
||||
},
|
||||
{
|
||||
0, 215, 255,
|
||||
},
|
||||
{
|
||||
0, 255, 0,
|
||||
},
|
||||
{
|
||||
0, 255, 95,
|
||||
},
|
||||
{
|
||||
0, 255, 135,
|
||||
},
|
||||
{
|
||||
0, 255, 175,
|
||||
},
|
||||
{
|
||||
0, 255, 215,
|
||||
},
|
||||
{
|
||||
0, 255, 255,
|
||||
},
|
||||
{
|
||||
95, 0, 0,
|
||||
},
|
||||
{
|
||||
95, 0, 95,
|
||||
},
|
||||
{
|
||||
95, 0, 135,
|
||||
},
|
||||
{
|
||||
95, 0, 175,
|
||||
},
|
||||
{
|
||||
95, 0, 215,
|
||||
},
|
||||
{
|
||||
95, 0, 255,
|
||||
},
|
||||
{
|
||||
95, 95, 0,
|
||||
},
|
||||
{
|
||||
95, 95, 95,
|
||||
},
|
||||
{
|
||||
95, 95, 135,
|
||||
},
|
||||
{
|
||||
95, 95, 175,
|
||||
},
|
||||
{
|
||||
95, 95, 215,
|
||||
},
|
||||
{
|
||||
95, 95, 255,
|
||||
},
|
||||
{
|
||||
95, 135, 0,
|
||||
},
|
||||
{
|
||||
95, 135, 95,
|
||||
},
|
||||
{
|
||||
95, 135, 135,
|
||||
},
|
||||
{
|
||||
95, 135, 175,
|
||||
},
|
||||
{
|
||||
95, 135, 215,
|
||||
},
|
||||
{
|
||||
95, 135, 255,
|
||||
},
|
||||
{
|
||||
95, 175, 0,
|
||||
},
|
||||
{
|
||||
95, 175, 95,
|
||||
},
|
||||
{
|
||||
95, 175, 135,
|
||||
},
|
||||
{
|
||||
95, 175, 175,
|
||||
},
|
||||
{
|
||||
95, 175, 215,
|
||||
},
|
||||
{
|
||||
95, 175, 255,
|
||||
},
|
||||
{
|
||||
95, 215, 0,
|
||||
},
|
||||
{
|
||||
95, 215, 95,
|
||||
},
|
||||
{
|
||||
95, 215, 135,
|
||||
},
|
||||
{
|
||||
95, 215, 175,
|
||||
},
|
||||
{
|
||||
95, 215, 215,
|
||||
},
|
||||
{
|
||||
95, 215, 255,
|
||||
},
|
||||
{
|
||||
95, 255, 0,
|
||||
},
|
||||
{
|
||||
95, 255, 95,
|
||||
},
|
||||
{
|
||||
95, 255, 135,
|
||||
},
|
||||
{
|
||||
95, 255, 175,
|
||||
},
|
||||
{
|
||||
95, 255, 215,
|
||||
},
|
||||
{
|
||||
95, 255, 255,
|
||||
},
|
||||
{
|
||||
135, 0, 0,
|
||||
},
|
||||
{
|
||||
135, 0, 95,
|
||||
},
|
||||
{
|
||||
135, 0, 135,
|
||||
},
|
||||
{
|
||||
135, 0, 175,
|
||||
},
|
||||
{
|
||||
135, 0, 215,
|
||||
},
|
||||
{
|
||||
135, 0, 255,
|
||||
},
|
||||
{
|
||||
135, 95, 0,
|
||||
},
|
||||
{
|
||||
135, 95, 95,
|
||||
},
|
||||
{
|
||||
135, 95, 135,
|
||||
},
|
||||
{
|
||||
135, 95, 175,
|
||||
},
|
||||
{
|
||||
135, 95, 215,
|
||||
},
|
||||
{
|
||||
135, 95, 255,
|
||||
},
|
||||
{
|
||||
135, 135, 0,
|
||||
},
|
||||
{
|
||||
135, 135, 95,
|
||||
},
|
||||
{
|
||||
135, 135, 135,
|
||||
},
|
||||
{
|
||||
135, 135, 175,
|
||||
},
|
||||
{
|
||||
135, 135, 215,
|
||||
},
|
||||
{
|
||||
135, 135, 255,
|
||||
},
|
||||
{
|
||||
135, 175, 0,
|
||||
},
|
||||
{
|
||||
135, 175, 95,
|
||||
},
|
||||
{
|
||||
135, 175, 135,
|
||||
},
|
||||
{
|
||||
135, 175, 175,
|
||||
},
|
||||
{
|
||||
135, 175, 215,
|
||||
},
|
||||
{
|
||||
135, 175, 255,
|
||||
},
|
||||
{
|
||||
135, 215, 0,
|
||||
},
|
||||
{
|
||||
135, 215, 95,
|
||||
},
|
||||
{
|
||||
135, 215, 135,
|
||||
},
|
||||
{
|
||||
135, 215, 175,
|
||||
},
|
||||
{
|
||||
135, 215, 215,
|
||||
},
|
||||
{
|
||||
135, 215, 255,
|
||||
},
|
||||
{
|
||||
135, 255, 0,
|
||||
},
|
||||
{
|
||||
135, 255, 95,
|
||||
},
|
||||
{
|
||||
135, 255, 135,
|
||||
},
|
||||
{
|
||||
135, 255, 175,
|
||||
},
|
||||
{
|
||||
135, 255, 215,
|
||||
},
|
||||
{
|
||||
135, 255, 255,
|
||||
},
|
||||
{
|
||||
175, 0, 0,
|
||||
},
|
||||
{
|
||||
175, 0, 95,
|
||||
},
|
||||
{
|
||||
175, 0, 135,
|
||||
},
|
||||
{
|
||||
175, 0, 175,
|
||||
},
|
||||
{
|
||||
175, 0, 215,
|
||||
},
|
||||
{
|
||||
175, 0, 255,
|
||||
},
|
||||
{
|
||||
175, 95, 0,
|
||||
},
|
||||
{
|
||||
175, 95, 95,
|
||||
},
|
||||
{
|
||||
175, 95, 135,
|
||||
},
|
||||
{
|
||||
175, 95, 175,
|
||||
},
|
||||
{
|
||||
175, 95, 215,
|
||||
},
|
||||
{
|
||||
175, 95, 255,
|
||||
},
|
||||
{
|
||||
175, 135, 0,
|
||||
},
|
||||
{
|
||||
175, 135, 95,
|
||||
},
|
||||
{
|
||||
175, 135, 135,
|
||||
},
|
||||
{
|
||||
175, 135, 175,
|
||||
},
|
||||
{
|
||||
175, 135, 215,
|
||||
},
|
||||
{
|
||||
175, 135, 255,
|
||||
},
|
||||
{
|
||||
175, 175, 0,
|
||||
},
|
||||
{
|
||||
175, 175, 95,
|
||||
},
|
||||
{
|
||||
175, 175, 135,
|
||||
},
|
||||
{
|
||||
175, 175, 175,
|
||||
},
|
||||
{
|
||||
175, 175, 215,
|
||||
},
|
||||
{
|
||||
175, 175, 255,
|
||||
},
|
||||
{
|
||||
175, 215, 0,
|
||||
},
|
||||
{
|
||||
175, 215, 95,
|
||||
},
|
||||
{
|
||||
175, 215, 135,
|
||||
},
|
||||
{
|
||||
175, 215, 175,
|
||||
},
|
||||
{
|
||||
175, 215, 215,
|
||||
},
|
||||
{
|
||||
175, 215, 255,
|
||||
},
|
||||
{
|
||||
175, 255, 0,
|
||||
},
|
||||
{
|
||||
175, 255, 95,
|
||||
},
|
||||
{
|
||||
175, 255, 135,
|
||||
},
|
||||
{
|
||||
175, 255, 175,
|
||||
},
|
||||
{
|
||||
175, 255, 215,
|
||||
},
|
||||
{
|
||||
175, 255, 255,
|
||||
},
|
||||
{
|
||||
215, 0, 0,
|
||||
},
|
||||
{
|
||||
215, 0, 95,
|
||||
},
|
||||
{
|
||||
215, 0, 135,
|
||||
},
|
||||
{
|
||||
215, 0, 175,
|
||||
},
|
||||
{
|
||||
215, 0, 215,
|
||||
},
|
||||
{
|
||||
215, 0, 255,
|
||||
},
|
||||
{
|
||||
215, 95, 0,
|
||||
},
|
||||
{
|
||||
215, 95, 95,
|
||||
},
|
||||
{
|
||||
215, 95, 135,
|
||||
},
|
||||
{
|
||||
215, 95, 175,
|
||||
},
|
||||
{
|
||||
215, 95, 215,
|
||||
},
|
||||
{
|
||||
215, 95, 255,
|
||||
},
|
||||
{
|
||||
215, 135, 0,
|
||||
},
|
||||
{
|
||||
215, 135, 95,
|
||||
},
|
||||
{
|
||||
215, 135, 135,
|
||||
},
|
||||
{
|
||||
215, 135, 175,
|
||||
},
|
||||
{
|
||||
215, 135, 215,
|
||||
},
|
||||
{
|
||||
215, 135, 255,
|
||||
},
|
||||
{
|
||||
215, 175, 0,
|
||||
},
|
||||
{
|
||||
215, 175, 95,
|
||||
},
|
||||
{
|
||||
215, 175, 135,
|
||||
},
|
||||
{
|
||||
215, 175, 175,
|
||||
},
|
||||
{
|
||||
215, 175, 215,
|
||||
},
|
||||
{
|
||||
215, 175, 255,
|
||||
},
|
||||
{
|
||||
215, 215, 0,
|
||||
},
|
||||
{
|
||||
215, 215, 95,
|
||||
},
|
||||
{
|
||||
215, 215, 135,
|
||||
},
|
||||
{
|
||||
215, 215, 175,
|
||||
},
|
||||
{
|
||||
215, 215, 215,
|
||||
},
|
||||
{
|
||||
215, 215, 255,
|
||||
},
|
||||
{
|
||||
215, 255, 0,
|
||||
},
|
||||
{
|
||||
215, 255, 95,
|
||||
},
|
||||
{
|
||||
215, 255, 135,
|
||||
},
|
||||
{
|
||||
215, 255, 175,
|
||||
},
|
||||
{
|
||||
215, 255, 215,
|
||||
},
|
||||
{
|
||||
215, 255, 255,
|
||||
},
|
||||
{
|
||||
255, 0, 0,
|
||||
},
|
||||
{
|
||||
255, 0, 95,
|
||||
},
|
||||
{
|
||||
255, 0, 135,
|
||||
},
|
||||
{
|
||||
255, 0, 175,
|
||||
},
|
||||
{
|
||||
255, 0, 215,
|
||||
},
|
||||
{
|
||||
255, 0, 255,
|
||||
},
|
||||
{
|
||||
255, 95, 0,
|
||||
},
|
||||
{
|
||||
255, 95, 95,
|
||||
},
|
||||
{
|
||||
255, 95, 135,
|
||||
},
|
||||
{
|
||||
255, 95, 175,
|
||||
},
|
||||
{
|
||||
255, 95, 215,
|
||||
},
|
||||
{
|
||||
255, 95, 255,
|
||||
},
|
||||
{
|
||||
255, 135, 0,
|
||||
},
|
||||
{
|
||||
255, 135, 95,
|
||||
},
|
||||
{
|
||||
255, 135, 135,
|
||||
},
|
||||
{
|
||||
255, 135, 175,
|
||||
},
|
||||
{
|
||||
255, 135, 215,
|
||||
},
|
||||
{
|
||||
255, 135, 255,
|
||||
},
|
||||
{
|
||||
255, 175, 0,
|
||||
},
|
||||
{
|
||||
255, 175, 95,
|
||||
},
|
||||
{
|
||||
255, 175, 135,
|
||||
},
|
||||
{
|
||||
255, 175, 175,
|
||||
},
|
||||
{
|
||||
255, 175, 215,
|
||||
},
|
||||
{
|
||||
255, 175, 255,
|
||||
},
|
||||
{
|
||||
255, 215, 0,
|
||||
},
|
||||
{
|
||||
255, 215, 95,
|
||||
},
|
||||
{
|
||||
255, 215, 135,
|
||||
},
|
||||
{
|
||||
255, 215, 175,
|
||||
},
|
||||
{
|
||||
255, 215, 215,
|
||||
},
|
||||
{
|
||||
255, 215, 255,
|
||||
},
|
||||
{
|
||||
255, 255, 0,
|
||||
},
|
||||
{
|
||||
255, 255, 95,
|
||||
},
|
||||
{
|
||||
255, 255, 135,
|
||||
},
|
||||
{
|
||||
255, 255, 175,
|
||||
},
|
||||
{
|
||||
255, 255, 215,
|
||||
},
|
||||
{
|
||||
255, 255, 255,
|
||||
},
|
||||
{
|
||||
8, 8, 8,
|
||||
},
|
||||
{
|
||||
18, 18, 18,
|
||||
},
|
||||
{
|
||||
28, 28, 28,
|
||||
},
|
||||
{
|
||||
38, 38, 38,
|
||||
},
|
||||
{
|
||||
48, 48, 48,
|
||||
},
|
||||
{
|
||||
58, 58, 58,
|
||||
},
|
||||
{
|
||||
68, 68, 68,
|
||||
},
|
||||
{
|
||||
78, 78, 78,
|
||||
},
|
||||
{
|
||||
88, 88, 88,
|
||||
},
|
||||
{
|
||||
98, 98, 98,
|
||||
},
|
||||
{
|
||||
108, 108, 108,
|
||||
},
|
||||
{
|
||||
118, 118, 118,
|
||||
},
|
||||
{
|
||||
128, 128, 128,
|
||||
},
|
||||
{
|
||||
138, 138, 138,
|
||||
},
|
||||
{
|
||||
148, 148, 148,
|
||||
},
|
||||
{
|
||||
158, 158, 158,
|
||||
},
|
||||
{
|
||||
168, 168, 168,
|
||||
},
|
||||
{
|
||||
178, 178, 178,
|
||||
},
|
||||
{
|
||||
188, 188, 188,
|
||||
},
|
||||
{
|
||||
198, 198, 198,
|
||||
},
|
||||
{
|
||||
208, 208, 208,
|
||||
},
|
||||
{
|
||||
218, 218, 218,
|
||||
},
|
||||
{
|
||||
228, 228, 228,
|
||||
},
|
||||
{
|
||||
238, 238, 238,
|
||||
},
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
module example.com/m/v2
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/chubin/vt10x v0.0.0-20231112153020-ef4f56837bf1 // indirect
|
||||
github.com/fogleman/gg v1.3.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
golang.org/x/image v0.14.0 // indirect
|
||||
)
|
@ -0,0 +1,8 @@
|
||||
github.com/chubin/vt10x v0.0.0-20231112153020-ef4f56837bf1 h1:CHg5BTAJZmCjBaAAQrD92s248JHH3JTsLlaC6QBJo/Y=
|
||||
github.com/chubin/vt10x v0.0.0-20231112153020-ef4f56837bf1/go.mod h1:mQssL2gI1LTqWgbffl6DESqe6QkAF67ujBdzSe4bWkU=
|
||||
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
||||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
|
||||
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
@ -0,0 +1,224 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/chubin/vt10x"
|
||||
"github.com/fogleman/gg"
|
||||
)
|
||||
|
||||
func StringSliceToRuneSlice(s string) [][]rune {
|
||||
strings := strings.Split(s, "\n")
|
||||
result := make([][]rune, len(strings))
|
||||
|
||||
i := 0 |