all repos — paprika @ 1c51f1922297e7136d39f49dc2b7d002a25da1e4

go rewrite of taigabot

Crypto crap and stonks (#19)

* starting point...

* stocks and crypto

* rename to one word
Anthony DeDominic adedomin@gmail.com
Sat, 15 Jan 2022 21:13:28 -0500
commit

1c51f1922297e7136d39f49dc2b7d002a25da1e4

parent

4713a97cc5df6d8f4d9bd48f83fa1d8429ac58d4

4 files changed, 441 insertions(+), 0 deletions(-)

jump to
A plugins/coingecko.go

@@ -0,0 +1,107 @@

+package plugins + +import ( + "fmt" + "strings" + + coingecko "git.icyphox.sh/paprika/plugins/coingecko" + "github.com/dustin/go-humanize" + "gopkg.in/irc.v3" +) + +type CoinGecko struct{} + +func init() { + Register(CoinGecko{}) +} + +var ( + aliases = []string{".btc", ".eth", ".doge", ".bsc"} + triggers = append([]string{".cg", ".coin", ".crypto"}, aliases...) +) + +func (CoinGecko) Triggers() []string { + return triggers +} + +func formatCgNum(field string, value float64, percent bool) string { + if percent { + v := humanize.CommafWithDigits(value + 0.00000000001, 2) + if value < 0 { + return fmt.Sprintf("%s: \x0304%s%%\x03 ", field, v) + } else { + return fmt.Sprintf("%s: \x0303%s%%\x03 ", field, v) + } + } else if value >= 0.01 { + v := humanize.CommafWithDigits(value + 0.00000000001, 2) + return fmt.Sprintf("%s: $%s ", field, v) + } else { + return fmt.Sprintf("%s: $%.3e ", field, value) + } +} + +func (CoinGecko) Execute(m *irc.Message) (string, error) { + parsed := strings.SplitN(m.Trailing(), " ", 3) + if len(parsed) > 2 { + return fmt.Sprintf("Usage: %s <Ticker>", parsed[0]), nil + } + cmd := parsed[0] + + var coin string + for _, alias := range aliases { + if cmd == alias { + coin = alias[1:] + break + } + } + + var sym string + if len(parsed) != 2 && coin == "" { + return fmt.Sprintf("Usage: %s <Ticker>", parsed[0]), nil + } else if coin != "" { + sym = coin + } else { + sym = parsed[1] + } + + err := coingecko.CheckUpdateCoinList() + if err != nil { + return "", err + } + + cid, err := coingecko.GetCoinId(sym) + if err != nil { + return "", err + } else if cid == "" { + return fmt.Sprintf("No such coin found: %s", sym), nil + } + + stats, err := coingecko.GetCoinPrice(cid) + if err != nil { + return "", err + } else if stats == nil { + return fmt.Sprintf("No such coin found: %s", sym), nil + } + + mData := stats.MarketData + + var outRes strings.Builder + outRes.WriteString(fmt.Sprintf( + "\x02%s (%s)\x02 - ", + stats.Name, stats.Symbol, + )) + outRes.WriteString(formatCgNum("Current", mData.Current.Usd, false)) + outRes.WriteString(formatCgNum("High", mData.High.Usd, false)) + outRes.WriteString(formatCgNum("Low", mData.Low.Usd, false)) + + outRes.WriteString(formatCgNum("MCap", mData.MarketCap.Usd, false)) + outRes.WriteString(fmt.Sprintf("(#%s) ", humanize.Comma(mData.MarketCapRank))) + + outRes.WriteString(formatCgNum("24h", mData.Change24h, true)) + outRes.WriteString(formatCgNum("7d", mData.Change7d, true)) + outRes.WriteString(formatCgNum("30d", mData.Change30d, true)) + outRes.WriteString(formatCgNum("60d", mData.Change60d, true)) + outRes.WriteString(formatCgNum("1y", mData.Change1y, true)) + + return outRes.String(), nil +}
A plugins/coingecko/db.go

@@ -0,0 +1,116 @@

+package coingecko + +import ( + "bytes" + "encoding/gob" + "log" + "time" + + "git.icyphox.sh/paprika/database" + "github.com/dgraph-io/badger/v3" +) + +var ( + canary = []byte("coin-gecko/list-up-to-date") + expire = 72 * time.Hour +) + +func upsertCoinList() error { + coins, err := GetCoinList() + if err != nil { + return err + } + err = database.DB.DB.Update(func(txn *badger.Txn) error { + newCanary := badger. + NewEntry(canary, []byte{}). + WithTTL(time.Duration(expire)) + err := txn.SetEntry(newCanary) + if err != nil { + return err + } + + for _, coin := range coins { + var buf bytes.Buffer + encoder := gob.NewEncoder(&buf) + err := encoder.Encode(coin) + if err != nil { + return err + } + c := buf.Bytes() + err = txn.Set([]byte("coin-gecko/id/"+coin.Id), c) + if err != nil { + return err + } + err = txn.Set([]byte("coin-gecko/symbol/"+coin.Symbol), c) + if err != nil { + return err + } + } + return nil + }) + return err +} + +func GetCoinId(sym string) (string, error) { + var ret string + err := database.DB.DB.View(func(txn *badger.Txn) error { + item, err := txn.Get([]byte("coin-gecko/symbol/" + sym)) + if err != nil && err != badger.ErrKeyNotFound { + return err + } else if err != badger.ErrKeyNotFound { + err = item.Value(func(val []byte) error { + var coinId CoinId + decoder := gob.NewDecoder(bytes.NewReader(val)) + err := decoder.Decode(&coinId) + if err != nil { + return err + } else { + ret = coinId.Id + return nil + } + }) + if err != nil { + return err + } + } + + if ret != "" { + return nil + } + + item, err = txn.Get([]byte("coin-gecko/id/" + sym)) + if err != nil && err != badger.ErrKeyNotFound { + return err + } else if err != badger.ErrKeyNotFound { + err = item.Value(func(val []byte) error { + var coinId CoinId + decoder := gob.NewDecoder(bytes.NewReader(val)) + err := decoder.Decode(&coinId) + if err != nil { + return err + } else { + ret = coinId.Id + return nil + } + }) + if err != nil { + return err + } + } + + return nil + }) + return ret, err +} + +func CheckUpdateCoinList() error { + _, err := database.DB.Get(canary) + if err == badger.ErrKeyNotFound { + log.Print("Updating Coin Gecko Coin IDs List... ") + err = upsertCoinList() + log.Println("Done.") + } else if err != nil { + return err + } + return err +}
A plugins/coingecko/http.go

@@ -0,0 +1,96 @@

+package coingecko + +import ( + "encoding/json" + "net/http" + "net/url" + "time" +) + +var ( + CoinGeckoClient = &http.Client{ + Timeout: 10 * time.Second, + } + apiEndpoint = "https://api.coingecko.com/api/v3/coins" +) + +type CoinId struct { + Id string `json:"id"` + Symbol string `json:"symbol"` +} + +type CoinData struct { + Name string `json:"name"` + Symbol string `json:"symbol"` + MarketData struct { + Current struct { + Usd float64 `json:"usd"` + } `json:"current_price"` + High struct { + Usd float64 `json:"usd"` + } `json:"high_24h"` + Low struct { + Usd float64 `json:"usd"` + } `json:"low_24h"` + MarketCap struct { + Usd float64 `json:"usd"` + } `json:"market_cap"` + MarketCapRank int64 `json:"market_cap_rank"` + Change24h float64 `json:"price_change_percentage_24h"` + Change7d float64 `json:"price_change_percentage_7d"` + Change14d float64 `json:"price_change_percentage_14d"` + Change30d float64 `json:"price_change_percentage_30d"` + Change60d float64 `json:"price_change_percentage_60d"` + Change200d float64 `json:"price_change_percentage_200d"` + Change1y float64 `json:"price_change_percentage_1y"` + } `json:"market_data"` +} + +func GetCoinList() ([]CoinId, error) { + req, err := http.NewRequest("GET", apiEndpoint+"/list", nil) + if err != nil { + return nil, err + } + req.Header.Add("User-Agent", "github.com/icyphox/paprika") + + res, err := CoinGeckoClient.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var resData []CoinId + err = json.NewDecoder(res.Body).Decode(&resData) + if err != nil { + return nil, err + } + + return resData, nil +} + +func GetCoinPrice(coinId string) (*CoinData, error) { + cid := url.PathEscape(coinId) + req, err := http.NewRequest("GET", apiEndpoint+"/"+cid, nil) + if err != nil { + return nil, err + } + req.Header.Add("User-Agent", "github.com/icyphox/paprika") + + res, err := CoinGeckoClient.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode == 404 { + return nil, nil + } + + var resData *CoinData + err = json.NewDecoder(res.Body).Decode(&resData) + if err != nil { + return nil, err + } + + return resData, nil +}
A plugins/stocks.go

@@ -0,0 +1,122 @@

+package plugins + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "time" + + "git.icyphox.sh/paprika/config" + "github.com/dustin/go-humanize" + "gopkg.in/irc.v3" +) + +func init() { + Register(Stocks{}) +} + +type Stocks struct{} + +var ( + stockClient = &http.Client{ + Timeout: 10 * time.Second, + } + api_endpoint = "https://cloud.iexapis.com/v1" + NoIEXApi = errors.New("No IEX API key") +) + +func (Stocks) Triggers() []string { + return []string{".stock", ".stonk"} +} + +type tickerData struct { + Quote struct { + Symbol string `json:"symbol"` + Current float64 `json:"latestPrice"` + High float64 `json:"high,omitempty"` + Low float64 `json:"low,omitempty"` + ChangePercent float64 `json:"changePercent"` + } `json:"quote"` + Stats struct { + Company string `json:"companyName"` + Change1y float64 `json:"year1ChangePercent"` + Change6M float64 `json:"month6ChangePercent"` + Change30d float64 `json:"day30ChangePercent"` + Change5d float64 `json:"day5ChangePercent"` + } `json:"stats"` +} + +func formatMoneyNum(field string, value float64, percent bool) string { + if percent { + v := humanize.CommafWithDigits(value*100+0.00000000001, 2) + if value < 0 { + return fmt.Sprintf("%s: \x0304%s%%\x03 ", field, v) + } else { + return fmt.Sprintf("%s: \x0303%s%%\x03 ", field, v) + } + } else { + v := humanize.CommafWithDigits(value+0.00000000001, 2) + return fmt.Sprintf("%s: $%s ", field, v) + } +} + +func getStock(symbol, apiKey string) (string, error) { + req, err := http.NewRequest("GET", api_endpoint+"/stock/market/batch", nil) + if err != nil { + return "[Stocks] Request construction error.", err + } + req.Header.Add("User-Agent", "github.com/icyphox/paprika") + q := req.URL.Query() + q.Add("token", apiKey) + q.Add("symbols", symbol) + q.Add("types", "quote,stats") + req.URL.RawQuery = q.Encode() + + res, err := stockClient.Do(req) + if err != nil { + return "[Stocks] API Client Error", err + } + defer res.Body.Close() + + if res.StatusCode == 404 { + return fmt.Sprintf("[Stocks] Could not get quote for \x02%s\x02", symbol), nil + } + + var resData map[string]tickerData + err = json.NewDecoder(res.Body).Decode(&resData) + if err != nil { + return "[Stock] API response malformed", err + } + + quote := resData[symbol].Quote + stats := resData[symbol].Stats + var outRes strings.Builder + outRes.WriteString(fmt.Sprintf("\x02%s (%s)\x02 - ", stats.Company, quote.Symbol)) + outRes.WriteString(formatMoneyNum("Current", quote.Current, false)) + if quote.High != 0.0 { + outRes.WriteString(formatMoneyNum("High", quote.High, false)) + outRes.WriteString(formatMoneyNum("Low", quote.Low, false)) + } + outRes.WriteString(formatMoneyNum("24h", quote.ChangePercent, true)) + outRes.WriteString(formatMoneyNum("5d", stats.Change5d, true)) + outRes.WriteString(formatMoneyNum("6M", stats.Change6M, true)) + outRes.WriteString(formatMoneyNum("1y", stats.Change1y, true)) + + return outRes.String(), nil +} + +func (Stocks) Execute(m *irc.Message) (string, error) { + parsed := strings.SplitN(m.Trailing(), " ", 3) + if len(parsed) != 2 { + return fmt.Sprintf("Usage: %s <Ticker>", parsed[0]), nil + } + sym := strings.ToUpper(parsed[1]) + + if apiKey, ok := config.C.ApiKeys["iex"]; ok { + return getStock(sym, apiKey) + } + + return "", NoIEXApi +}