From 8224dd12030b845614f550a9ac63a6859e5bb237 Mon Sep 17 00:00:00 2001 From: Utku Ufuk Date: Sun, 7 Jun 2020 03:29:01 +0300 Subject: [PATCH] Integrate Google Spreadsheet Habits (#16) * initial PR commit * add boilerplate source & configuration * simplify source config access * don't require sources to save the whole config in a struct * add telegram dependency to readme * reorder dependencies in readme * add a context argument to the source interface method FetchNewCards * refactor main package * implement auth & basic reads from sheets * simplify method signature * fix bug * polish * allow no-description cards in trello package * fully implement FetchNewCards method for habits * add comments * update readme * polish & add some unit tests * get rid of problematic test case --- .gitignore | 1 + README.md | 44 +++- cmd/entrello/main.go | 61 ++++- cmd/entrello/{queue_test.go => main_test.go} | 0 cmd/entrello/queue.go | 63 ----- cmd/entrello/source.go | 81 +++--- cmd/entrello/source_test.go | 51 ++-- config.example.yml | 49 ++-- go.mod | 1 + go.sum | 262 +++++++++++++++++++ internal/config/config.go | 28 +- internal/github/github.go | 41 +-- internal/habits/habits.go | 80 ++++++ internal/habits/habits_test.go | 74 ++++++ internal/habits/service.go | 66 +++++ internal/habits/service_test.go | 33 +++ internal/tododock/api.go | 8 +- internal/tododock/tododock.go | 40 +-- internal/trello/trello.go | 4 - internal/trello/trello_test.go | 7 - 20 files changed, 741 insertions(+), 253 deletions(-) rename cmd/entrello/{queue_test.go => main_test.go} (100%) delete mode 100644 cmd/entrello/queue.go create mode 100644 internal/habits/habits.go create mode 100644 internal/habits/habits_test.go create mode 100644 internal/habits/service.go create mode 100644 internal/habits/service_test.go diff --git a/.gitignore b/.gitignore index 1d3ed4c..9c2a50f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ config.yml +*.json diff --git a/README.md b/README.md index ec7e75c..325edb2 100644 --- a/README.md +++ b/README.md @@ -9,38 +9,52 @@ Run this as a cron job to periodically check custom data sources and automatical An example use case (which is already implemented) could be to create a Trello card for each GitHub issue that's been assigned to you. ## Currently Available Sources - * Github Issues - https://github.com/issues/assigned - * TodoDock Tasks - https://tododock.com +| | | +|:-|:-| +| Assigned Github Issues | https://github.com/issues/assigned | +| TodoDock Tasks | https://tododock.com | +| Daily Habits (Google Spreadsheets) | https://www.google.com/sheets/about/ | Feel free to add new sources or improve the implementations of the existing ones. Contributions are always welcome! ## Configuration -Copy and rename `config.example.yml` as `config.yml`, then set your own values in `config.yml` according to the following: +Copy and rename `config.example.yml` as `config.yml`, then set your own values in `config.yml`. Most of the configuration parameters are self explanatory, so the following only covers some of them: -#### Global Timeout +### Global Timeout You can edit the `timeout_secs` config value in order to update global timeout (in seconds) for a single execution. The execution will not terminate until the timeout is reached, so it's important that the timeout is shorter than the cron job period. -#### Trello +### Trello You need to set your [Trello API key & token](https://trello.com/app-key) in the configuraiton file, as well as the Trello board & list IDs. The given list will be the one that new cards is going to be inserted, and it has to be in the given board. -#### Enabling/Disbling Individual Data Sources +### Telegram +You need a Telegram token & a chat ID in order to enable the integration if you want to receive messages on card updates & possible errors. + +### Data Sources +Every data source must have the following configuration parameters under the `source_config` key: + * `name` + * `enabled` + * `strict` + * `label_id` + * `period` + +#### `enabled` In order to disable a source, just update the `enabled` flag to `false`. There's no need to remove/edit the other parameters for that source. -#### Strict Mode -Strict mode can be enabled for individual data sources by setting the `strict` flag to `true`. When strict mode is enabled, all the existing Trello cards in the board with the label for the corresponding data source will be deleted, unless the card also exists in the fresh data. +#### `strict` +Strict mode, which is recommended for most cases, can be enabled for individual data sources by setting the `strict` flag to `true`. -For instance, strict mode can be used to automatically remove resolved GitHub issues from the board. Every time the source is queried, it will return an up-to-date set of open issues. If the board contains any cards that doesn't exist in that set, they will be automatically deleted. +When strict mode is enabled, all the existing Trello cards in the board with the label for the corresponding data source will be deleted, unless the card also exists in the fresh data. -#### Telegram -Since it's not very practical to manually check a log file for a cron job, entrello has an optional Telegram integration that you can use if you want to receive messages on card updates & possible errors. +For instance, strict mode can be used to automatically remove resolved GitHub issues from the board. Every time the source is queried, it will return an up-to-date set of open issues. If the board contains any cards that doesn't exist in that set, they will be automatically deleted. -You need a Telegram token & a chat ID in order to enable the integration. +#### `label_id` +Each data source must have a distinct Trello label associated with it. -#### Custom Periods +#### `period` You can define a custom query period for each source, by populating the `type` and `interval` fields under the `period` for a source. Example: @@ -83,10 +97,12 @@ Both of the following jobs run every hour and both assume that `config.yml` is l ``` ## 3rd Party Dependencies -| Dependency | Purpose | +| | | |:-|:-| | [adlio/trello](https://github.com/adlio/trello) | Trello API Client | +| [go-telegram-bot-api/telegram-bot-api](https://github.com/go-telegram-bot-api/telegram-bot-api) | Telegram Bot API | | [google/go-cmp](https://github.com/google/go-cmp) | Equality Comparisons in Tests | | [go-github/github](https://github.com/google/go-github) | GitHub API Client | | [golang/oauth2](https://github.com/golang/oauth2) | OAuth 2.0 Client | +| [googleapis/google-api-go-client](https://github.com/googleapis/google-api-go-client) | Google API Client | | [go-yaml/yaml](https://github.com/go-yaml/yaml) | Decoding YAML Configuration | diff --git a/cmd/entrello/main.go b/cmd/entrello/main.go index 4686411..32ae35d 100644 --- a/cmd/entrello/main.go +++ b/cmd/entrello/main.go @@ -6,7 +6,10 @@ import ( "time" "github.com/utkuufuk/entrello/internal/config" + "github.com/utkuufuk/entrello/internal/github" + "github.com/utkuufuk/entrello/internal/habits" "github.com/utkuufuk/entrello/internal/syslog" + "github.com/utkuufuk/entrello/internal/tododock" "github.com/utkuufuk/entrello/internal/trello" ) @@ -14,6 +17,12 @@ var ( logger syslog.Logger ) +type CardQueue struct { + add chan trello.Card + del chan trello.Card + err chan error +} + func main() { // read config params cfg, err := config.ReadConfig("config.yml") @@ -30,7 +39,7 @@ func main() { defer cancel() // get a list of enabled sources and the corresponding labels for each source - sources, labels := getEnabledSourcesAndLabels(ctx, cfg.Sources) + sources, labels := getEnabledSourcesAndLabels(cfg.Sources) if len(sources) == 0 { return } @@ -49,7 +58,55 @@ func main() { // concurrently fetch new cards from sources and start processing cards to be created & deleted q := CardQueue{make(chan trello.Card), make(chan trello.Card), make(chan error)} for _, src := range sources { - go queueActionables(src, client, q) + go src.queueActionables(ctx, client, q) } processActionables(ctx, client, q) } + +// getEnabledSourcesAndLabels returns a slice of enabled sources & their labels as a separate slice +func getEnabledSourcesAndLabels(cfg config.Sources) (sources []source, labels []string) { + arr := []source{ + {cfg.GithubIssues.SourceConfig, github.GetSource(cfg.GithubIssues)}, + {cfg.TodoDock.SourceConfig, tododock.GetSource(cfg.TodoDock)}, + {cfg.Habits.SourceConfig, habits.GetSource(cfg.Habits)}, + } + + now := time.Now() + + for _, src := range arr { + if ok, err := src.shouldQuery(now); !ok { + if err != nil { + logger.Errorf("could not check if '%s' should be queried or not, skipping", src.cfg.Name) + } + continue + } + sources = append(sources, src) + labels = append(labels, src.cfg.Label) + } + return sources, labels +} + +// processActionables listens to the card queue in an infinite loop and creates/deletes Trello cards +// depending on which channel the cards come from. Terminates whenever the global timeout is reached. +func processActionables(ctx context.Context, client trello.Client, q CardQueue) { + for { + select { + case c := <-q.add: + if err := client.CreateCard(c); err != nil { + logger.Errorf("could not create Trello card: %v", err) + break + } + logger.Printf("created new card: %s", c.Name) + case c := <-q.del: + if err := client.ArchiveCard(c); err != nil { + logger.Errorf("could not archive card card: %v", err) + break + } + logger.Printf("archived stale card: %s", c.Name) + case err := <-q.err: + logger.Errorf("%v", err) + case <-ctx.Done(): + return + } + } +} diff --git a/cmd/entrello/queue_test.go b/cmd/entrello/main_test.go similarity index 100% rename from cmd/entrello/queue_test.go rename to cmd/entrello/main_test.go diff --git a/cmd/entrello/queue.go b/cmd/entrello/queue.go deleted file mode 100644 index 9f7d473..0000000 --- a/cmd/entrello/queue.go +++ /dev/null @@ -1,63 +0,0 @@ -package main - -import ( - "context" - "fmt" - - "github.com/utkuufuk/entrello/internal/trello" -) - -type CardQueue struct { - add chan trello.Card - del chan trello.Card - err chan error -} - -// queueActionables fetches new cards from the source, then pushes those to be created and -// to be deleted into the corresponding channels, as well as any errors encountered. -func queueActionables(src source, client trello.Client, q CardQueue) { - cards, err := src.FetchNewCards() - if err != nil { - q.err <- fmt.Errorf("could not fetch cards for source '%s': %v", src.GetName(), err) - return - } - - new, stale := client.CompareWithExisting(cards, src.GetLabel()) - - for _, c := range new { - q.add <- c - } - - if !src.IsStrict() { - return - } - - for _, c := range stale { - q.del <- c - } -} - -// processActionables listens to the card queue in an infinite loop and creates/deletes Trello cards -// depending on which channel the cards come from. Terminates whenever the global timeout is reached. -func processActionables(ctx context.Context, client trello.Client, q CardQueue) { - for { - select { - case c := <-q.add: - if err := client.CreateCard(c); err != nil { - logger.Errorf("could not create Trello card: %v", err) - break - } - logger.Printf("created new card: %s", c.Name) - case c := <-q.del: - if err := client.ArchiveCard(c); err != nil { - logger.Errorf("could not archive card card: %v", err) - break - } - logger.Printf("archived stale card: %s", c.Name) - case err := <-q.err: - logger.Errorf("%v", err) - case <-ctx.Done(): - return - } - } -} diff --git a/cmd/entrello/source.go b/cmd/entrello/source.go index c542fa9..28a85da 100644 --- a/cmd/entrello/source.go +++ b/cmd/entrello/source.go @@ -6,65 +6,28 @@ import ( "time" "github.com/utkuufuk/entrello/internal/config" - "github.com/utkuufuk/entrello/internal/github" - "github.com/utkuufuk/entrello/internal/tododock" "github.com/utkuufuk/entrello/internal/trello" ) -// source defines an interface for a Trello card source -type source interface { - // IsEnabled returns true if the source is enabled. - IsEnabled() bool - - // IsStrict returns true if "strict" mode is enabled for the source - IsStrict() bool - - // GetName returns a human-readable name of the source - GetName() string - - // GetLabel returns the corresponding card label ID for the source - GetLabel() string - - // GetPeriod returns the period in minutes that the source should be checked - GetPeriod() config.Period - - // FetchNewCards returns a list of Trello cards to be inserted into the board from the source - FetchNewCards() ([]trello.Card, error) -} - -// getEnabledSourcesAndLabels returns a list of enabled sources & all relevant label IDs -func getEnabledSourcesAndLabels(ctx context.Context, cfg config.Sources) (sources []source, labels []string) { - arr := []source{ - github.GetSource(ctx, cfg.GithubIssues), - tododock.GetSource(cfg.TodoDock), - } - now := time.Now() - - for _, src := range arr { - if ok, err := shouldQuery(src, now); !ok { - if err != nil { - logger.Errorf("could not check if '%s' should be queried or not, skipping", src.GetName()) - } - continue - } - sources = append(sources, src) - labels = append(labels, src.GetLabel()) +type source struct { + cfg config.SourceConfig + api interface { + FetchNewCards(ctx context.Context, cfg config.SourceConfig) ([]trello.Card, error) } - return sources, labels } -// shouldQuery checks if the given source should be queried at the given time -func shouldQuery(src source, now time.Time) (bool, error) { - if !src.IsEnabled() { +// shouldQuery checks if a the source should be queried at the given time +func (s source) shouldQuery(now time.Time) (bool, error) { + if !s.cfg.Enabled { return false, nil } - interval := src.GetPeriod().Interval + interval := s.cfg.Period.Interval if interval < 0 { return false, fmt.Errorf("period interval must be a positive integer, got: '%d'", interval) } - switch src.GetPeriod().Type { + switch s.cfg.Period.Type { case config.PERIOD_TYPE_DEFAULT: return true, nil case config.PERIOD_TYPE_DAY: @@ -84,5 +47,29 @@ func shouldQuery(src source, now time.Time) (bool, error) { return now.Minute()%interval == 0, nil } - return false, fmt.Errorf("unrecognized source period type: '%s'", src.GetPeriod().Type) + return false, fmt.Errorf("unrecognized source period type: '%s'", s.cfg.Period.Type) +} + +// queueActionables fetches new cards from the source, then pushes those to be created and +// to be deleted into the corresponding channels, as well as any errors encountered. +func (s source) queueActionables(ctx context.Context, client trello.Client, q CardQueue) { + cards, err := s.api.FetchNewCards(ctx, s.cfg) + if err != nil { + q.err <- fmt.Errorf("could not fetch cards for source '%s': %v", s.cfg.Name, err) + return + } + + new, stale := client.CompareWithExisting(cards, s.cfg.Label) + + for _, c := range new { + q.add <- c + } + + if !s.cfg.Strict { + return + } + + for _, c := range stale { + q.del <- c + } } diff --git a/cmd/entrello/source_test.go b/cmd/entrello/source_test.go index c14bcbc..ed3a8d9 100644 --- a/cmd/entrello/source_test.go +++ b/cmd/entrello/source_test.go @@ -1,14 +1,12 @@ package main import ( - "context" "fmt" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/utkuufuk/entrello/internal/config" - "github.com/utkuufuk/entrello/internal/tododock" ) func TestGetEnabledSourcesAndLabels(t *testing.T) { @@ -18,33 +16,33 @@ func TestGetEnabledSourcesAndLabels(t *testing.T) { } tt := []struct { - name string - githubIssues config.GithubIssues - todoDock config.TodoDock - numResults int - labels []string + name string + githubIssuesCfg config.SourceConfig + todoDockCfg config.SourceConfig + numResults int + labels []string }{ { - name: "nothing enabled", - githubIssues: config.GithubIssues{Enabled: false, Period: period}, - todoDock: config.TodoDock{Enabled: false, Period: period}, - numResults: 0, + name: "nothing enabled", + githubIssuesCfg: config.SourceConfig{Enabled: false, Period: period}, + todoDockCfg: config.SourceConfig{Enabled: false, Period: period}, + numResults: 0, }, { name: "only github issues enabled", - githubIssues: config.GithubIssues{ + githubIssuesCfg: config.SourceConfig{ Enabled: true, Period: period, Label: "github-label", }, - todoDock: config.TodoDock{Enabled: false, Period: period}, - numResults: 1, - labels: []string{"github-label"}, + todoDockCfg: config.SourceConfig{Enabled: false, Period: period}, + numResults: 1, + labels: []string{"github-label"}, }, { - name: "only tododock enabled", - githubIssues: config.GithubIssues{Enabled: false, Period: period}, - todoDock: config.TodoDock{ + name: "only tododock enabled", + githubIssuesCfg: config.SourceConfig{Enabled: false, Period: period}, + todoDockCfg: config.SourceConfig{ Enabled: true, Period: period, Label: "tododock-label", @@ -54,12 +52,12 @@ func TestGetEnabledSourcesAndLabels(t *testing.T) { }, { name: "all enabled", - githubIssues: config.GithubIssues{ + githubIssuesCfg: config.SourceConfig{ Enabled: true, Period: period, Label: "github-label", }, - todoDock: config.TodoDock{ + todoDockCfg: config.SourceConfig{ Enabled: true, Period: period, Label: "tododock-label", @@ -69,15 +67,14 @@ func TestGetEnabledSourcesAndLabels(t *testing.T) { }, } - ctx := context.Background() for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { cfg := config.Sources{ - GithubIssues: tc.githubIssues, - TodoDock: tc.todoDock, + GithubIssues: config.GithubIssues{SourceConfig: tc.githubIssuesCfg}, + TodoDock: config.TodoDock{SourceConfig: tc.todoDockCfg}, } - sources, labels := getEnabledSourcesAndLabels(ctx, cfg) + sources, labels := getEnabledSourcesAndLabels(cfg) if len(sources) != tc.numResults { t.Errorf("expected %d source(s); got %v", tc.numResults, len(sources)) } @@ -230,15 +227,15 @@ func TestShouldQuery(t *testing.T) { for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - cfg := config.TodoDock{ + cfg := config.SourceConfig{ Enabled: true, Period: config.Period{ Type: tc.pType, Interval: tc.pInterval, }, } - src := tododock.GetSource(cfg) - ok, err := shouldQuery(src, tc.date) + src := source{cfg, nil} + ok, err := src.shouldQuery(tc.date) if err != nil || tc.err != nil { if err == nil || tc.err == nil || err.Error() != tc.err.Error() { diff --git a/config.example.yml b/config.example.yml index b013db8..99baeb6 100644 --- a/config.example.yml +++ b/config.example.yml @@ -6,27 +6,44 @@ trello: board_id: xxxxxxxxxxxxxxxxxxxxxxxx list_id: xxxxxxxxxxxxxxxxxxxxxxxx +telegram: + enabled: true + token: xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + chat_id: 1234567890 + sources: github_issues: - enabled: true - strict: true - period: - type: minute - interval: 15 personal_access_token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - label_id: xxxxxxxxxxxxxxxxxxxxxxxx + source_config: + name: "Github Issues" + enabled: true + strict: true + label_id: xxxxxxxxxxxxxxxxxxxxxxxx + period: + type: minute + interval: 15 tododock: - enabled: true - strict: false - period: - type: hour - interval: 1 email: abc@def.com password: xxxxxxxx - label_id: xxxxxxxxxxxxxxxxxxxxxxxx + source_config: + name: "TodoDock" + enabled: true + strict: false + label_id: xxxxxxxxxxxxxxxxxxxxxxxx + period: + type: hour + interval: 1 -telegram: - enabled: true - token: xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - chat_id: 1234567890 + habits: + spreadsheet_id: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + credentials_file: credentials.json + token_file: token.json + source_config: + name: "Google Spreadsheet Habits" + enabled: true + strict: true + label_id: xxxxxxxxxxxxxxxxxxxxxxxx + period: + type: hour + interval: 12 diff --git a/go.mod b/go.mod index c99e458..9ea869b 100644 --- a/go.mod +++ b/go.mod @@ -12,5 +12,6 @@ require ( github.com/technoweenie/multipartstreamer v1.0.1 // indirect golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d + google.golang.org/api v0.26.0 gopkg.in/yaml.v2 v2.3.0 ) diff --git a/go.sum b/go.sum index a97268b..6c35565 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,63 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.56.0 h1:WRz29PgAsVEyPSDHyk+0fpEkwEFyfhHn+JbksT6gIL4= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/adlio/trello v1.7.0 h1:syLRJ27wCM8URf7zOBWGr981cG+dpmLSyMqjEoQc+4g= github.com/adlio/trello v1.7.0/go.mod h1:l2068AhUuUuQ9Vsb95ECMueHThYyAj4e85lWPmr2/LE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU= github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -11,6 +65,9 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -20,27 +77,219 @@ github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4r github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.26.0 h1:VJZ8h6E8ip82FRpQl848c5vAadxlTXrUh8RzQzSRm08= +google.golang.org/api v0.26.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940 h1:MRHtG0U6SnaUb+s+LhNE1qt1FQ1wlhqr5E4usBKC0uA= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0 h1:bO/TA4OxCOummhSf10siHuG7vJOiwh7SpRpFZDkOgl4= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -50,5 +299,18 @@ google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyz google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/config/config.go b/internal/config/config.go index b5904e2..b6dbe9f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,26 +19,36 @@ type Period struct { Interval int `yaml:"interval"` } -type GithubIssues struct { +type SourceConfig struct { + Name string `yaml:"name"` Enabled bool `yaml:"enabled"` Strict bool `yaml:"strict"` - Period Period `yaml:"period"` - Token string `yaml:"personal_access_token"` Label string `yaml:"label_id"` + Period Period `yaml:"period"` +} + +type GithubIssues struct { + SourceConfig SourceConfig `yaml:"source_config"` + Token string `yaml:"personal_access_token"` } type TodoDock struct { - Enabled bool `yaml:"enabled"` - Strict bool `yaml:"strict"` - Period Period `yaml:"period"` - Email string `yaml:"email"` - Password string `yaml:"password"` - Label string `yaml:"label_id"` + SourceConfig SourceConfig `yaml:"source_config"` + Email string `yaml:"email"` + Password string `yaml:"password"` +} + +type Habits struct { + SourceConfig SourceConfig `yaml:"source_config"` + SpreadsheetId string `yaml:"spreadsheet_id"` + CredentialsFile string `yaml:"credentials_file"` + TokenFile string `yaml:"token_file"` } type Sources struct { GithubIssues GithubIssues `yaml:"github_issues"` TodoDock TodoDock `yaml:"tododock"` + Habits Habits `yaml:"habits"` } type Trello struct { diff --git a/internal/github/github.go b/internal/github/github.go index f4428b5..cb6b899 100644 --- a/internal/github/github.go +++ b/internal/github/github.go @@ -11,46 +11,24 @@ import ( "golang.org/x/oauth2" ) -type GithubIssuesSource struct { +type source struct { client *github.Client - ctx context.Context - cfg config.GithubIssues } -func GetSource(ctx context.Context, cfg config.GithubIssues) GithubIssuesSource { +func GetSource(cfg config.GithubIssues) source { ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: cfg.Token}) - tc := oauth2.NewClient(ctx, ts) + tc := oauth2.NewClient(context.Background(), ts) client := github.NewClient(tc) - return GithubIssuesSource{client, ctx, cfg} + return source{client} } -func (g GithubIssuesSource) IsEnabled() bool { - return g.cfg.Enabled -} - -func (g GithubIssuesSource) IsStrict() bool { - return g.cfg.Strict -} - -func (g GithubIssuesSource) GetName() string { - return "Github Issues" -} - -func (g GithubIssuesSource) GetLabel() string { - return g.cfg.Label -} - -func (g GithubIssuesSource) GetPeriod() config.Period { - return g.cfg.Period -} - -func (g GithubIssuesSource) FetchNewCards() ([]trello.Card, error) { - issues, _, err := g.client.Issues.List(g.ctx, false, nil) +func (s source) FetchNewCards(ctx context.Context, cfg config.SourceConfig) ([]trello.Card, error) { + issues, _, err := s.client.Issues.List(ctx, false, nil) if err != nil { return nil, fmt.Errorf("could not retrieve issues: %w", err) } - return toCards(issues, g.cfg.Label) + return toCards(issues, cfg.Label) } // toCards converts a list of issues into a list of trello card @@ -63,7 +41,7 @@ func toCards(issues []*github.Issue, label string) ([]trello.Card, error) { c, err := toCard(issue, label) if err != nil { - return nil, fmt.Errorf("could not create card: %w", err) + return nil, fmt.Errorf("could not create github issue card: %w", err) } cards = append(cards, c) } @@ -73,7 +51,8 @@ func toCards(issues []*github.Issue, label string) ([]trello.Card, error) { // toCard converts the given issue into a trello card func toCard(issue *github.Issue, label string) (c trello.Card, err error) { if *issue.Repository.Name == "" || *issue.Title == "" || *issue.URL == "" || label == "" { - return c, fmt.Errorf("could not convert issue to card, title, repo name, url and label cannot be blank") + e := "could not create card from issue; title, repo name, url and label are mandatory" + return c, fmt.Errorf(e) } name := fmt.Sprintf("[%s] %s", *issue.Repository.Name, *issue.Title) url := strings.Replace(*issue.URL, "api.", "", 1) diff --git a/internal/habits/habits.go b/internal/habits/habits.go new file mode 100644 index 0000000..6373e56 --- /dev/null +++ b/internal/habits/habits.go @@ -0,0 +1,80 @@ +package habits + +import ( + "context" + "fmt" + "time" + + "github.com/utkuufuk/entrello/internal/config" + "github.com/utkuufuk/entrello/internal/trello" + "google.golang.org/api/sheets/v4" +) + +type source struct { + spreadsheetId string + credentialsFile string + tokenFile string + service *sheets.SpreadsheetsValuesService +} + +func GetSource(cfg config.Habits) source { + return source{cfg.SpreadsheetId, cfg.CredentialsFile, cfg.TokenFile, nil} +} + +func (s source) FetchNewCards(ctx context.Context, cfg config.SourceConfig) ([]trello.Card, error) { + err := s.initializeService(ctx) + if err != nil { + return nil, fmt.Errorf("could not initialize google spreadsheet service: %w", err) + } + habits, err := s.fetchHabits() + if err != nil { + return nil, fmt.Errorf("could not fetch habits: %w", err) + } + + return toCards(habits, cfg.Label) +} + +// fetchHabits retrieves the state of today's habits from the spreadsheet +func (s source) fetchHabits() (map[string]string, error) { + today := time.Now() + rangeName := fmt.Sprintf("%s %d!B1:Z%d", today.Month().String()[:3], today.Year(), today.Day()+3) + rows, err := s.readCells(rangeName) + if err != nil { + return nil, fmt.Errorf("could not read cells: %w", err) + } + + states := make(map[string]string) + for i := 0; i < len(rows[0]); i++ { + name := fmt.Sprintf("%v", rows[0][i]) + state := fmt.Sprintf("%v", rows[today.Day()+2][i]) + states[name] = state + } + + return states, nil +} + +// readCells reads a range of cell values with the given range +func (s source) readCells(rangeName string) ([][]interface{}, error) { + resp, err := s.service.Get(s.spreadsheetId, rangeName).Do() + if err != nil { + return nil, fmt.Errorf("could not read cells: %w", err) + } + return resp.Values, nil +} + +// toCards returns a slice of trello cards from the given habits which haven't been marked today +func toCards(habits map[string]string, label string) (cards []trello.Card, err error) { + for habit, state := range habits { + if state != "" { + continue + } + + c, err := trello.NewCard(fmt.Sprintf("%v", habit), label, "", nil) + if err != nil { + return nil, fmt.Errorf("could not create habit card: %w", err) + } + + cards = append(cards, c) + } + return cards, nil +} diff --git a/internal/habits/habits_test.go b/internal/habits/habits_test.go new file mode 100644 index 0000000..989e3a9 --- /dev/null +++ b/internal/habits/habits_test.go @@ -0,0 +1,74 @@ +package habits + +import ( + "errors" + "testing" +) + +func TestToCards(t *testing.T) { + str := "test" + + tt := []struct { + name string + label string + habits map[string]string + numCards int + err error + }{ + { + name: "blank habit name", + label: str, + habits: map[string]string{"": ""}, + numCards: 0, + err: errors.New(""), + }, + { + name: "missing label", + label: "", + habits: map[string]string{str: ""}, + numCards: 0, + err: errors.New(""), + }, + { + name: "marked habits", + label: str, + habits: map[string]string{ + "a": "✔", + "b": "x", + "c": "✘", + "d": "–", + "e": "-", + }, + numCards: 0, + err: nil, + }, + { + name: "some marked some unhabits", + label: str, + habits: map[string]string{ + "a": "✔", + "b": "x", + "c": "✘", + "d": "–", + "e": "-", + "f": "", + "g": "", + }, + numCards: 2, + err: nil, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + cards, err := toCards(tc.habits, tc.label) + if same := (err == nil && tc.err == nil) || tc.err != nil && err != nil; !same { + t.Fatalf("want '%v', got '%v'", tc.err, err) + } + + if len(cards) != tc.numCards { + t.Errorf("expected %d cards, got %d", tc.numCards, len(cards)) + } + }) + } +} diff --git a/internal/habits/service.go b/internal/habits/service.go new file mode 100644 index 0000000..f280fc9 --- /dev/null +++ b/internal/habits/service.go @@ -0,0 +1,66 @@ +package habits + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "os" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "google.golang.org/api/sheets/v4" +) + +const ( + SCOPE = "https://www.googleapis.com/auth/spreadsheets" +) + +// initializeService creates and sets a spreadsheet service within the source struct +func (s *source) initializeService(ctx context.Context) error { + cfg, token, err := readCreds(s.credentialsFile, s.tokenFile) + if err != nil { + return fmt.Errorf("failed to get credentials for google spreadsheets: %w", err) + } + + client := cfg.Client(ctx, token) + service, err := sheets.New(client) + if err != nil { + return fmt.Errorf("could not create google spreadsheets client: %w", err) + } + + s.service = service.Spreadsheets.Values + return nil +} + +// readCreds reads and returns credentials from the configured files +func readCreds(credentialsFile, tokenFile string) (*oauth2.Config, *oauth2.Token, error) { + c, err := ioutil.ReadFile(credentialsFile) + if err != nil { + return nil, nil, fmt.Errorf("could not read client credentials file: %w", err) + } + + cfg, err := google.ConfigFromJSON(c, SCOPE) + if err != nil { + return nil, nil, fmt.Errorf("could not parse client secret file: %w", err) + } + + token, err := readToken(tokenFile) + if err != nil { + return nil, nil, fmt.Errorf("could not find auth token: %w", err) + } + return cfg, token, nil +} + +// readToken reads the client auth token from a JSON file +func readToken(tokenPath string) (*oauth2.Token, error) { + f, err := os.Open(tokenPath) + if err != nil { + return nil, err + } + defer f.Close() + + token := &oauth2.Token{} + err = json.NewDecoder(f).Decode(token) + return token, err +} diff --git a/internal/habits/service_test.go b/internal/habits/service_test.go new file mode 100644 index 0000000..d9eb92f --- /dev/null +++ b/internal/habits/service_test.go @@ -0,0 +1,33 @@ +package habits + +import ( + "testing" +) + +func TestReadToken(t *testing.T) { + tt := []struct { + name string + filename string + err bool + }{ + { + name: "invalid json file", + filename: "../../config.example.yml", + err: true, + }, + { + name: "non-existing token file", + filename: "./no-way-this-file-exists.json", + err: true, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + _, err := readToken(tc.filename) + if (err != nil && !tc.err) || err == nil && tc.err { + t.Fatalf("did not expect to get '%v' error", err) + } + }) + } +} diff --git a/internal/tododock/api.go b/internal/tododock/api.go index eb73582..27c42e7 100644 --- a/internal/tododock/api.go +++ b/internal/tododock/api.go @@ -19,10 +19,10 @@ type fetchTasksResponse struct { // login logs-in to TodoDock with the configured user's credentials, // and returns the user ID and JWT obtained from the HTTP response -func (t TodoDockSource) login() (id int, jwt string, err error) { +func (s source) login() (id int, jwt string, err error) { req, err := json.Marshal(map[string]string{ - "email": t.cfg.Email, - "password": t.cfg.Password, + "email": s.email, + "password": s.password, }) if err != nil { return -1, "", fmt.Errorf("could not build TodoDock login request body: %w", err) @@ -41,7 +41,7 @@ func (t TodoDockSource) login() (id int, jwt string, err error) { } // fetchTasks retrieves all TodoDock tasks owned by the logged-in user with the given ID -func (t TodoDockSource) fetchTasks(id int, token string) (tasks []task, err error) { +func (s source) fetchTasks(id int, token string) (tasks []task, err error) { // build GET request with auth header url := fmt.Sprintf("%s/tasks/%d", BASE_URL, id) req, err := http.NewRequest("GET", url, nil) diff --git a/internal/tododock/tododock.go b/internal/tododock/tododock.go index 1bb0f77..c200907 100644 --- a/internal/tododock/tododock.go +++ b/internal/tododock/tododock.go @@ -1,6 +1,7 @@ package tododock import ( + "context" "fmt" "time" @@ -12,8 +13,9 @@ const ( BASE_URL = "https://tododock.com/api" ) -type TodoDockSource struct { - cfg config.TodoDock +type source struct { + email string + password string } // task represents the TodoDock task model @@ -28,37 +30,17 @@ type task struct { MuteEmails int `json:"mute_reminder_emails"` } -func GetSource(cfg config.TodoDock) TodoDockSource { - return TodoDockSource{cfg} +func GetSource(cfg config.TodoDock) source { + return source{cfg.Email, cfg.Password} } -func (t TodoDockSource) IsEnabled() bool { - return t.cfg.Enabled -} - -func (t TodoDockSource) IsStrict() bool { - return t.cfg.Strict -} - -func (t TodoDockSource) GetName() string { - return "TodoDock" -} - -func (t TodoDockSource) GetLabel() string { - return t.cfg.Label -} - -func (t TodoDockSource) GetPeriod() config.Period { - return t.cfg.Period -} - -func (t TodoDockSource) FetchNewCards() (cards []trello.Card, err error) { - id, token, err := t.login() +func (s source) FetchNewCards(ctx context.Context, cfg config.SourceConfig) (cards []trello.Card, err error) { + id, token, err := s.login() if err != nil { - return cards, nil + return cards, fmt.Errorf("failed to authenticate with TodoDock: %w", err) } - tasks, err := t.fetchTasks(id, token) - return toCards(tasks, t.cfg.Label) + tasks, err := s.fetchTasks(id, token) + return toCards(tasks, cfg.Label) } // toCards cherry-picks the 'active' and 'due' tasks from a list of tasks, diff --git a/internal/trello/trello.go b/internal/trello/trello.go index c503579..e355b21 100644 --- a/internal/trello/trello.go +++ b/internal/trello/trello.go @@ -41,10 +41,6 @@ func NewCard(name, label, description string, dueDate *time.Time) (card Card, er return card, fmt.Errorf("label ID cannot be blank") } - if description == "" { - return card, fmt.Errorf("description cannot be blank") - } - return &trello.Card{ Name: name, Desc: description, diff --git a/internal/trello/trello_test.go b/internal/trello/trello_test.go index 4226812..7723f6c 100644 --- a/internal/trello/trello_test.go +++ b/internal/trello/trello_test.go @@ -31,13 +31,6 @@ func TestNewCard(t *testing.T) { cDesc: "desc", err: fmt.Errorf("card name cannot be blank"), }, - { - name: "missing description", - cName: "name", - cLabel: "label", - cDesc: "", - err: fmt.Errorf("description cannot be blank"), - }, { name: "missing label ID", cName: "name",