diff --git a/app/widget_maker.go b/app/widget_maker.go index e787e2a2..12abc86d 100644 --- a/app/widget_maker.go +++ b/app/widget_maker.go @@ -23,6 +23,7 @@ import ( "github.com/wtfutil/wtf/modules/docker" "github.com/wtfutil/wtf/modules/exchangerates" "github.com/wtfutil/wtf/modules/feedreader" + "github.com/wtfutil/wtf/modules/finnhub" "github.com/wtfutil/wtf/modules/football" "github.com/wtfutil/wtf/modules/gcal" "github.com/wtfutil/wtf/modules/gerrit" @@ -308,6 +309,9 @@ func MakeWidget( case "exchangerates": settings := exchangerates.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = exchangerates.NewWidget(app, pages, settings) + case "finnhub": + settings := finnhub.NewSettingsFromYAML(moduleName, moduleConfig, config) + widget = finnhub.NewWidget(app, settings) default: settings := unknown.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = unknown.NewWidget(app, settings) diff --git a/go.mod b/go.mod index 0ba1ecb2..31b850a8 100644 --- a/go.mod +++ b/go.mod @@ -25,12 +25,14 @@ require ( github.com/dustin/go-humanize v1.0.0 github.com/gdamore/tcell v1.4.0 github.com/go-ole/go-ole v1.2.4 // indirect + github.com/go-openapi/strfmt v0.19.6 // indirect github.com/godbus/dbus v4.1.0+incompatible // indirect github.com/google/go-github/v32 v32.1.0 github.com/gophercloud/gophercloud v0.5.0 // indirect github.com/hekmon/cunits v2.0.1+incompatible // indirect github.com/hekmon/transmissionrpc v0.0.0-20190525133028-1d589625bacd github.com/imdario/mergo v0.3.8 // indirect + github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/jessevdk/go-flags v1.4.0 github.com/lib/pq v1.2.0 // indirect github.com/logrusorgru/aurora v0.0.0-20190803045625-94edacc10f9b diff --git a/go.sum b/go.sum index b8b33ca5..10f5dfae 100644 --- a/go.sum +++ b/go.sum @@ -106,6 +106,8 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/aws/aws-sdk-go v1.19.11 h1:tqaTGER6Byw3QvsjGW0p018U2UOqaJPeJuzoaF7jjoQ= github.com/aws/aws-sdk-go v1.19.11/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -229,10 +231,15 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/go-openapi/errors v0.19.2 h1:a2kIyV3w+OS3S97zxUndRVD46+FhGOUBDFY7nmu4CsY= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/strfmt v0.19.6 h1:epWc+q5qSgsy7A7+/HYyxLF37vLEYdPSkNB9G8mRqjw= +github.com/go-openapi/strfmt v0.19.6/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4= github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4= @@ -388,6 +395,8 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/itsjamie/gin-cors v0.0.0-20160420130702-97b4a9da7933/go.mod h1:AYdLvrSBFloDBNt7Y8xkQ6gmhCODGl8CPikjyIOnNzA= github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= +github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= @@ -655,6 +664,7 @@ github.com/streadway/amqp v0.0.0-20180528204448-e5adc2ada8b8 h1:l6epF6yBwuejBfhG github.com/streadway/amqp v0.0.0-20180528204448-e5adc2ada8b8/go.mod h1:1WNBiOZtZQLpVAyu0iTduoJL9hEsMloAK5XWrtW0xdY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -667,6 +677,8 @@ github.com/studio-b12/gowebdav v0.0.0-20200303150724-9380631c29a1/go.mod h1:gCcf github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5/go.mod h1:f1SCnEOt6sc3fOJfPQDRDzHOtSXuTtnz0ImG9kPRDV0= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= @@ -711,6 +723,8 @@ github.com/zorkian/go-datadog-api v2.29.0+incompatible/go.mod h1:PkXwHX9CUQa/FpB go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.mongodb.org/mongo-driver v1.0.3 h1:GKoji1ld3tw2aC+GX1wbr/J2fX13yNacEYoJ8Nhr0yU= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= diff --git a/modules/finnhub/client.go b/modules/finnhub/client.go new file mode 100644 index 00000000..45dd7c7c --- /dev/null +++ b/modules/finnhub/client.go @@ -0,0 +1,79 @@ +package finnhub + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +// Client .. +type Client struct { + symbols []string + apiKey string +} + +// NewClient .. +func NewClient(symbols []string, apiKey string) *Client { + client := Client{ + symbols: symbols, + apiKey: apiKey, + } + + return &client +} + +// Getquote .. +func (client *Client) Getquote() ([]Quote, error) { + quotes := []Quote{} + + for _, s := range client.symbols { + resp, err := client.finnhubRequest(s) + if err != nil { + return quotes, err + } + + var quote Quote + quote.Stock = s + err = json.NewDecoder(resp.Body).Decode("e) + if err != nil { + return quotes, err + } + quotes = append(quotes, quote) + } + + return quotes, nil +} + +/* -------------------- Unexported Functions -------------------- */ + +var ( + finnhubURL = &url.URL{Scheme: "https", Host: "finnhub.io", Path: "/api/v1/quote"} +) + +func (client *Client) finnhubRequest(symbol string) (*http.Response, error) { + params := url.Values{} + params.Add("symbol", symbol) + params.Add("token", client.apiKey) + + url := finnhubURL.ResolveReference(&url.URL{RawQuery: params.Encode()}) + + req, err := http.NewRequest("GET", url.String(), nil) + req.Header.Add("Accept", "application/json") + req.Header.Add("Content-Type", "application/json") + if err != nil { + return nil, err + } + + httpClient := &http.Client{} + resp, err := httpClient.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return nil, fmt.Errorf(resp.Status) + } + + return resp, nil +} diff --git a/modules/finnhub/client_test.go b/modules/finnhub/client_test.go new file mode 100644 index 00000000..c73be5ad --- /dev/null +++ b/modules/finnhub/client_test.go @@ -0,0 +1,25 @@ +package finnhub + +import ( + "fmt" + "testing" +) + +func TestFinnhubClient(t *testing.T) { + testClient := &Client{ + symbols: []string{ + "AAPL", + "MSFT", + }, + apiKey: "", + } + + r, err := testClient.Getquote() + if err != nil { + fmt.Println(err) + } + + fmt.Println(r[0].Stock, r[0].C) + fmt.Println(r[1].Stock, r[1].C) + +} diff --git a/modules/finnhub/quote.go b/modules/finnhub/quote.go new file mode 100644 index 00000000..36704b28 --- /dev/null +++ b/modules/finnhub/quote.go @@ -0,0 +1,12 @@ +package finnhub + +type Quote struct { + C float64 `json:"c"` + H float64 `json:"h"` + L float64 `json:"l"` + O float64 `json:"o"` + Pc float64 `json:"pc"` + T int `json:"t"` + + Stock string +} diff --git a/modules/finnhub/settings.go b/modules/finnhub/settings.go new file mode 100644 index 00000000..289f895c --- /dev/null +++ b/modules/finnhub/settings.go @@ -0,0 +1,36 @@ +package finnhub + +import ( + "os" + + "github.com/olebedev/config" + "github.com/wtfutil/wtf/cfg" + "github.com/wtfutil/wtf/utils" +) + +const ( + defaultFocusable = true + defaultTitle = "📈 Stocks Price" +) + +// Settings defines the configuration properties for this module +type Settings struct { + common *cfg.Common + apiKey string `help:"Your finnhub API token."` + symbols []string `help:"An array of stocks symbols (i.e. AAPL, MSFT)"` +} + +// NewSettingsFromYAML creates a new settings instance from a YAML config block +func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings { + + settings := Settings{ + common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig), + + apiKey: ymlConfig.UString("apiKey", ymlConfig.UString("apikey", os.Getenv("WTF_FINNHUB_API_KEY"))), + symbols: utils.ToStrs(ymlConfig.UList("symbols")), + } + + cfg.ModuleSecret(name, globalConfig, &settings.apiKey).Load() + + return &settings +} diff --git a/modules/finnhub/widget.go b/modules/finnhub/widget.go new file mode 100644 index 00000000..e86563bb --- /dev/null +++ b/modules/finnhub/widget.go @@ -0,0 +1,61 @@ +package finnhub + +import ( + "fmt" + + "github.com/jedib0t/go-pretty/table" + "github.com/rivo/tview" + "github.com/wtfutil/wtf/view" +) + +// Widget .. +type Widget struct { + view.TextWidget + *Client + + settings *Settings +} + +// NewWidget .. +func NewWidget(app *tview.Application, settings *Settings) *Widget { + widget := Widget{ + TextWidget: view.NewTextWidget(app, settings.common), + Client: NewClient(settings.symbols, settings.apiKey), + + settings: settings, + } + + return &widget +} + +/* -------------------- Exported Functions -------------------- */ + +func (widget *Widget) Refresh() { + if widget.Disabled() { + return + } + + widget.Redraw(widget.content) +} + +/* -------------------- Unexported Functions -------------------- */ + +func (widget *Widget) content() (string, string, bool) { + quotes, err := widget.Client.Getquote() + + title := widget.CommonSettings().Title + t := table.NewWriter() + t.AppendHeader(table.Row{"#", "Stock", "Current Price", "Open Price", "Change"}) + wrap := false + if err != nil { + wrap = true + } else { + for idx, q := range quotes { + t.AppendRows([]table.Row{ + {idx, q.Stock, q.C, q.O, fmt.Sprintf("%.4f", (q.C-q.O)/q.C)}, + }) + } + } + + return title, t.Render(), wrap +}