mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
- forex_crypto.go: cap the CoinGecko response with io.LimitReader(64KiB); build the simple/price query via url.Values instead of bare Sprintf. - dnd_expedition_camp.go: a combat-session lookup error now fails open (allow camp) + logs a warning, instead of blocking standard camp with a misleading empty 'not cleared' rejection.
125 lines
3.3 KiB
Go
125 lines
3.3 KiB
Go
package plugin
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// ── Crypto support (CoinGecko, keyless) ──────────────────────────────────────
|
|
//
|
|
// CoinGecko's public simple/price endpoint needs no API key and no usage tier,
|
|
// matching Frankfurter's keyless model. Crypto is wired into the *conversion*
|
|
// path only (`!fx 100 USD to BTC`). The rate/report/alert analysis stays
|
|
// fiat-only on purpose: a daily-snapshot "buy signal" or 52-week range is
|
|
// meaningless for 24/7 crypto markets.
|
|
|
|
type fxCryptoInfo struct {
|
|
CoinGeckoID string
|
|
Emoji string
|
|
Label string
|
|
}
|
|
|
|
// fxCryptoMeta maps a ticker to its CoinGecko coin ID and display metadata.
|
|
// Add a row here to support another asset.
|
|
var fxCryptoMeta = map[string]fxCryptoInfo{
|
|
"BTC": {"bitcoin", "₿", "Bitcoin"},
|
|
"ETH": {"ethereum", "Ξ", "Ethereum"},
|
|
"SOL": {"solana", "◎", "Solana"},
|
|
"XRP": {"ripple", "✕", "XRP"},
|
|
"DOGE": {"dogecoin", "🐕", "Dogecoin"},
|
|
"ADA": {"cardano", "₳", "Cardano"},
|
|
"LTC": {"litecoin", "Ł", "Litecoin"},
|
|
}
|
|
|
|
// fxIsCrypto reports whether sym is a supported crypto asset.
|
|
func fxIsCrypto(sym string) bool {
|
|
_, ok := fxCryptoMeta[strings.ToUpper(sym)]
|
|
return ok
|
|
}
|
|
|
|
// fxIsConvertible reports whether a symbol can appear in a conversion —
|
|
// USD, a tracked fiat currency, or a supported crypto asset.
|
|
func fxIsConvertible(cur string) bool {
|
|
return fxIsSupported(cur) || fxIsCrypto(cur)
|
|
}
|
|
|
|
const coinGeckoBaseURL = "https://api.coingecko.com/api/v3"
|
|
|
|
// Short-TTL price cache to stay well under CoinGecko's public rate limit and
|
|
// keep repeated conversions snappy.
|
|
const cgCacheTTL = 60 * time.Second
|
|
|
|
var (
|
|
cgMu sync.Mutex
|
|
cgCache = map[string]cgCacheEntry{}
|
|
)
|
|
|
|
type cgCacheEntry struct {
|
|
priceUSD float64
|
|
at time.Time
|
|
}
|
|
|
|
// cgSpotUSD returns the USD spot price for one unit of the given crypto symbol.
|
|
func (p *ForexPlugin) cgSpotUSD(sym string) (float64, error) {
|
|
sym = strings.ToUpper(sym)
|
|
info, ok := fxCryptoMeta[sym]
|
|
if !ok {
|
|
return 0, fmt.Errorf("unsupported crypto %s", sym)
|
|
}
|
|
|
|
cgMu.Lock()
|
|
if e, ok := cgCache[sym]; ok && time.Since(e.at) < cgCacheTTL {
|
|
cgMu.Unlock()
|
|
return e.priceUSD, nil
|
|
}
|
|
cgMu.Unlock()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
|
defer cancel()
|
|
|
|
q := url.Values{}
|
|
q.Set("ids", info.CoinGeckoID)
|
|
q.Set("vs_currencies", "usd")
|
|
reqURL := coinGeckoBaseURL + "/simple/price?" + q.Encode()
|
|
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
resp, err := p.httpClient.Do(req)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("coingecko API error: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return 0, fmt.Errorf("coingecko API returned %d", resp.StatusCode)
|
|
}
|
|
|
|
var data map[string]map[string]float64
|
|
if err := json.NewDecoder(io.LimitReader(resp.Body, 64<<10)).Decode(&data); err != nil {
|
|
return 0, fmt.Errorf("coingecko decode error: %w", err)
|
|
}
|
|
entry, ok := data[info.CoinGeckoID]
|
|
if !ok {
|
|
return 0, fmt.Errorf("no price for %s", sym)
|
|
}
|
|
price, ok := entry["usd"]
|
|
if !ok || price <= 0 {
|
|
return 0, fmt.Errorf("invalid price for %s", sym)
|
|
}
|
|
|
|
cgMu.Lock()
|
|
cgCache[sym] = cgCacheEntry{priceUSD: price, at: time.Now()}
|
|
cgMu.Unlock()
|
|
|
|
return price, nil
|
|
}
|