182 lines
3.2 KiB
Go
182 lines
3.2 KiB
Go
package mycurrencynet
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/PuerkitoBio/goquery"
|
|
"github.com/go-resty/resty/v2"
|
|
|
|
"git.gensokyo.cafe/kkyy/mycurrencynet/iso"
|
|
)
|
|
|
|
var (
|
|
updateUrl = "https://www.mycurrency.net/=US"
|
|
|
|
client = resty.New()
|
|
)
|
|
|
|
type Currency struct {
|
|
Code string
|
|
Name string
|
|
Countries []string // TODO fill this
|
|
Rate float64
|
|
}
|
|
|
|
func (c *Currency) To(t *Currency) float64 {
|
|
if c == nil || t == nil || c.Rate == 0 {
|
|
return 0
|
|
}
|
|
return t.Rate / c.Rate
|
|
}
|
|
|
|
func init() {
|
|
client.
|
|
SetTimeout(5*time.Second).
|
|
SetRetryCount(1).
|
|
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0")
|
|
}
|
|
|
|
func fetch() (html []byte, err error) {
|
|
resp, err := client.R().Get(updateUrl)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if resp.StatusCode() != 200 {
|
|
return nil, fmt.Errorf("HTTP %d %s: %q", resp.StatusCode(), resp.Status(), resp.String())
|
|
}
|
|
|
|
return resp.Body(), nil
|
|
}
|
|
|
|
func parse(body []byte) (map[string]*Currency, error) {
|
|
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret := make(map[string]*Currency)
|
|
|
|
doc.Find("tr.country").Each(func(_ int, s *goquery.Selection) {
|
|
var (
|
|
c Currency
|
|
isoC iso.ISOCurrency
|
|
ok bool
|
|
err error
|
|
rate string
|
|
)
|
|
if c.Code, ok = s.Attr("data-currency-code"); !ok {
|
|
return
|
|
}
|
|
if _, exist := ret[c.Code]; exist {
|
|
return
|
|
}
|
|
|
|
if isoC, ok = iso.Currencies[c.Code]; !ok {
|
|
return
|
|
}
|
|
c.Name = isoC.Name
|
|
|
|
if rate, ok = s.Find(".money[data-rate]").Attr("data-rate"); !ok {
|
|
return
|
|
}
|
|
if c.Rate, err = strconv.ParseFloat(rate, 64); err != nil {
|
|
return
|
|
}
|
|
|
|
ret[c.Code] = &c
|
|
})
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func Fetch() (map[string]*Currency, error) {
|
|
html, err := fetch()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rates, err := parse(html)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// add base currency
|
|
rates["USD"] = &Currency{
|
|
Code: "USD",
|
|
Name: iso.Currencies["USD"].Name,
|
|
Rate: 1,
|
|
}
|
|
return rates, nil
|
|
}
|
|
|
|
type MyCurrencyNet struct {
|
|
rates map[string]*Currency
|
|
lastUpdate time.Time
|
|
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func New() *MyCurrencyNet {
|
|
return &MyCurrencyNet{}
|
|
}
|
|
|
|
func (m *MyCurrencyNet) Get(codes ...string) ([]*Currency, error) {
|
|
if m.rates == nil {
|
|
return nil, fmt.Errorf("not initialized.")
|
|
}
|
|
|
|
ret := make([]*Currency, len(codes))
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
var (
|
|
errors []string
|
|
err error = nil
|
|
)
|
|
for i, code := range codes {
|
|
rate, ok := m.rates[strings.ToUpper(code)]
|
|
if !ok {
|
|
errors = append(errors, "unknown currency code: "+code)
|
|
continue
|
|
}
|
|
var cp Currency = *rate
|
|
ret[i] = &cp
|
|
}
|
|
if len(errors) > 0 {
|
|
err = fmt.Errorf(strings.Join(errors, ", "))
|
|
}
|
|
return ret, err
|
|
}
|
|
|
|
func (m *MyCurrencyNet) Update() error {
|
|
currencies, err := Fetch()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.rates = currencies
|
|
m.lastUpdate = time.Now()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *MyCurrencyNet) UpdateEvery(interval time.Duration, onErr func(error), onSuccess func()) {
|
|
for {
|
|
<-time.After(interval)
|
|
|
|
if err := m.Update(); err != nil {
|
|
if onErr != nil {
|
|
onErr(err)
|
|
}
|
|
} else {
|
|
if onSuccess != nil {
|
|
onSuccess()
|
|
}
|
|
}
|
|
}
|
|
}
|