From 4c9990bafd995e9143cdf82a21cbb02810956eab Mon Sep 17 00:00:00 2001 From: Casey Primozic Date: Tue, 22 Oct 2019 11:58:20 -0700 Subject: [PATCH 1/5] Implement initial twitterstats module * Create module skeleton based off of the existing twitter module * Strip out unused pieces and try to make it as minimal as possible * Implement settings parsing, converting the untyped `screenNames` slice into an slice of strings * Implement initial minimal display, showing a table with all usernames and their follower count + # of tweets (using dummy metrics for now) --- app/widget_maker.go | 4 +++ modules/twitterstats/client.go | 48 ++++++++++++++++++++++++++ modules/twitterstats/settings.go | 39 +++++++++++++++++++++ modules/twitterstats/widget.go | 58 ++++++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 modules/twitterstats/client.go create mode 100644 modules/twitterstats/settings.go create mode 100644 modules/twitterstats/widget.go diff --git a/app/widget_maker.go b/app/widget_maker.go index cab1821a..17ef4b1d 100644 --- a/app/widget_maker.go +++ b/app/widget_maker.go @@ -54,6 +54,7 @@ import ( "github.com/wtfutil/wtf/modules/travisci" "github.com/wtfutil/wtf/modules/trello" "github.com/wtfutil/wtf/modules/twitter" + "github.com/wtfutil/wtf/modules/twitterstats" "github.com/wtfutil/wtf/modules/unknown" "github.com/wtfutil/wtf/modules/victorops" "github.com/wtfutil/wtf/modules/weatherservices/arpansagovau" @@ -239,6 +240,9 @@ func MakeWidget( case "twitter": settings := twitter.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = twitter.NewWidget(app, pages, settings) + case "twitterstats": + settings := twitterstats.NewSettingsFromYAML(moduleName, moduleConfig, config) + widget = twitterstats.NewWidget(app, pages, settings) case "victorops": settings := victorops.NewSettingsFromYAML(moduleName, moduleConfig, config) widget = victorops.NewWidget(app, settings) diff --git a/modules/twitterstats/client.go b/modules/twitterstats/client.go new file mode 100644 index 00000000..353d3e54 --- /dev/null +++ b/modules/twitterstats/client.go @@ -0,0 +1,48 @@ +package twitterstats + +import ( + "log" +) + +type Client struct { + apiBase string + consumerKey string + consumerSecret string + accessToken string + accessTokenSecret string + screenNames []string +} + +func NewClient(settings *Settings) *Client { + usernames := make([]string, len(settings.screenNames)) + for i, username := range settings.screenNames { + switch username.(type) { + default: + { + log.Fatalf("All `screenName`s in twitterstats config must be of type string") + } + case string: + usernames[i] = username.(string) + } + + } + + client := Client{ + apiBase: "https://api.twitter.com/1.1/", + consumerKey: settings.consumerKey, + consumerSecret: settings.consumerSecret, + accessToken: settings.accessToken, + accessTokenSecret: settings.accessTokenSecret, + screenNames: usernames, + } + + return &client +} + +func (client *Client) GetFollowerCounts() []int64 { + return []int64{0, 0, 0, 0, 0} // TODO +} + +func (client *Client) GetTweetCounts() []int64 { + return []int64{0, 0, 0, 0, 0} // TODO +} diff --git a/modules/twitterstats/settings.go b/modules/twitterstats/settings.go new file mode 100644 index 00000000..9229a59d --- /dev/null +++ b/modules/twitterstats/settings.go @@ -0,0 +1,39 @@ +package twitterstats + +import ( + "os" + + "github.com/olebedev/config" + "github.com/wtfutil/wtf/cfg" +) + +const ( + defaultFocusable = true + defaultTitle = "Twitter Stats" +) + +type Settings struct { + common *cfg.Common + + consumerKey string + consumerSecret string + accessToken string + accessTokenSecret string + + screenNames []interface{} +} + +func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings { + settings := Settings{ + common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig), + + consumerKey: ymlConfig.UString("consumerKey", os.Getenv("WTF_TWITTER_CONSUMER_KEY")), + consumerSecret: ymlConfig.UString("consumerSecret", os.Getenv("WTF_TWITTER_CONSUMER_SECRET")), + accessToken: ymlConfig.UString("accessToken", os.Getenv("WTF_TWITTER_ACCESS_TOKEN")), + accessTokenSecret: ymlConfig.UString("accessTokenSecret", os.Getenv("WTF_TWITTER_ACCESS_TOKEN_SECRET")), + + screenNames: ymlConfig.UList("screenNames"), + } + + return &settings +} diff --git a/modules/twitterstats/widget.go b/modules/twitterstats/widget.go new file mode 100644 index 00000000..eb12f87b --- /dev/null +++ b/modules/twitterstats/widget.go @@ -0,0 +1,58 @@ +package twitterstats + +import ( + "fmt" + + "github.com/rivo/tview" + "github.com/wtfutil/wtf/view" +) + +type Widget struct { + view.KeyboardWidget + view.TextWidget + + client *Client + idx int + settings *Settings + sources []string +} + +func NewWidget(app *tview.Application, pages *tview.Pages, settings *Settings) *Widget { + widget := Widget{ + TextWidget: view.NewTextWidget(app, settings.common), + + idx: 0, + settings: settings, + } + + widget.client = NewClient(settings) + + widget.View.SetBorderPadding(1, 1, 1, 1) + widget.View.SetWrap(true) + widget.View.SetWordWrap(true) + + return &widget +} + +func (widget *Widget) Refresh() { + widget.Redraw(widget.content) +} + +func (widget *Widget) content() (string, string, bool) { + usernames := widget.client.screenNames + followerCounts := widget.client.GetFollowerCounts() + tweetCounts := widget.client.GetTweetCounts() + + // Add header row + str := fmt.Sprintf("%-16s %-8s %-8s\n", "Username", "Followers", "# Tweets") + + // Add rows for each of the followed usernames + for i, username := range usernames { + followerCount := followerCounts[i] + tweetCount := tweetCounts[i] + + str += fmt.Sprintf("%-16s %8d %8d\n", username, followerCount, tweetCount) + } + + return "Twitter Stats", str, true +} From 0be63a404c71b457523cf079a57ddbe922b45ad3 Mon Sep 17 00:00:00 2001 From: Casey Primozic Date: Tue, 22 Oct 2019 12:42:11 -0700 Subject: [PATCH 2/5] Implement Twitter API fetching for twitterstats * Create Oauth2 client configured for Twitter and create a HTTP client out of that * Fetch user stats for each of the users, parse out of JSON, and return stats as stats structs --- modules/twitterstats/client.go | 80 ++++++++++++++++++++++++++-------- modules/twitterstats/widget.go | 7 ++- 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/modules/twitterstats/client.go b/modules/twitterstats/client.go index 353d3e54..247017ac 100644 --- a/modules/twitterstats/client.go +++ b/modules/twitterstats/client.go @@ -1,18 +1,31 @@ package twitterstats import ( + "encoding/json" + "fmt" + "io/ioutil" "log" + "net/http" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" ) type Client struct { - apiBase string - consumerKey string - consumerSecret string - accessToken string - accessTokenSecret string - screenNames []string + apiBase string + httpClient *http.Client + screenNames []string } +type TwitterStats struct { + followerCount int64 + tweetCount int64 +} + +const ( + userTimelineUrl = "https://api.twitter.com/1.1/users/show.json" +) + func NewClient(settings *Settings) *Client { usernames := make([]string, len(settings.screenNames)) for i, username := range settings.screenNames { @@ -27,22 +40,55 @@ func NewClient(settings *Settings) *Client { } + conf := &clientcredentials.Config{ + ClientID: settings.consumerKey, + ClientSecret: settings.consumerSecret, + TokenURL: "https://api.twitter.com/oauth2/token", + } + + // token, err := conf.Token(oauth2.NoContext) + httpClient := conf.Client(oauth2.NoContext) + client := Client{ - apiBase: "https://api.twitter.com/1.1/", - consumerKey: settings.consumerKey, - consumerSecret: settings.consumerSecret, - accessToken: settings.accessToken, - accessTokenSecret: settings.accessTokenSecret, - screenNames: usernames, + apiBase: "https://api.twitter.com/1.1/", + httpClient: httpClient, + screenNames: usernames, } return &client } -func (client *Client) GetFollowerCounts() []int64 { - return []int64{0, 0, 0, 0, 0} // TODO -} +func (client *Client) GetStats() []TwitterStats { + stats := make([]TwitterStats, len(client.screenNames)) -func (client *Client) GetTweetCounts() []int64 { - return []int64{0, 0, 0, 0, 0} // TODO + for i, username := range client.screenNames { + stats[i] = TwitterStats{ + followerCount: 0, + tweetCount: 0, + } + + res, err := client.httpClient.Get(fmt.Sprintf("%s?screen_name=%s", userTimelineUrl, username)) + if err != nil { + continue + } + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + continue + } + + var parsed map[string]interface{} + err = json.Unmarshal(body, &parsed) + if err != nil { + continue + } + + stats[i] = TwitterStats{ + followerCount: int64(parsed["followers_count"].(float64)), + tweetCount: int64(parsed["statuses_count"].(float64)), + } + } + + return stats } diff --git a/modules/twitterstats/widget.go b/modules/twitterstats/widget.go index eb12f87b..26f65a7f 100644 --- a/modules/twitterstats/widget.go +++ b/modules/twitterstats/widget.go @@ -40,16 +40,15 @@ func (widget *Widget) Refresh() { func (widget *Widget) content() (string, string, bool) { usernames := widget.client.screenNames - followerCounts := widget.client.GetFollowerCounts() - tweetCounts := widget.client.GetTweetCounts() + stats := widget.client.GetStats() // Add header row str := fmt.Sprintf("%-16s %-8s %-8s\n", "Username", "Followers", "# Tweets") // Add rows for each of the followed usernames for i, username := range usernames { - followerCount := followerCounts[i] - tweetCount := tweetCounts[i] + followerCount := stats[i].followerCount + tweetCount := stats[i].tweetCount str += fmt.Sprintf("%-16s %8d %8d\n", username, followerCount, tweetCount) } From 3c95d8e39d2728cf73978f70a9dd04fbf0527efc Mon Sep 17 00:00:00 2001 From: Casey Primozic Date: Tue, 22 Oct 2019 13:23:17 -0700 Subject: [PATCH 3/5] Improve styling + remove unused code * Got rid of unused struct fields, unused settings, added some comments to functions + structs --- modules/twitterstats/client.go | 13 +++++++------ modules/twitterstats/settings.go | 15 +++++---------- modules/twitterstats/widget.go | 14 +++----------- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/modules/twitterstats/client.go b/modules/twitterstats/client.go index 247017ac..d3cd059b 100644 --- a/modules/twitterstats/client.go +++ b/modules/twitterstats/client.go @@ -11,21 +11,23 @@ import ( "golang.org/x/oauth2/clientcredentials" ) +// Client contains state that allows stats to be fetched about a list of Twitter users type Client struct { - apiBase string httpClient *http.Client screenNames []string } +// TwitterStats Represents a stats snapshot for a single Twitter user at a point in time type TwitterStats struct { followerCount int64 tweetCount int64 } const ( - userTimelineUrl = "https://api.twitter.com/1.1/users/show.json" + userTimelineURL = "https://api.twitter.com/1.1/users/show.json" ) +// NewClient creates a new twitterstats client that contains an OAuth2 HTTP client which can be used func NewClient(settings *Settings) *Client { usernames := make([]string, len(settings.screenNames)) for i, username := range settings.screenNames { @@ -45,12 +47,9 @@ func NewClient(settings *Settings) *Client { ClientSecret: settings.consumerSecret, TokenURL: "https://api.twitter.com/oauth2/token", } - - // token, err := conf.Token(oauth2.NoContext) httpClient := conf.Client(oauth2.NoContext) client := Client{ - apiBase: "https://api.twitter.com/1.1/", httpClient: httpClient, screenNames: usernames, } @@ -58,6 +57,8 @@ func NewClient(settings *Settings) *Client { return &client } +// GetStats Returns a slice of `TwitterStats` structs for each username in `client.screenNames` in the same +// order of `client.screenNames` func (client *Client) GetStats() []TwitterStats { stats := make([]TwitterStats, len(client.screenNames)) @@ -67,7 +68,7 @@ func (client *Client) GetStats() []TwitterStats { tweetCount: 0, } - res, err := client.httpClient.Get(fmt.Sprintf("%s?screen_name=%s", userTimelineUrl, username)) + res, err := client.httpClient.Get(fmt.Sprintf("%s?screen_name=%s", userTimelineURL, username)) if err != nil { continue } diff --git a/modules/twitterstats/settings.go b/modules/twitterstats/settings.go index 9229a59d..cd2122af 100644 --- a/modules/twitterstats/settings.go +++ b/modules/twitterstats/settings.go @@ -15,22 +15,17 @@ const ( type Settings struct { common *cfg.Common - consumerKey string - consumerSecret string - accessToken string - accessTokenSecret string - - screenNames []interface{} + consumerKey string + consumerSecret string + screenNames []interface{} } func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings { settings := Settings{ common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig), - consumerKey: ymlConfig.UString("consumerKey", os.Getenv("WTF_TWITTER_CONSUMER_KEY")), - consumerSecret: ymlConfig.UString("consumerSecret", os.Getenv("WTF_TWITTER_CONSUMER_SECRET")), - accessToken: ymlConfig.UString("accessToken", os.Getenv("WTF_TWITTER_ACCESS_TOKEN")), - accessTokenSecret: ymlConfig.UString("accessTokenSecret", os.Getenv("WTF_TWITTER_ACCESS_TOKEN_SECRET")), + consumerKey: ymlConfig.UString("consumerKey", os.Getenv("WTF_TWITTER_CONSUMER_KEY")), + consumerSecret: ymlConfig.UString("consumerSecret", os.Getenv("WTF_TWITTER_CONSUMER_SECRET")), screenNames: ymlConfig.UList("screenNames"), } diff --git a/modules/twitterstats/widget.go b/modules/twitterstats/widget.go index 26f65a7f..672915ec 100644 --- a/modules/twitterstats/widget.go +++ b/modules/twitterstats/widget.go @@ -8,25 +8,17 @@ import ( ) type Widget struct { - view.KeyboardWidget view.TextWidget - client *Client - idx int - settings *Settings - sources []string + client *Client } func NewWidget(app *tview.Application, pages *tview.Pages, settings *Settings) *Widget { widget := Widget{ TextWidget: view.NewTextWidget(app, settings.common), - - idx: 0, - settings: settings, + client: NewClient(settings), } - widget.client = NewClient(settings) - widget.View.SetBorderPadding(1, 1, 1, 1) widget.View.SetWrap(true) widget.View.SetWordWrap(true) @@ -43,7 +35,7 @@ func (widget *Widget) content() (string, string, bool) { stats := widget.client.GetStats() // Add header row - str := fmt.Sprintf("%-16s %-8s %-8s\n", "Username", "Followers", "# Tweets") + str := fmt.Sprintf("%-16s %8s %8s\n", "Username", "Followers", "Tweets") // Add rows for each of the followed usernames for i, username := range usernames { From d82eda19338ead96515a1c699d7212734105399d Mon Sep 17 00:00:00 2001 From: Casey Primozic Date: Wed, 23 Oct 2019 11:49:33 -0700 Subject: [PATCH 4/5] Improvements to twitterstats code from review * Unmarshal directly into a `TwitterStats` struct by using json struct annotations * Pull stats fetching for a single user out into its own function so that closing the request body is done after each request and the HTTP request can be re-used for multiple usernames' stats * Improve type casting code used in settings parsing logic --- modules/twitterstats/client.go | 68 ++++++++++++++++------------------ modules/twitterstats/widget.go | 4 +- 2 files changed, 34 insertions(+), 38 deletions(-) diff --git a/modules/twitterstats/client.go b/modules/twitterstats/client.go index d3cd059b..6a18a09a 100644 --- a/modules/twitterstats/client.go +++ b/modules/twitterstats/client.go @@ -19,8 +19,8 @@ type Client struct { // TwitterStats Represents a stats snapshot for a single Twitter user at a point in time type TwitterStats struct { - followerCount int64 - tweetCount int64 + FollowerCount int64 `json:"followers_count"` + TweetCount int64 `json:"statuses_count"` } const ( @@ -31,15 +31,10 @@ const ( func NewClient(settings *Settings) *Client { usernames := make([]string, len(settings.screenNames)) for i, username := range settings.screenNames { - switch username.(type) { - default: - { - log.Fatalf("All `screenName`s in twitterstats config must be of type string") - } - case string: - usernames[i] = username.(string) + var ok bool + if usernames[i], ok = username.(string); !ok { + log.Fatalf("All `screenName`s in twitterstats config must be of type string") } - } conf := &clientcredentials.Config{ @@ -57,38 +52,39 @@ func NewClient(settings *Settings) *Client { return &client } +// GetStatsForUser Fetches stats for a single user. If there is an error fetching or parsing the response +// from the Twitter API, an empty stats struct will be returned. +func (client *Client) GetStatsForUser(username string) TwitterStats { + stats := TwitterStats{ + FollowerCount: 0, + TweetCount: 0, + } + + url := fmt.Sprintf("%s?screen_name=%s", userTimelineURL, username) + res, err := client.httpClient.Get(url) + if err != nil { + return stats + } + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return stats + } + + // If there is an error while parsing, just discard the error and return the empty stats + json.Unmarshal(body, &stats) + + return stats +} + // GetStats Returns a slice of `TwitterStats` structs for each username in `client.screenNames` in the same // order of `client.screenNames` func (client *Client) GetStats() []TwitterStats { stats := make([]TwitterStats, len(client.screenNames)) for i, username := range client.screenNames { - stats[i] = TwitterStats{ - followerCount: 0, - tweetCount: 0, - } - - res, err := client.httpClient.Get(fmt.Sprintf("%s?screen_name=%s", userTimelineURL, username)) - if err != nil { - continue - } - defer res.Body.Close() - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - continue - } - - var parsed map[string]interface{} - err = json.Unmarshal(body, &parsed) - if err != nil { - continue - } - - stats[i] = TwitterStats{ - followerCount: int64(parsed["followers_count"].(float64)), - tweetCount: int64(parsed["statuses_count"].(float64)), - } + stats[i] = client.GetStatsForUser(username) } return stats diff --git a/modules/twitterstats/widget.go b/modules/twitterstats/widget.go index 672915ec..985f226e 100644 --- a/modules/twitterstats/widget.go +++ b/modules/twitterstats/widget.go @@ -39,8 +39,8 @@ func (widget *Widget) content() (string, string, bool) { // Add rows for each of the followed usernames for i, username := range usernames { - followerCount := stats[i].followerCount - tweetCount := stats[i].tweetCount + followerCount := stats[i].FollowerCount + tweetCount := stats[i].TweetCount str += fmt.Sprintf("%-16s %8d %8d\n", username, followerCount, tweetCount) } From a99af9a0919490d76f06d7e416e2483a5184a104 Mon Sep 17 00:00:00 2001 From: Casey Primozic Date: Tue, 29 Oct 2019 03:15:57 -0700 Subject: [PATCH 5/5] Support both bearer + consumer tokens for Twitter modules * Add support to both the twitter and twitterstats modules for authenticating using both bearer tokens as well as consumer key + secret. * A bearer token is defaulted to if it's supplied * Add this support to both the twitterstats module as well as to the existing twitter module, modifying its functionality to re-use the same HTTP client and handle authentication upfront via oauth2 --- modules/twitter/client.go | 41 +++++++++++++++++++++++++------- modules/twitter/request.go | 14 ++--------- modules/twitter/settings.go | 16 ++++++++----- modules/twitterstats/client.go | 23 ++++++++++++++---- modules/twitterstats/settings.go | 2 ++ 5 files changed, 64 insertions(+), 32 deletions(-) diff --git a/modules/twitter/client.go b/modules/twitter/client.go index affe0a20..2da812dd 100644 --- a/modules/twitter/client.go +++ b/modules/twitter/client.go @@ -1,9 +1,14 @@ package twitter import ( + "context" "encoding/json" "fmt" + "net/http" "strconv" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" ) /* NOTE: Currently single application ONLY @@ -12,19 +17,37 @@ import ( // Client represents the data required to connect to the Twitter API type Client struct { - apiBase string - bearerToken string - count int - screenName string + apiBase string + count int + screenName string + httpClient *http.Client } // NewClient creates and returns a new Twitter client func NewClient(settings *Settings) *Client { + var httpClient *http.Client + // If a bearer token is supplied, use that directly. Otherwise, let the Oauth client fetch a token + // using the consumer key and secret. + if settings.bearerToken == "" { + conf := &clientcredentials.Config{ + ClientID: settings.consumerKey, + ClientSecret: settings.consumerSecret, + TokenURL: "https://api.twitter.com/oauth2/token", + } + httpClient = conf.Client(oauth2.NoContext) + } else { + ctx := context.Background() + httpClient = oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: settings.bearerToken, + TokenType: "Bearer", + })) + } + client := Client{ - apiBase: "https://api.twitter.com/1.1/", - count: settings.count, - screenName: "", - bearerToken: settings.bearerToken, + apiBase: "https://api.twitter.com/1.1/", + count: settings.count, + screenName: "", + httpClient: httpClient, } return &client @@ -53,7 +76,7 @@ func (client *Client) tweets() (tweets []Tweet, err error) { strconv.Itoa(client.count), ) - data, err := Request(client.bearerToken, apiURL) + data, err := Request(client.httpClient, apiURL) if err != nil { return tweets, err } diff --git a/modules/twitter/request.go b/modules/twitter/request.go index e20a306c..8c4ee915 100644 --- a/modules/twitter/request.go +++ b/modules/twitter/request.go @@ -2,21 +2,11 @@ package twitter import ( "bytes" - "fmt" "net/http" ) -func Request(bearerToken string, apiURL string) ([]byte, error) { - req, err := http.NewRequest("GET", apiURL, nil) - if err != nil { - return nil, err - } - - // Expected authorization format for single-application twitter dev accounts - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", bearerToken)) - - client := &http.Client{} - resp, err := client.Do(req) +func Request(httpClient *http.Client, apiURL string) ([]byte, error) { + resp, err := httpClient.Get(apiURL) if err != nil { return nil, err } diff --git a/modules/twitter/settings.go b/modules/twitter/settings.go index 60734759..3b4bb8ce 100644 --- a/modules/twitter/settings.go +++ b/modules/twitter/settings.go @@ -15,18 +15,22 @@ const ( type Settings struct { common *cfg.Common - bearerToken string - count int - screenNames []interface{} + bearerToken string + consumerKey string + consumerSecret string + count int + screenNames []interface{} } func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *config.Config) *Settings { settings := Settings{ common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig), - bearerToken: ymlConfig.UString("bearerToken", os.Getenv("WTF_TWITTER_BEARER_TOKEN")), - count: ymlConfig.UInt("count", 5), - screenNames: ymlConfig.UList("screenName"), + bearerToken: ymlConfig.UString("bearerToken", os.Getenv("WTF_TWITTER_BEARER_TOKEN")), + consumerKey: ymlConfig.UString("consumerKey", os.Getenv("WTF_TWITTER_CONSUMER_KEY")), + consumerSecret: ymlConfig.UString("consumerSecret", os.Getenv("WTF_TWITTER_CONSUMER_SECRET")), + count: ymlConfig.UInt("count", 5), + screenNames: ymlConfig.UList("screenName"), } return &settings diff --git a/modules/twitterstats/client.go b/modules/twitterstats/client.go index 6a18a09a..0fc67d78 100644 --- a/modules/twitterstats/client.go +++ b/modules/twitterstats/client.go @@ -1,6 +1,7 @@ package twitterstats import ( + "context" "encoding/json" "fmt" "io/ioutil" @@ -15,6 +16,7 @@ import ( type Client struct { httpClient *http.Client screenNames []string + bearerToken string } // TwitterStats Represents a stats snapshot for a single Twitter user at a point in time @@ -37,12 +39,23 @@ func NewClient(settings *Settings) *Client { } } - conf := &clientcredentials.Config{ - ClientID: settings.consumerKey, - ClientSecret: settings.consumerSecret, - TokenURL: "https://api.twitter.com/oauth2/token", + var httpClient *http.Client + // If a bearer token is supplied, use that directly. Otherwise, let the Oauth client fetch a token + // using the consumer key and secret. + if settings.bearerToken == "" { + conf := &clientcredentials.Config{ + ClientID: settings.consumerKey, + ClientSecret: settings.consumerSecret, + TokenURL: "https://api.twitter.com/oauth2/token", + } + httpClient = conf.Client(oauth2.NoContext) + } else { + ctx := context.Background() + httpClient = oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: settings.bearerToken, + TokenType: "Bearer", + })) } - httpClient := conf.Client(oauth2.NoContext) client := Client{ httpClient: httpClient, diff --git a/modules/twitterstats/settings.go b/modules/twitterstats/settings.go index cd2122af..793ae917 100644 --- a/modules/twitterstats/settings.go +++ b/modules/twitterstats/settings.go @@ -15,6 +15,7 @@ const ( type Settings struct { common *cfg.Common + bearerToken string consumerKey string consumerSecret string screenNames []interface{} @@ -24,6 +25,7 @@ func NewSettingsFromYAML(name string, ymlConfig *config.Config, globalConfig *co settings := Settings{ common: cfg.NewCommonSettingsFromModule(name, defaultTitle, defaultFocusable, ymlConfig, globalConfig), + bearerToken: ymlConfig.UString("bearerToken", os.Getenv("WTF_TWITTER_BEARER_TOKEN")), consumerKey: ymlConfig.UString("consumerKey", os.Getenv("WTF_TWITTER_CONSUMER_KEY")), consumerSecret: ymlConfig.UString("consumerSecret", os.Getenv("WTF_TWITTER_CONSUMER_SECRET")),