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