├── .vscode └── settings.json ├── configs ├── app.go ├── configs.go └── configs_test.go ├── main.go ├── .gitignore ├── scripts ├── test.sh └── mock.sh ├── .editorconfig ├── cmd └── dingtalk │ ├── version_test.go │ ├── version.go │ ├── text.go │ ├── markdown.go │ ├── link.go │ ├── text_test.go │ ├── feedCard.go │ ├── markdown_test.go │ ├── root.go │ ├── feedCard_test.go │ ├── link_test.go │ ├── root_test.go │ ├── actionCard.go │ └── actionCard_test.go ├── .github └── workflows │ └── go.yml ├── pkg └── dingtalk │ ├── message.go │ ├── text.go │ ├── link.go │ ├── markdown.go │ ├── link_test.go │ ├── feedCard.go │ ├── feedCard_test.go │ ├── text_test.go │ ├── markdown_test.go │ ├── client.go │ ├── actionCard.go │ ├── actionCard_test.go │ └── client_test.go ├── sonar-project.properties ├── CHANGELOG.md ├── LICENSE ├── go.mod ├── test └── mocks │ └── message │ └── message.go ├── internal └── security │ ├── security.go │ └── security_test.go ├── Makefile ├── README.md ├── READMEEN.md └── go.sum /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["btns", "dingtalk", "gomock", "mitchellh", "OAPI", "Unpatch"] 3 | } 4 | -------------------------------------------------------------------------------- /configs/app.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | const ( 4 | // AccessToken access token key 5 | AccessToken string = "access_token" 6 | // Secret secret key 7 | Secret string = "secret" 8 | ) 9 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/CatchZeng/dingtalk/cmd/dingtalk" 8 | ) 9 | 10 | func main() { 11 | log.SetOutput(os.Stdout) 12 | log.SetFlags(0) 13 | dingtalk.Execute() 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dingtalk-darwin-amd64.zip 2 | dingtalk-darwin-arm64.zip 3 | dingtalk-linux-amd64.zip 4 | dingtalk-windows-386.zip 5 | dingtalk-windows-amd64.zip 6 | dingtalk.exe 7 | .idea 8 | coverage.txt 9 | coverage.data 10 | .scannerwork 11 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | shell_dir=$(dirname $0) 3 | cd ${shell_dir} 4 | 5 | cd .. 6 | 7 | set -e 8 | echo "" > coverage.txt 9 | 10 | for d in $(go list ./... | grep -v vendor); do 11 | go test -gcflags=-l -race -coverprofile=profile.out -covermode=atomic $d 12 | if [ -f profile.out ]; then 13 | cat profile.out >> coverage.txt 14 | rm profile.out 15 | fi 16 | done 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.go] 12 | indent_style = tab 13 | indent_size = 2 14 | 15 | [{Makefile, Dockerfile}] 16 | indent_style = tab 17 | indent_size = 4 18 | 19 | [*.{yml, yaml, json}] 20 | indent_style = space 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /scripts/mock.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | shell_dir=$(dirname $0) 3 | cd ${shell_dir} 4 | 5 | cd .. 6 | 7 | rm -rf test/mocks 8 | mkdir -p test/mocks/message 9 | 10 | # open with vscode 11 | if which mockgen >/dev/null; then 12 | echo "mockgen has installed in PATH" 13 | else 14 | echo "warning: 'mockgen' command has not installed in PATH" 15 | GO111MODULE=on go get github.com/golang/mock/mockgen@v1.4.3 16 | fi 17 | 18 | mockgen -package=mock_message -source=message.go > test/mocks/message/message.go -------------------------------------------------------------------------------- /cmd/dingtalk/version_test.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | v "github.com/CatchZeng/gutils/version" 11 | 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | func Test_runVersionCmd(t *testing.T) { 16 | var buf bytes.Buffer 17 | log.SetOutput(&buf) 18 | defer func() { 19 | log.SetOutput(os.Stderr) 20 | }() 21 | runVersionCmd(&cobra.Command{}, []string{}) 22 | got := buf.String() 23 | 24 | want := v.Stringify(version, buildTime) 25 | 26 | if !strings.Contains(got, want) { 27 | t.Errorf("runVersionCmd() = %v, want %v", got, want) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /cmd/dingtalk/version.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "log" 5 | 6 | v "github.com/CatchZeng/gutils/version" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | const ( 11 | version = "1.5.0" 12 | buildTime = "2022/04/20" 13 | ) 14 | 15 | // versionCmd represents the version command 16 | var versionCmd = &cobra.Command{ 17 | Use: "version", 18 | Short: "dingtalk version", 19 | Long: `dingtalk version`, 20 | Run: runVersionCmd, 21 | } 22 | 23 | func runVersionCmd(_ *cobra.Command, _ []string) { 24 | v := v.Stringify(version, buildTime) 25 | log.Println(v) 26 | } 27 | 28 | func init() { 29 | rootCmd.AddCommand(versionCmd) 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.16 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Get dependencies 26 | run: | 27 | make mod 28 | 29 | - name: Test 30 | run: make test 31 | 32 | - name: codecoe 33 | run: bash <(curl -s https://codecov.io/bash) 34 | -------------------------------------------------------------------------------- /pkg/dingtalk/message.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | // MsgType message type enum 4 | type MsgType string 5 | 6 | const ( 7 | // MsgTypeText text 8 | MsgTypeText MsgType = "text" 9 | // MsgTypeMarkdown markdown 10 | MsgTypeMarkdown MsgType = "markdown" 11 | // MsgTypeLink link 12 | MsgTypeLink MsgType = "link" 13 | // MsgTypeActionCard actionCard 14 | MsgTypeActionCard MsgType = "actionCard" 15 | // MsgTypeFeedCard feedCard 16 | MsgTypeFeedCard MsgType = "feedCard" 17 | ) 18 | 19 | // Message interface 20 | type Message interface { 21 | ToByte() ([]byte, error) 22 | } 23 | 24 | // At at struct 25 | type At struct { 26 | AtMobiles []string `json:"atMobiles"` 27 | IsAtAll bool `json:"isAtAll"` 28 | } 29 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | # must be unique in a given SonarQube instance 2 | sonar.projectKey=dingtalk 3 | 4 | # --- optional properties --- 5 | 6 | # defaults to project key 7 | #sonar.projectName=My project 8 | # defaults to 'not provided' 9 | sonar.projectVersion=1.0 10 | 11 | # Path is relative to the sonar-project.properties file. Defaults to . 12 | sonar.sources=. 13 | 14 | # Encoding of the source code. Default is default system encoding 15 | sonar.sourceEncoding=UTF-8 16 | 17 | sonar.exclusions=**/proto/** 18 | sonar.language=go 19 | sonar.tests=. 20 | sonar.test.inclusions=**/*_test.go 21 | sonar.test.exclusions=**/vendor/**,**/proto/** 22 | sonar.go.coverage.reportPaths=coverage.data 23 | Dsonar.coverage.dtdVerification=false 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v1.5.0 4 | 5 | ### Added 6 | 7 | - Support the environment variable prefix 8 | 9 | ## v1.4.0 10 | 11 | ### Added 12 | 13 | - Support environment variables 14 | 15 | ### Refactor 16 | 17 | - update to golang 1.18.1 18 | 19 | ## v1.2.0 20 | 21 | ### Refactor 22 | 23 | - project layout 24 | 25 | ### Added 26 | 27 | - support config.xml 28 | - validate function 29 | - .editconfig file 30 | 31 | ## v1.1.1 32 | 33 | ### Fixed 34 | 35 | - Fix release response body resource, see more at . 36 | - Fix bug about timestamp, see more at . 37 | 38 | ## v1.1.0 39 | 40 | ### Added 41 | 42 | - actionCard 43 | - feedCard 44 | 45 | ## v1.0.0 46 | 47 | ### Added 48 | 49 | - text 50 | - link 51 | - markdown 52 | -------------------------------------------------------------------------------- /pkg/dingtalk/text.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import "encoding/json" 4 | 5 | // TextMessage text message struct 6 | type TextMessage struct { 7 | MsgType MsgType `json:"msgtype"` 8 | Text Text `json:"text"` 9 | At At `json:"at"` 10 | } 11 | 12 | // Text text struct 13 | type Text struct { 14 | Content string `json:"content"` 15 | } 16 | 17 | // ToByte to byte 18 | func (m *TextMessage) ToByte() ([]byte, error) { 19 | m.MsgType = MsgTypeText 20 | jsonByte, err := json.Marshal(m) 21 | return jsonByte, err 22 | } 23 | 24 | // NewTextMessage new message 25 | func NewTextMessage() *TextMessage { 26 | msg := TextMessage{} 27 | return &msg 28 | } 29 | 30 | // SetContent set content 31 | func (m *TextMessage) SetContent(content string) *TextMessage { 32 | m.Text = Text{ 33 | Content: content, 34 | } 35 | return m 36 | } 37 | 38 | // SetAt set at 39 | func (m *TextMessage) SetAt(atMobiles []string, isAtAll bool) *TextMessage { 40 | m.At = At{ 41 | AtMobiles: atMobiles, 42 | IsAtAll: isAtAll, 43 | } 44 | return m 45 | } 46 | -------------------------------------------------------------------------------- /pkg/dingtalk/link.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import "encoding/json" 4 | 5 | // LinkMessage link message struct 6 | type LinkMessage struct { 7 | MsgType MsgType `json:"msgtype"` 8 | Link Link `json:"link"` 9 | } 10 | 11 | // Link link struct 12 | type Link struct { 13 | Title string `json:"title"` 14 | Text string `json:"text"` 15 | PicURL string `json:"picUrl"` 16 | MessageURL string `json:"messageUrl"` 17 | } 18 | 19 | // ToByte to byte 20 | func (m *LinkMessage) ToByte() ([]byte, error) { 21 | m.MsgType = MsgTypeLink 22 | jsonByte, err := json.Marshal(m) 23 | return jsonByte, err 24 | } 25 | 26 | // NewLinkMessage new message 27 | func NewLinkMessage() *LinkMessage { 28 | msg := LinkMessage{} 29 | return &msg 30 | } 31 | 32 | // SetLink set link 33 | func (m *LinkMessage) SetLink( 34 | title string, 35 | text string, 36 | picURL string, 37 | messageURL string) *LinkMessage { 38 | m.Link = Link{ 39 | Title: title, 40 | Text: text, 41 | PicURL: picURL, 42 | MessageURL: messageURL, 43 | } 44 | return m 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Catch Zeng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /cmd/dingtalk/text.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/CatchZeng/dingtalk/pkg/dingtalk" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var textCmd = &cobra.Command{ 11 | Use: "text", 12 | Short: "send text message with DingTalk robot", 13 | Long: `send text message with DingTalk robot`, 14 | Args: cobra.MinimumNArgs(0), 15 | Run: runTextCmd, 16 | } 17 | 18 | func runTextCmd(_ *cobra.Command, _ []string) { 19 | if len(textVars.content) < 1 { 20 | log.Fatal("content can not be empty") 21 | return 22 | } 23 | 24 | client, err := newClient() 25 | if err != nil { 26 | log.Fatal(err.Error()) 27 | return 28 | } 29 | 30 | msg := dingtalk.NewTextMessage(). 31 | SetContent(textVars.content). 32 | SetAt(atMobiles, isAtAll) 33 | req, _, err := client.Send(msg) 34 | if debug { 35 | log.Print(req) 36 | } 37 | if err != nil { 38 | log.Fatal(err.Error()) 39 | } 40 | } 41 | 42 | // TextVars struct 43 | type TextVars struct { 44 | content string 45 | } 46 | 47 | var textVars TextVars 48 | 49 | func init() { 50 | rootCmd.AddCommand(textCmd) 51 | textCmd.Flags().StringVarP(&textVars.content, "content", "c", "", "content") 52 | } 53 | -------------------------------------------------------------------------------- /pkg/dingtalk/markdown.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import "encoding/json" 4 | 5 | // MarkdownMessage markdown message struct 6 | type MarkdownMessage struct { 7 | MsgType MsgType `json:"msgtype"` 8 | Markdown Markdown `json:"markdown"` 9 | At At `json:"at"` 10 | } 11 | 12 | // Markdown markdown struct 13 | type Markdown struct { 14 | Title string `json:"title"` 15 | Text string `json:"text"` 16 | } 17 | 18 | // ToByte to byte 19 | func (m *MarkdownMessage) ToByte() ([]byte, error) { 20 | m.MsgType = MsgTypeMarkdown 21 | jsonByte, err := json.Marshal(m) 22 | return jsonByte, err 23 | } 24 | 25 | // NewMarkdownMessage new message 26 | func NewMarkdownMessage() *MarkdownMessage { 27 | msg := MarkdownMessage{} 28 | return &msg 29 | } 30 | 31 | // SetMarkdown set markdown 32 | func (m *MarkdownMessage) SetMarkdown(title string, text string) *MarkdownMessage { 33 | m.Markdown = Markdown{ 34 | Title: title, 35 | Text: text, 36 | } 37 | return m 38 | } 39 | 40 | // SetAt set at 41 | func (m *MarkdownMessage) SetAt(atMobiles []string, isAtAll bool) *MarkdownMessage { 42 | m.At = At{ 43 | AtMobiles: atMobiles, 44 | IsAtAll: isAtAll, 45 | } 46 | return m 47 | } 48 | -------------------------------------------------------------------------------- /pkg/dingtalk/link_test.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestLinkMessage_ToByte(t *testing.T) { 9 | msg := NewLinkMessage() 10 | _, _ = msg.ToByte() 11 | if msg.MsgType != MsgTypeLink { 12 | t.Errorf("LinkMessage.ToByte() type error") 13 | } 14 | } 15 | 16 | func TestNewLinkMessage(t *testing.T) { 17 | tests := []struct { 18 | name string 19 | want *LinkMessage 20 | }{ 21 | { 22 | name: "Should return a LinkMessage instance", 23 | want: &LinkMessage{}, 24 | }, 25 | } 26 | for _, tt := range tests { 27 | t.Run(tt.name, func(t *testing.T) { 28 | if got := NewLinkMessage(); !reflect.DeepEqual(got, tt.want) { 29 | t.Errorf("NewLinkMessage() = %v, want %v", got, tt.want) 30 | } 31 | }) 32 | } 33 | } 34 | 35 | func TestLinkMessage_SetLink(t *testing.T) { 36 | got := NewLinkMessage() 37 | got.SetLink("title", "text", "picURL", "messageURL") 38 | 39 | want := NewLinkMessage() 40 | want.Link = Link{ 41 | Title: "title", 42 | Text: "text", 43 | PicURL: "picURL", 44 | MessageURL: "messageURL", 45 | } 46 | 47 | if !reflect.DeepEqual(got, want) { 48 | t.Errorf("SetLink() = %v, want %v", got, want) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/CatchZeng/dingtalk 2 | 3 | go 1.18 4 | 5 | require ( 6 | bou.ke/monkey v1.0.2 7 | github.com/CatchZeng/gutils v0.1.4 8 | github.com/golang/mock v1.4.4 9 | github.com/mitchellh/go-homedir v1.1.0 10 | github.com/spf13/cobra v1.4.0 11 | github.com/spf13/viper v1.11.0 12 | ) 13 | 14 | require ( 15 | github.com/fsnotify/fsnotify v1.5.1 // indirect 16 | github.com/hashicorp/hcl v1.0.0 // indirect 17 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 18 | github.com/magiconair/properties v1.8.6 // indirect 19 | github.com/mitchellh/mapstructure v1.4.3 // indirect 20 | github.com/pelletier/go-toml v1.9.4 // indirect 21 | github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect 22 | github.com/spf13/afero v1.8.2 // indirect 23 | github.com/spf13/cast v1.4.1 // indirect 24 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 25 | github.com/spf13/pflag v1.0.5 // indirect 26 | github.com/subosito/gotenv v1.2.0 // indirect 27 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect 28 | golang.org/x/text v0.3.7 // indirect 29 | gopkg.in/ini.v1 v1.66.4 // indirect 30 | gopkg.in/yaml.v2 v2.4.0 // indirect 31 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /pkg/dingtalk/feedCard.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import "encoding/json" 4 | 5 | // FeedCardMessage feed message struct 6 | type FeedCardMessage struct { 7 | MsgType MsgType `json:"msgtype"` 8 | FeedCard FeedCard `json:"feedCard"` 9 | } 10 | 11 | // FeedCard feedCard struct 12 | type FeedCard struct { 13 | Links []FeedCardLink `json:"links"` 14 | } 15 | 16 | // FeedCardLink struct 17 | type FeedCardLink struct { 18 | Title string `json:"title"` 19 | PicURL string `json:"picURL"` 20 | MessageURL string `json:"messageURL"` 21 | } 22 | 23 | // ToByte to byte 24 | func (m *FeedCardMessage) ToByte() ([]byte, error) { 25 | m.MsgType = MsgTypeFeedCard 26 | jsonByte, err := json.Marshal(m) 27 | return jsonByte, err 28 | } 29 | 30 | // NewFeedCardMessage new message 31 | func NewFeedCardMessage() *FeedCardMessage { 32 | msg := FeedCardMessage{} 33 | return &msg 34 | } 35 | 36 | // AppendLink append link 37 | func (m *FeedCardMessage) AppendLink( 38 | title string, 39 | messageURL string, 40 | picURL string) *FeedCardMessage { 41 | var link = FeedCardLink{ 42 | Title: title, 43 | MessageURL: messageURL, 44 | PicURL: picURL, 45 | } 46 | m.FeedCard.Links = append(m.FeedCard.Links, link) 47 | return m 48 | } 49 | -------------------------------------------------------------------------------- /pkg/dingtalk/feedCard_test.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestFeedCardMessage_ToByte(t *testing.T) { 9 | msg := NewFeedCardMessage() 10 | _, _ = msg.ToByte() 11 | if msg.MsgType != MsgTypeFeedCard { 12 | t.Errorf("FeedCardMessage.ToByte() type error") 13 | } 14 | } 15 | 16 | func TestNewFeedCardMessage(t *testing.T) { 17 | tests := []struct { 18 | name string 19 | want *FeedCardMessage 20 | }{ 21 | { 22 | name: "Should return a FeedCardMessage instance", 23 | want: &FeedCardMessage{}, 24 | }, 25 | } 26 | for _, tt := range tests { 27 | t.Run(tt.name, func(t *testing.T) { 28 | if got := NewFeedCardMessage(); !reflect.DeepEqual(got, tt.want) { 29 | t.Errorf("NewFeedCardMessage() = %v, want %v", got, tt.want) 30 | } 31 | }) 32 | } 33 | } 34 | 35 | func TestFeedCardMessage_AppendLink(t *testing.T) { 36 | msg := NewFeedCardMessage() 37 | msg.AppendLink("title", "messageURL", "picURL") 38 | if len(msg.FeedCard.Links) != 1 { 39 | t.Errorf("The number of links after AppendLink should be 1") 40 | } 41 | 42 | msg.AppendLink("title2", "messageURL2", "picURL2") 43 | if len(msg.FeedCard.Links) != 2 { 44 | t.Errorf("The number of links after AppendLink should be 2") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cmd/dingtalk/markdown.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/CatchZeng/dingtalk/pkg/dingtalk" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var markdownCmd = &cobra.Command{ 11 | Use: "markdown", 12 | Short: "send markdown message with DingTalk robot", 13 | Long: `send markdown message with DingTalk robot`, 14 | Args: cobra.MinimumNArgs(0), 15 | Run: runMarkdownCmd, 16 | } 17 | 18 | func runMarkdownCmd(_ *cobra.Command, args []string) { 19 | if len(markdownVars.title) < 1 { 20 | log.Fatal("title can not be empty") 21 | return 22 | } 23 | 24 | if len(markdownVars.text) < 1 { 25 | log.Fatal("text can not be empty") 26 | return 27 | } 28 | 29 | client, err := newClient() 30 | if err != nil { 31 | log.Fatal(err.Error()) 32 | return 33 | } 34 | 35 | msg := dingtalk.NewMarkdownMessage(). 36 | SetMarkdown(markdownVars.title, markdownVars.text). 37 | SetAt(atMobiles, isAtAll) 38 | req, _, err := client.Send(msg) 39 | if debug { 40 | log.Print(req) 41 | } 42 | if err != nil { 43 | log.Fatal(err.Error()) 44 | } 45 | } 46 | 47 | // MarkdownVars struct 48 | type MarkdownVars struct { 49 | title string 50 | text string 51 | } 52 | 53 | var markdownVars MarkdownVars 54 | 55 | func init() { 56 | rootCmd.AddCommand(markdownCmd) 57 | markdownCmd.Flags().StringVarP(&markdownVars.title, "title", "i", "", "title") 58 | markdownCmd.Flags().StringVarP(&markdownVars.text, "text", "e", "", "text") 59 | } 60 | -------------------------------------------------------------------------------- /configs/configs.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "path" 7 | "strings" 8 | 9 | "github.com/mitchellh/go-homedir" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | // InitConfig reads in configs file and ENV variables if set. 14 | func InitConfig() { 15 | // Find home directory. 16 | home, err := homedir.Dir() 17 | if err != nil { 18 | log.Panic(err) 19 | } 20 | 21 | // Search configs in home directory with name ".dingtalk" (without extension). 22 | configPath := path.Join(home, ".dingtalk") 23 | viper.AddConfigPath(configPath) 24 | viper.SetConfigName("config") 25 | 26 | envPrefix := os.Getenv("DINGTALK_ENV_PREFIX") 27 | viper.SetEnvPrefix(envPrefix) 28 | viper.AutomaticEnv() // read in environment variables that match 29 | 30 | // If a configs file is found, read it in. 31 | if err := viper.ReadInConfig(); err != nil { 32 | return 33 | } 34 | log.Println("using configs file:", viper.ConfigFileUsed()) 35 | } 36 | 37 | // GetConfig get configs with key 38 | func GetConfig(key string) (string, error) { 39 | // Check the environment variable 40 | envPrefix := os.Getenv("DINGTALK_ENV_PREFIX") 41 | envKey := envPrefix + strings.ToUpper(key) 42 | result := os.Getenv(envKey) 43 | 44 | if result != "" { 45 | return result, nil 46 | } 47 | 48 | // If a configs file is found, read it in. 49 | err := viper.ReadInConfig() 50 | if err == nil { 51 | return viper.GetString(key), nil 52 | } 53 | return "", err 54 | } 55 | -------------------------------------------------------------------------------- /pkg/dingtalk/text_test.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestTextMessage_ToByte(t *testing.T) { 9 | msg := NewTextMessage() 10 | _, _ = msg.ToByte() 11 | if msg.MsgType != MsgTypeText { 12 | t.Errorf("TextMessage.ToByte() type error") 13 | } 14 | } 15 | 16 | func TestNewTextMessage(t *testing.T) { 17 | tests := []struct { 18 | name string 19 | want *TextMessage 20 | }{ 21 | { 22 | name: "Should return a TextMessage instance", 23 | want: &TextMessage{}, 24 | }, 25 | } 26 | for _, tt := range tests { 27 | t.Run(tt.name, func(t *testing.T) { 28 | if got := NewTextMessage(); !reflect.DeepEqual(got, tt.want) { 29 | t.Errorf("NewTextMessage() = %v, want %v", got, tt.want) 30 | } 31 | }) 32 | } 33 | } 34 | 35 | func TestTextMessage_SetContent(t *testing.T) { 36 | got := NewTextMessage() 37 | got.SetContent("content") 38 | 39 | want := NewTextMessage() 40 | want.Text = Text{ 41 | Content: "content", 42 | } 43 | 44 | if !reflect.DeepEqual(got, want) { 45 | t.Errorf("SetContent() = %v, want %v", got, want) 46 | } 47 | } 48 | 49 | func TestTextMessage_SetAt(t *testing.T) { 50 | got := NewTextMessage() 51 | got.SetAt([]string{"atMobiles"}, false) 52 | 53 | want := NewTextMessage() 54 | want.At = At{ 55 | AtMobiles: []string{"atMobiles"}, 56 | IsAtAll: false, 57 | } 58 | 59 | if !reflect.DeepEqual(got, want) { 60 | t.Errorf("SetMarkdown() = %v, want %v", got, want) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/mocks/message/message.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: message.go 3 | 4 | // Package mock_message is a generated GoMock package. 5 | package mock_message 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | ) 12 | 13 | // MockMessage is a mock of Message interface 14 | type MockMessage struct { 15 | ctrl *gomock.Controller 16 | recorder *MockMessageMockRecorder 17 | } 18 | 19 | // MockMessageMockRecorder is the mock recorder for MockMessage 20 | type MockMessageMockRecorder struct { 21 | mock *MockMessage 22 | } 23 | 24 | // NewMockMessage creates a new mock instance 25 | func NewMockMessage(ctrl *gomock.Controller) *MockMessage { 26 | mock := &MockMessage{ctrl: ctrl} 27 | mock.recorder = &MockMessageMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockMessage) EXPECT() *MockMessageMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // ToByte mocks base method 37 | func (m *MockMessage) ToByte() ([]byte, error) { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "ToByte") 40 | ret0, _ := ret[0].([]byte) 41 | ret1, _ := ret[1].(error) 42 | return ret0, ret1 43 | } 44 | 45 | // ToByte indicates an expected call of ToByte 46 | func (mr *MockMessageMockRecorder) ToByte() *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToByte", reflect.TypeOf((*MockMessage)(nil).ToByte)) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/dingtalk/markdown_test.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestMarkdownMessage_ToByte(t *testing.T) { 9 | msg := NewMarkdownMessage() 10 | _, _ = msg.ToByte() 11 | if msg.MsgType != MsgTypeMarkdown { 12 | t.Errorf("MarkdownMessage.ToByte() type error") 13 | } 14 | } 15 | 16 | func TestNewMarkdownMessage(t *testing.T) { 17 | tests := []struct { 18 | name string 19 | want *MarkdownMessage 20 | }{ 21 | { 22 | name: "Should return a MarkdownMessage instance", 23 | want: &MarkdownMessage{}, 24 | }, 25 | } 26 | for _, tt := range tests { 27 | t.Run(tt.name, func(t *testing.T) { 28 | if got := NewMarkdownMessage(); !reflect.DeepEqual(got, tt.want) { 29 | t.Errorf("NewMarkdownMessage() = %v, want %v", got, tt.want) 30 | } 31 | }) 32 | } 33 | } 34 | 35 | func TestMarkdownMessage_SetMarkdown(t *testing.T) { 36 | got := NewMarkdownMessage() 37 | got.SetMarkdown("title", "text") 38 | 39 | want := NewMarkdownMessage() 40 | want.Markdown = Markdown{ 41 | Title: "title", 42 | Text: "text", 43 | } 44 | 45 | if !reflect.DeepEqual(got, want) { 46 | t.Errorf("SetMarkdown() = %v, want %v", got, want) 47 | } 48 | } 49 | 50 | func TestMarkdownMessage_SetAt(t *testing.T) { 51 | got := NewMarkdownMessage() 52 | got.SetAt([]string{"atMobiles"}, false) 53 | 54 | want := NewMarkdownMessage() 55 | want.At = At{ 56 | AtMobiles: []string{"atMobiles"}, 57 | IsAtAll: false, 58 | } 59 | 60 | if !reflect.DeepEqual(got, want) { 61 | t.Errorf("SetMarkdown() = %v, want %v", got, want) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /cmd/dingtalk/link.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/CatchZeng/dingtalk/pkg/dingtalk" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var linkCmd = &cobra.Command{ 11 | Use: "link", 12 | Short: "send link message with DingTalk robot", 13 | Long: `send link message with DingTalk robot`, 14 | Args: cobra.MinimumNArgs(0), 15 | Run: runLinkCmd, 16 | } 17 | 18 | func runLinkCmd(_ *cobra.Command, args []string) { 19 | if len(linkVars.title) < 1 { 20 | log.Fatal("title can not be empty") 21 | return 22 | } 23 | 24 | if len(linkVars.text) < 1 { 25 | log.Fatal("text can not be empty") 26 | return 27 | } 28 | 29 | if len(linkVars.messageURL) < 1 { 30 | log.Fatal("messageURL can not be empty") 31 | return 32 | } 33 | 34 | client, err := newClient() 35 | if err != nil { 36 | log.Fatal(err.Error()) 37 | return 38 | } 39 | 40 | msg := dingtalk.NewLinkMessage(). 41 | SetLink(linkVars.title, linkVars.text, linkVars.picURL, linkVars.messageURL) 42 | req, _, err := client.Send(msg) 43 | if debug { 44 | log.Print(req) 45 | } 46 | if err != nil { 47 | log.Fatal(err.Error()) 48 | } 49 | } 50 | 51 | // LinkVars struct 52 | type LinkVars struct { 53 | title string 54 | text string 55 | picURL string 56 | messageURL string 57 | } 58 | 59 | var linkVars LinkVars 60 | 61 | func init() { 62 | rootCmd.AddCommand(linkCmd) 63 | linkCmd.Flags().StringVarP(&linkVars.title, "title", "i", "", "title") 64 | linkCmd.Flags().StringVarP(&linkVars.text, "text", "e", "", "text") 65 | linkCmd.Flags().StringVarP(&linkVars.picURL, "picURL", "p", "", "picURL") 66 | linkCmd.Flags().StringVarP(&linkVars.messageURL, "messageURL", "u", "", "messageURL") 67 | } 68 | -------------------------------------------------------------------------------- /cmd/dingtalk/text_test.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "log" 7 | "os" 8 | "strings" 9 | "testing" 10 | 11 | "bou.ke/monkey" 12 | "github.com/CatchZeng/dingtalk/pkg/dingtalk" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func Test_runTextCmd(t *testing.T) { 17 | fakeExit := func(int) { 18 | log.Print("fake exit") 19 | } 20 | patch := monkey.Patch(os.Exit, fakeExit) 21 | defer patch.Unpatch() 22 | 23 | t.Run("content is empty", func(t *testing.T) { 24 | var buf bytes.Buffer 25 | log.SetOutput(&buf) 26 | defer func() { 27 | log.SetOutput(os.Stderr) 28 | }() 29 | 30 | runTextCmd(&cobra.Command{}, []string{}) 31 | got := buf.String() 32 | 33 | want := "content can not be empty" 34 | 35 | if !strings.Contains(got, want) { 36 | t.Errorf("runTextCmd() = %v, want %v", got, want) 37 | } 38 | }) 39 | 40 | t.Run("new client error", func(t *testing.T) { 41 | var buf bytes.Buffer 42 | log.SetOutput(&buf) 43 | defer func() { 44 | log.SetOutput(os.Stderr) 45 | }() 46 | 47 | textVars.content = "123" 48 | msg := "new client error" 49 | 50 | monkey.Patch(newClient, func() (*dingtalk.Client, error) { 51 | return nil, errors.New(msg) 52 | }) 53 | defer monkey.Unpatch(newClient) 54 | 55 | runTextCmd(&cobra.Command{}, []string{}) 56 | 57 | got := buf.String() 58 | 59 | if !strings.Contains(got, msg) { 60 | t.Errorf("runTextCmd() = %v, want %v", got, msg) 61 | } 62 | }) 63 | 64 | t.Run("client send", func(t *testing.T) { 65 | textVars.content = "123" 66 | client := &dingtalk.Client{} 67 | 68 | monkey.Patch(newClient, func() (*dingtalk.Client, error) { 69 | return client, nil 70 | }) 71 | defer monkey.Unpatch(newClient) 72 | 73 | runTextCmd(&cobra.Command{}, []string{}) 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /cmd/dingtalk/feedCard.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/CatchZeng/dingtalk/pkg/dingtalk" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var feedCardCmd = &cobra.Command{ 11 | Use: "feedCard", 12 | Short: "send feedCard message with DingTalk robot", 13 | Long: `send feedCard message with DingTalk robot`, 14 | Args: cobra.MinimumNArgs(0), 15 | Run: runFeedCardCmd, 16 | } 17 | 18 | func runFeedCardCmd(_ *cobra.Command, args []string) { 19 | if len(feedCardVars.titles) < 1 || len(feedCardVars.picURLs) < 1 || len(feedCardVars.messageURLs) < 1 { 20 | log.Fatal("titles & picURLs & messageURLs can not be empty") 21 | return 22 | } 23 | 24 | if len(feedCardVars.titles) == len(feedCardVars.picURLs) && len(feedCardVars.picURLs) == len(feedCardVars.messageURLs) { 25 | client, err := newClient() 26 | if err != nil { 27 | log.Fatal(err.Error()) 28 | return 29 | } 30 | 31 | msg := dingtalk.NewFeedCardMessage() 32 | for i := 0; i < len(feedCardVars.titles); i++ { 33 | msg.AppendLink(feedCardVars.titles[i], feedCardVars.messageURLs[i], feedCardVars.picURLs[i]) 34 | } 35 | req, _, err := client.Send(msg) 36 | if debug { 37 | log.Print(req) 38 | } 39 | if err != nil { 40 | log.Fatal(err.Error()) 41 | return 42 | } 43 | } else { 44 | log.Fatal("titles & picURLs & messageURLs count must be equal") 45 | } 46 | } 47 | 48 | // FeedCardVars struct 49 | type FeedCardVars struct { 50 | titles []string 51 | picURLs []string 52 | messageURLs []string 53 | } 54 | 55 | var feedCardVars FeedCardVars 56 | 57 | func init() { 58 | rootCmd.AddCommand(feedCardCmd) 59 | 60 | feedCardCmd.Flags().StringSliceVarP(&feedCardVars.titles, "titles", "i", []string{}, "titles") 61 | feedCardCmd.Flags().StringSliceVarP(&feedCardVars.picURLs, "picURLs", "p", []string{}, "picURLs") 62 | feedCardCmd.Flags().StringSliceVarP(&feedCardVars.messageURLs, "messageURLs", "u", []string{}, "messageURLs") 63 | } 64 | -------------------------------------------------------------------------------- /configs/configs_test.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "bou.ke/monkey" 8 | "github.com/mitchellh/go-homedir" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | func TestInitConfig(t *testing.T) { 13 | t.Run("homedir.Dir() return error", func(t *testing.T) { 14 | monkey.Patch(homedir.Dir, func() (string, error) { 15 | return "", errors.New("homedir error") 16 | }) 17 | shouldPanic(t, InitConfig) 18 | }) 19 | 20 | t.Run("viper.ReadInConfig() return error", func(t *testing.T) { 21 | monkey.Patch(homedir.Dir, func() (string, error) { 22 | return "/catchzeng", nil 23 | }) 24 | 25 | monkey.Patch(viper.ReadInConfig, func() error { 26 | return errors.New("ReadInConfig error") 27 | }) 28 | 29 | InitConfig() 30 | }) 31 | 32 | t.Run("viper.ReadInConfig() return nil", func(t *testing.T) { 33 | monkey.Patch(homedir.Dir, func() (string, error) { 34 | return "/catchzeng", nil 35 | }) 36 | 37 | monkey.Patch(viper.ReadInConfig, func() error { 38 | return nil 39 | }) 40 | 41 | InitConfig() 42 | }) 43 | } 44 | 45 | func shouldPanic(t *testing.T, f func()) { 46 | defer func() { 47 | if r := recover(); r == nil { 48 | t.Errorf("should have panicked") 49 | } 50 | }() 51 | f() 52 | } 53 | 54 | func TestGetConfig(t *testing.T) { 55 | t.Run("viper.ReadInConfig() return error", func(t *testing.T) { 56 | monkey.Patch(viper.ReadInConfig, func() error { 57 | return errors.New("ReadInConfig error") 58 | }) 59 | 60 | key := "123" 61 | if _, err := GetConfig(key); err == nil { 62 | t.Error("GetConfig error") 63 | } 64 | }) 65 | 66 | t.Run("viper.ReadInConfig() return nil", func(t *testing.T) { 67 | monkey.Patch(viper.ReadInConfig, func() error { 68 | return nil 69 | }) 70 | 71 | monkey.Patch(viper.GetString, func(key string) string { 72 | return key 73 | }) 74 | 75 | key := "456" 76 | if value, err := GetConfig(key); err != nil || value != key { 77 | t.Error("GetConfig error") 78 | } 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /pkg/dingtalk/client.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "time" 10 | 11 | "github.com/CatchZeng/dingtalk/internal/security" 12 | ) 13 | 14 | // Client dingtalk client 15 | type Client struct { 16 | AccessToken string 17 | Secret string 18 | } 19 | 20 | // NewClient new dingtalk client 21 | func NewClient(accessToken, secret string) *Client { 22 | return &Client{ 23 | AccessToken: accessToken, 24 | Secret: secret, 25 | } 26 | } 27 | 28 | // Response response struct 29 | type Response struct { 30 | ErrMsg string `json:"errmsg"` 31 | ErrCode int64 `json:"errcode"` 32 | } 33 | 34 | const httpTimoutSecond = time.Duration(30) * time.Second 35 | 36 | // Send message 37 | func (d *Client) Send(message Message) (string, *Response, error) { 38 | res := &Response{} 39 | 40 | reqBytes, err := message.ToByte() 41 | if err != nil { 42 | return "", res, err 43 | } 44 | reqString := string(reqBytes) 45 | 46 | pushURL, err := security.URL(d.AccessToken, d.Secret) 47 | if err != nil { 48 | return reqString, res, err 49 | } 50 | 51 | req, err := http.NewRequest(http.MethodPost, pushURL, bytes.NewReader(reqBytes)) 52 | if err != nil { 53 | return reqString, res, err 54 | } 55 | req.Header.Add("Accept-Charset", "utf8") 56 | req.Header.Add("Content-Type", "application/json") 57 | 58 | client := new(http.Client) 59 | client.Timeout = httpTimoutSecond 60 | resp, err := client.Do(req) 61 | if err != nil { 62 | return reqString, res, err 63 | } 64 | defer resp.Body.Close() 65 | 66 | resultByte, err := ioutil.ReadAll(resp.Body) 67 | if err != nil { 68 | return reqString, res, err 69 | } 70 | 71 | err = json.Unmarshal(resultByte, &res) 72 | if err != nil { 73 | return reqString, res, fmt.Errorf("unmarshal http response body from json error = %v", err) 74 | } 75 | 76 | if res.ErrCode != 0 { 77 | return reqString, res, fmt.Errorf("send message to dingtalk error = %s", res.ErrMsg) 78 | } 79 | 80 | return reqString, res, nil 81 | } 82 | -------------------------------------------------------------------------------- /pkg/dingtalk/actionCard.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import "encoding/json" 4 | 5 | // ActionCardMessage struct 6 | type ActionCardMessage struct { 7 | MsgType MsgType `json:"msgtype"` 8 | ActionCard ActionCard `json:"actionCard"` 9 | } 10 | 11 | // ActionCard actionCard struct 12 | type ActionCard struct { 13 | Title string `json:"title"` 14 | Text string `json:"text"` 15 | SingleTitle string `json:"singleTitle"` 16 | SingleURL string `json:"singleURL"` 17 | Btns []Btn `json:"btns"` 18 | BtnOrientation string `json:"btnOrientation"` 19 | HideAvatar string `json:"hideAvatar"` 20 | } 21 | 22 | // Btn struct 23 | type Btn struct { 24 | Title string `json:"title"` 25 | ActionURL string `json:"actionURL"` 26 | } 27 | 28 | // ToByte to byte 29 | func (m *ActionCardMessage) ToByte() ([]byte, error) { 30 | m.MsgType = MsgTypeActionCard 31 | jsonByte, err := json.Marshal(m) 32 | return jsonByte, err 33 | } 34 | 35 | // NewActionCardMessage new message 36 | func NewActionCardMessage() *ActionCardMessage { 37 | msg := ActionCardMessage{} 38 | return &msg 39 | } 40 | 41 | // SetOverallJump set overall jump actionCard 42 | func (m *ActionCardMessage) SetOverallJump( 43 | title string, 44 | text string, 45 | singleTitle string, 46 | singleURL string, 47 | btnOrientation string, 48 | hideAvatar string) *ActionCardMessage { 49 | m.ActionCard = ActionCard{ 50 | Title: title, 51 | Text: text, 52 | SingleTitle: singleTitle, 53 | SingleURL: singleURL, 54 | BtnOrientation: btnOrientation, 55 | HideAvatar: hideAvatar, 56 | } 57 | return m 58 | } 59 | 60 | // SetIndependentJump set independent jump actionCard 61 | func (m *ActionCardMessage) SetIndependentJump( 62 | title string, 63 | text string, 64 | btns []Btn, 65 | btnOrientation string, 66 | hideAvatar string) *ActionCardMessage { 67 | m.ActionCard = ActionCard{ 68 | Title: title, 69 | Text: text, 70 | Btns: btns, 71 | BtnOrientation: btnOrientation, 72 | HideAvatar: hideAvatar, 73 | } 74 | return m 75 | } 76 | -------------------------------------------------------------------------------- /pkg/dingtalk/actionCard_test.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestActionCardMessage_ToByte(t *testing.T) { 9 | msg := NewActionCardMessage() 10 | _, _ = msg.ToByte() 11 | if msg.MsgType != MsgTypeActionCard { 12 | t.Errorf("ActionCardMessage.ToByte() type error") 13 | } 14 | } 15 | 16 | func TestNewActionCardMessage(t *testing.T) { 17 | tests := []struct { 18 | name string 19 | want *ActionCardMessage 20 | }{ 21 | { 22 | name: "Should return a ActionCardMessage instance", 23 | want: &ActionCardMessage{}, 24 | }, 25 | } 26 | for _, tt := range tests { 27 | t.Run(tt.name, func(t *testing.T) { 28 | if got := NewActionCardMessage(); !reflect.DeepEqual(got, tt.want) { 29 | t.Errorf("NewActionCardMessage() = %v, want %v", got, tt.want) 30 | } 31 | }) 32 | } 33 | } 34 | 35 | func TestActionCardMessage_SetOverallJump(t *testing.T) { 36 | got := NewActionCardMessage() 37 | got.SetOverallJump("title", "text", "singleTitle", "singleURL", "btnOrientation", "hideAvatar") 38 | 39 | card := ActionCard{ 40 | Title: "title", 41 | Text: "text", 42 | SingleTitle: "singleTitle", 43 | SingleURL: "singleURL", 44 | BtnOrientation: "btnOrientation", 45 | HideAvatar: "hideAvatar", 46 | } 47 | want := NewActionCardMessage() 48 | want.ActionCard = card 49 | 50 | if !reflect.DeepEqual(got, want) { 51 | t.Errorf("SetOverallJump() = %v, want %v", got, want) 52 | } 53 | } 54 | 55 | func TestActionCardMessage_SetIndependentJump(t *testing.T) { 56 | got := NewActionCardMessage() 57 | got.SetIndependentJump("title", "text", []Btn{{ 58 | Title: "title", 59 | ActionURL: "actionURL", 60 | }}, "btnOrientation", "hideAvatar") 61 | 62 | card := ActionCard{ 63 | Title: "title", 64 | Text: "text", 65 | Btns: []Btn{{ 66 | Title: "title", 67 | ActionURL: "actionURL", 68 | }}, 69 | BtnOrientation: "btnOrientation", 70 | HideAvatar: "hideAvatar", 71 | } 72 | want := NewActionCardMessage() 73 | want.ActionCard = card 74 | 75 | if !reflect.DeepEqual(got, want) { 76 | t.Errorf("SetIndependentJump() = %v, want %v", got, want) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /cmd/dingtalk/markdown_test.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "log" 7 | "os" 8 | "strings" 9 | "testing" 10 | 11 | "bou.ke/monkey" 12 | "github.com/CatchZeng/dingtalk/pkg/dingtalk" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func Test_runMarkdownCmd(t *testing.T) { 17 | fakeExit := func(int) { 18 | log.Print("fake exit") 19 | } 20 | patch := monkey.Patch(os.Exit, fakeExit) 21 | defer patch.Unpatch() 22 | 23 | t.Run("title is empty", func(t *testing.T) { 24 | var buf bytes.Buffer 25 | log.SetOutput(&buf) 26 | defer func() { 27 | log.SetOutput(os.Stderr) 28 | }() 29 | 30 | runMarkdownCmd(&cobra.Command{}, []string{}) 31 | got := buf.String() 32 | 33 | want := "title can not be empty" 34 | 35 | if !strings.Contains(got, want) { 36 | t.Errorf("runMarkdownCmd() = %v, want %v", got, want) 37 | } 38 | }) 39 | 40 | t.Run("text is empty", func(t *testing.T) { 41 | var buf bytes.Buffer 42 | log.SetOutput(&buf) 43 | defer func() { 44 | log.SetOutput(os.Stderr) 45 | }() 46 | 47 | markdownVars.title = "123" 48 | 49 | runMarkdownCmd(&cobra.Command{}, []string{}) 50 | got := buf.String() 51 | 52 | want := "text can not be empty" 53 | 54 | if !strings.Contains(got, want) { 55 | t.Errorf("runMarkdownCmd() = %v, want %v", got, want) 56 | } 57 | }) 58 | 59 | t.Run("new client error", func(t *testing.T) { 60 | var buf bytes.Buffer 61 | log.SetOutput(&buf) 62 | defer func() { 63 | log.SetOutput(os.Stderr) 64 | }() 65 | 66 | markdownVars.title = "123" 67 | markdownVars.text = "123" 68 | msg := "new client error" 69 | 70 | monkey.Patch(newClient, func() (*dingtalk.Client, error) { 71 | return nil, errors.New(msg) 72 | }) 73 | defer monkey.Unpatch(newClient) 74 | 75 | runMarkdownCmd(&cobra.Command{}, []string{}) 76 | 77 | got := buf.String() 78 | 79 | if !strings.Contains(got, msg) { 80 | t.Errorf("runMarkdownCmd() = %v, want %v", got, msg) 81 | } 82 | }) 83 | 84 | t.Run("client send", func(t *testing.T) { 85 | markdownVars.title = "123" 86 | markdownVars.text = "123" 87 | client := &dingtalk.Client{} 88 | 89 | monkey.Patch(newClient, func() (*dingtalk.Client, error) { 90 | return client, nil 91 | }) 92 | defer monkey.Unpatch(newClient) 93 | 94 | runMarkdownCmd(&cobra.Command{}, []string{}) 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /internal/security/security.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "fmt" 8 | "io" 9 | "math" 10 | "net/url" 11 | "strconv" 12 | "time" 13 | ) 14 | 15 | // https://oapi.dingtalk.com/robot/send?access_token=xxx 16 | const dingTalkOAPI = "oapi.dingtalk.com" 17 | 18 | var dingTalkURL url.URL = url.URL{ 19 | Scheme: "https", 20 | Host: dingTalkOAPI, 21 | Path: "robot/send", 22 | } 23 | 24 | // URL get DingTalk URL with accessToken & secret 25 | // If no signature is set, the secret is set to "" 26 | // 如果没有加签,secret 设置为 "" 即可 27 | func URL(accessToken string, secret string) (string, error) { 28 | timestamp := strconv.FormatInt(time.Now().Unix()*1000, 10) 29 | return URLWithTimestamp(timestamp, accessToken, secret) 30 | } 31 | 32 | // URLWithTimestamp get DingTalk URL with timestamp & accessToken & secret 33 | func URLWithTimestamp(timestamp string, accessToken string, secret string) (string, error) { 34 | dtu := dingTalkURL 35 | value := url.Values{} 36 | value.Set("access_token", accessToken) 37 | 38 | if secret == "" { 39 | dtu.RawQuery = value.Encode() 40 | return dtu.String(), nil 41 | } 42 | 43 | sign, err := sign(timestamp, secret) 44 | if err != nil { 45 | dtu.RawQuery = value.Encode() 46 | return dtu.String(), err 47 | } 48 | 49 | value.Set("timestamp", timestamp) 50 | value.Set("sign", sign) 51 | dtu.RawQuery = value.Encode() 52 | return dtu.String(), nil 53 | } 54 | 55 | // Validate validate 56 | // https://ding-doc.dingtalk.com/doc#/serverapi2/elzz1p 57 | func Validate(signStr, timestamp, secret string) (bool, error) { 58 | t, err := strconv.ParseInt(timestamp, 10, 64) 59 | if err != nil { 60 | return false, err 61 | } 62 | 63 | timeGap := time.Since(time.Unix(t, 0)) 64 | if math.Abs(timeGap.Hours()) > 1 { 65 | return false, fmt.Errorf("specified timestamp is expired") 66 | } 67 | 68 | ourSign, err := sign(timestamp, secret) 69 | if err != nil { 70 | return false, err 71 | } 72 | return ourSign == signStr, nil 73 | } 74 | 75 | func sign(timestamp string, secret string) (string, error) { 76 | stringToSign := fmt.Sprintf("%s\n%s", timestamp, secret) 77 | h := hmac.New(sha256.New, []byte(secret)) 78 | if _, err := io.WriteString(h, stringToSign); err != nil { 79 | return "", err 80 | } 81 | return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil 82 | } 83 | -------------------------------------------------------------------------------- /cmd/dingtalk/root.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/CatchZeng/dingtalk/configs" 10 | "github.com/CatchZeng/dingtalk/pkg/dingtalk" 11 | "github.com/spf13/viper" 12 | 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | var rootCmd = &cobra.Command{ 17 | Use: "dingtalk", 18 | Short: "dingtalk is a command line tool for DingTalk", 19 | Long: "dingtalk is a command line tool for DingTalk", 20 | } 21 | 22 | // Execute adds all child commands to the root command and sets flags appropriately. 23 | // This is called by main.main(). It only needs to happen once to the rootCmd. 24 | func Execute() { 25 | if err := rootCmd.Execute(); err != nil { 26 | fmt.Println(err) 27 | os.Exit(1) 28 | } 29 | } 30 | 31 | func newClient() (*dingtalk.Client, error) { 32 | token := getAccessToken() 33 | secret := getSecret() 34 | 35 | if len(token) < 1 { 36 | return nil, errors.New("access_token can not be empty") 37 | } 38 | client := dingtalk.NewClient(token, secret) 39 | return client, nil 40 | } 41 | 42 | func getAccessToken() string { 43 | if len(accessToken) > 0 { 44 | return accessToken 45 | } 46 | 47 | value, err := configs.GetConfig(configs.AccessToken) 48 | if err == nil { 49 | return value 50 | } 51 | return "" 52 | } 53 | 54 | func getSecret() string { 55 | if len(secret) > 0 { 56 | return secret 57 | } 58 | 59 | value, err := configs.GetConfig(configs.Secret) 60 | if err == nil { 61 | return value 62 | } 63 | return "" 64 | } 65 | 66 | var accessToken, secret string 67 | var isAtAll bool 68 | var atMobiles []string 69 | var debug bool 70 | 71 | func init() { 72 | cobra.OnInitialize(configs.InitConfig) 73 | 74 | rootCmd.PersistentFlags().StringVarP(&accessToken, configs.AccessToken, "t", "", configs.AccessToken) 75 | rootCmd.PersistentFlags().StringVarP(&secret, configs.Secret, "s", "", configs.Secret) 76 | rootCmd.PersistentFlags().BoolVarP(&isAtAll, "isAtAll", "a", false, "isAtAll") 77 | rootCmd.PersistentFlags().StringSliceVarP(&atMobiles, "atMobiles", "m", []string{}, "atMobiles") 78 | rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "D", false, "debug") 79 | 80 | if err := viper.BindPFlag(configs.AccessToken, rootCmd.PersistentFlags().Lookup(configs.AccessToken)); err != nil { 81 | log.Print(err) 82 | } 83 | if err := viper.BindPFlag(configs.Secret, rootCmd.PersistentFlags().Lookup(configs.Secret)); err != nil { 84 | log.Print(err) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | BASEDIR = $(shell pwd) 3 | 4 | APP_NAME=dingtalk 5 | APP_VERSION=1.5.0 6 | IMAGE_NAME="catchzeng/${APP_NAME}:${APP_VERSION}" 7 | IMAGE_LATEST="catchzeng/${APP_NAME}:latest" 8 | 9 | all: mod fmt imports lint test 10 | first: 11 | go install golang.org/x/tools/cmd/goimports@latest 12 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 13 | fmt: 14 | gofmt -w . 15 | mod: 16 | go mod tidy 17 | imports: 18 | goimports -w . 19 | lint: 20 | golangci-lint run 21 | .PHONY: test 22 | test: 23 | sh scripts/test.sh 24 | test-sonar: 25 | go test -gcflags=-l -coverpkg=./... -coverprofile=coverage.data ./... 26 | mock: 27 | sh scripts/mock.sh 28 | .PHONY: build 29 | build: 30 | rm -f dingtalk 31 | go build -o dingtalk main.go 32 | build-darwin-amd64: 33 | rm -f dingtalk dingtalk-darwin-amd64.zip 34 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o dingtalk main.go 35 | zip dingtalk-darwin-amd64.zip dingtalk 36 | build-darwin-arm64: 37 | rm -f dingtalk dingtalk-darwin-arm64.zip 38 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o dingtalk main.go 39 | zip dingtalk-darwin-arm64.zip dingtalk 40 | build-linux: 41 | rm -f dingtalk dingtalk-linux-amd64.zip 42 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dingtalk main.go 43 | zip dingtalk-linux-amd64.zip dingtalk 44 | build-win: 45 | rm -f dingtalk.exe dingtalk-windows-amd64.zip 46 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o dingtalk.exe main.go 47 | zip dingtalk-windows-amd64.zip dingtalk.exe 48 | build-win32: 49 | rm -f dingtalk.exe dingtalk-windows-386.zip 50 | CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -o dingtalk.exe main.go 51 | zip dingtalk-windows-386.zip dingtalk.exe 52 | build-release: 53 | make build-darwin-amd64 54 | make build-darwin-arm64 55 | make build-linux 56 | make build-win 57 | make build-win32 58 | rm -f dingtalk dingtalk.exe 59 | build-docker: 60 | sh build/package/build.sh ${IMAGE_NAME} 61 | push-docker: 62 | docker tag ${IMAGE_NAME} ${IMAGE_LATEST}; 63 | docker push ${IMAGE_NAME}; 64 | docker push ${IMAGE_LATEST}; 65 | help: 66 | @echo "first - first time" 67 | @echo "fmt - go format" 68 | @echo "mod - go mod tidy" 69 | @echo "imports - go imports" 70 | @echo "lint - run golangci-lint" 71 | @echo "test - unit test" 72 | @echo "mock - mockgen" 73 | @echo "build - build binary" 74 | @echo "build-mac - build mac binary" 75 | @echo "build-linux - build linux amd64 binary" 76 | @echo "build-win - build win amd64 binary" 77 | @echo "build-win32 - build win 386 binary" 78 | @echo "build-docker - build docker image" 79 | @echo "push-docker - push docker image to docker hub" 80 | -------------------------------------------------------------------------------- /pkg/dingtalk/client_test.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net/http" 7 | "reflect" 8 | "testing" 9 | 10 | "bou.ke/monkey" 11 | "github.com/CatchZeng/dingtalk/internal/security" 12 | 13 | mock_message "github.com/CatchZeng/dingtalk/test/mocks/message" 14 | "github.com/golang/mock/gomock" 15 | ) 16 | 17 | func TestNewClient(t *testing.T) { 18 | type args struct { 19 | accessToken string 20 | secret string 21 | } 22 | tests := []struct { 23 | name string 24 | args args 25 | want *Client 26 | }{ 27 | { 28 | name: "", 29 | args: args{ 30 | accessToken: "123456", 31 | secret: "111111", 32 | }, 33 | want: &Client{ 34 | AccessToken: "123456", 35 | Secret: "111111", 36 | }, 37 | }, 38 | } 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | if got := NewClient(tt.args.accessToken, tt.args.secret); !reflect.DeepEqual(got, tt.want) { 42 | t.Errorf("NewClient() = %v, want %v", got, tt.want) 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func TestClient_Send(t *testing.T) { 49 | ctrl := gomock.NewController(t) 50 | defer ctrl.Finish() 51 | 52 | message := mock_message.NewMockMessage(ctrl) 53 | 54 | t.Run("message return error", func(t *testing.T) { 55 | c := &Client{} 56 | 57 | message.EXPECT().ToByte().Return([]byte{}, errors.New("test")) 58 | 59 | if _, _, err := c.Send(message); err == nil { 60 | t.Error("send error") 61 | } 62 | }) 63 | 64 | t.Run("security.URL return error", func(t *testing.T) { 65 | c := &Client{ 66 | AccessToken: "test-access-token", 67 | Secret: "test-secret", 68 | } 69 | 70 | message.EXPECT().ToByte().Return([]byte{}, nil) 71 | monkey.Patch(security.URL, func(accessToken string, secret string) (string, error) { 72 | return "", errors.New("URL error") 73 | }) 74 | 75 | if _, _, err := c.Send(message); err == nil { 76 | t.Error("send error") 77 | } 78 | }) 79 | 80 | t.Run("http.NewRequest return error", func(t *testing.T) { 81 | c := &Client{ 82 | AccessToken: "test-access-token", 83 | Secret: "test-secret", 84 | } 85 | 86 | message.EXPECT().ToByte().Return([]byte{}, nil) 87 | monkey.Patch(security.URL, func(accessToken string, secret string) (string, error) { 88 | return "https://oapi.dingtalk.com/robot/send?access_token=ewfewfwfwefwafew", nil 89 | }) 90 | 91 | monkey.Patch(http.NewRequest, func(method, url string, body io.Reader) (*http.Request, error) { 92 | return nil, errors.New("NewRequest error") 93 | }) 94 | 95 | if _, _, err := c.Send(message); err == nil { 96 | t.Error("send error") 97 | } 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /cmd/dingtalk/feedCard_test.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "log" 7 | "os" 8 | "strings" 9 | "testing" 10 | 11 | "bou.ke/monkey" 12 | "github.com/CatchZeng/dingtalk/pkg/dingtalk" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func Test_runFeedCardCmd(t *testing.T) { 17 | fakeExit := func(int) { 18 | log.Print("fake exit") 19 | } 20 | patch := monkey.Patch(os.Exit, fakeExit) 21 | defer patch.Unpatch() 22 | 23 | const emptyMsg = "titles & picURLs & messageURLs can not be empty" 24 | const differentCountMsg = "titles & picURLs & messageURLs count must be equal" 25 | 26 | t.Run("titles is empty", func(t *testing.T) { 27 | var buf bytes.Buffer 28 | log.SetOutput(&buf) 29 | defer func() { 30 | log.SetOutput(os.Stderr) 31 | }() 32 | 33 | runFeedCardCmd(&cobra.Command{}, []string{}) 34 | got := buf.String() 35 | 36 | want := emptyMsg 37 | 38 | if !strings.Contains(got, want) { 39 | t.Errorf("runFeedCardCmd() = %v, want %v", got, want) 40 | } 41 | }) 42 | 43 | t.Run("titles & picURLs different count", func(t *testing.T) { 44 | var buf bytes.Buffer 45 | log.SetOutput(&buf) 46 | defer func() { 47 | log.SetOutput(os.Stderr) 48 | }() 49 | 50 | feedCardVars.titles = []string{"1", "2"} 51 | feedCardVars.picURLs = []string{"1"} 52 | feedCardVars.messageURLs = []string{"1"} 53 | 54 | runFeedCardCmd(&cobra.Command{}, []string{}) 55 | got := buf.String() 56 | 57 | want := differentCountMsg 58 | 59 | if !strings.Contains(got, want) { 60 | t.Errorf("runFeedCardCmd() = %v, want %v", got, want) 61 | } 62 | }) 63 | 64 | t.Run("new client error", func(t *testing.T) { 65 | var buf bytes.Buffer 66 | log.SetOutput(&buf) 67 | defer func() { 68 | log.SetOutput(os.Stderr) 69 | }() 70 | 71 | feedCardVars.titles = []string{"1"} 72 | feedCardVars.picURLs = []string{"1"} 73 | feedCardVars.messageURLs = []string{"1"} 74 | msg := "new client error" 75 | 76 | monkey.Patch(newClient, func() (*dingtalk.Client, error) { 77 | return nil, errors.New(msg) 78 | }) 79 | defer monkey.Unpatch(newClient) 80 | 81 | runFeedCardCmd(&cobra.Command{}, []string{}) 82 | 83 | got := buf.String() 84 | 85 | if !strings.Contains(got, msg) { 86 | t.Errorf("runFeedCardCmd() = %v, want %v", got, msg) 87 | } 88 | }) 89 | 90 | t.Run("client send", func(t *testing.T) { 91 | feedCardVars.titles = []string{"1"} 92 | feedCardVars.picURLs = []string{"1"} 93 | feedCardVars.messageURLs = []string{"1"} 94 | client := &dingtalk.Client{} 95 | 96 | monkey.Patch(newClient, func() (*dingtalk.Client, error) { 97 | return client, nil 98 | }) 99 | defer monkey.Unpatch(newClient) 100 | 101 | runFeedCardCmd(&cobra.Command{}, []string{}) 102 | }) 103 | } 104 | -------------------------------------------------------------------------------- /cmd/dingtalk/link_test.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "log" 7 | "os" 8 | "strings" 9 | "testing" 10 | 11 | "bou.ke/monkey" 12 | "github.com/CatchZeng/dingtalk/pkg/dingtalk" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func Test_runLinkCmd(t *testing.T) { 17 | fakeExit := func(int) { 18 | log.Print("fake exit") 19 | } 20 | patch := monkey.Patch(os.Exit, fakeExit) 21 | defer patch.Unpatch() 22 | 23 | t.Run("title is empty", func(t *testing.T) { 24 | var buf bytes.Buffer 25 | log.SetOutput(&buf) 26 | defer func() { 27 | log.SetOutput(os.Stderr) 28 | }() 29 | 30 | runLinkCmd(&cobra.Command{}, []string{}) 31 | got := buf.String() 32 | 33 | want := "title can not be empty" 34 | 35 | if !strings.Contains(got, want) { 36 | t.Errorf("runLinkCmd() = %v, want %v", got, want) 37 | } 38 | }) 39 | 40 | t.Run("text is empty", func(t *testing.T) { 41 | var buf bytes.Buffer 42 | log.SetOutput(&buf) 43 | defer func() { 44 | log.SetOutput(os.Stderr) 45 | }() 46 | 47 | linkVars.title = "123" 48 | 49 | runLinkCmd(&cobra.Command{}, []string{}) 50 | got := buf.String() 51 | 52 | want := "text can not be empty" 53 | 54 | if !strings.Contains(got, want) { 55 | t.Errorf("runLinkCmd() = %v, want %v", got, want) 56 | } 57 | }) 58 | 59 | t.Run("messageURL is empty", func(t *testing.T) { 60 | var buf bytes.Buffer 61 | log.SetOutput(&buf) 62 | defer func() { 63 | log.SetOutput(os.Stderr) 64 | }() 65 | 66 | linkVars.title = "123" 67 | linkVars.text = "123" 68 | 69 | runLinkCmd(&cobra.Command{}, []string{}) 70 | got := buf.String() 71 | 72 | want := "messageURL can not be empty" 73 | 74 | if !strings.Contains(got, want) { 75 | t.Errorf("runLinkCmd() = %v, want %v", got, want) 76 | } 77 | }) 78 | 79 | t.Run("new client error", func(t *testing.T) { 80 | var buf bytes.Buffer 81 | log.SetOutput(&buf) 82 | defer func() { 83 | log.SetOutput(os.Stderr) 84 | }() 85 | 86 | linkVars.title = "123" 87 | linkVars.text = "123" 88 | linkVars.messageURL = "123" 89 | msg := "new client error" 90 | 91 | monkey.Patch(newClient, func() (*dingtalk.Client, error) { 92 | return nil, errors.New(msg) 93 | }) 94 | defer monkey.Unpatch(newClient) 95 | 96 | runLinkCmd(&cobra.Command{}, []string{}) 97 | 98 | got := buf.String() 99 | 100 | if !strings.Contains(got, msg) { 101 | t.Errorf("runLinkCmd() = %v, want %v", got, msg) 102 | } 103 | }) 104 | 105 | t.Run("client send", func(t *testing.T) { 106 | linkVars.title = "123" 107 | linkVars.text = "123" 108 | linkVars.messageURL = "123" 109 | client := &dingtalk.Client{} 110 | 111 | monkey.Patch(newClient, func() (*dingtalk.Client, error) { 112 | return client, nil 113 | }) 114 | defer monkey.Unpatch(newClient) 115 | 116 | runLinkCmd(&cobra.Command{}, []string{}) 117 | }) 118 | } 119 | -------------------------------------------------------------------------------- /cmd/dingtalk/root_test.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "os" 7 | "testing" 8 | 9 | "bou.ke/monkey" 10 | "github.com/CatchZeng/dingtalk/configs" 11 | ) 12 | 13 | func Test_newClient(t *testing.T) { 14 | fakeExit := func(int) { 15 | log.Print("fake exit") 16 | } 17 | patch := monkey.Patch(os.Exit, fakeExit) 18 | defer patch.Unpatch() 19 | 20 | t.Run("getAccessToken return empty", func(t *testing.T) { 21 | accessToken = "" 22 | _, err := newClient() 23 | if err == nil { 24 | t.Error("newClient() error") 25 | } 26 | }) 27 | 28 | t.Run("getAccessToken return token", func(t *testing.T) { 29 | accessToken = "123" 30 | client, err := newClient() 31 | if err != nil || client == nil { 32 | t.Error("newClient() error") 33 | } 34 | }) 35 | } 36 | 37 | func Test_getAccessToken(t *testing.T) { 38 | t.Run("get from accessToken", func(t *testing.T) { 39 | accessToken = "123" 40 | got := getAccessToken() 41 | if got != accessToken { 42 | t.Errorf("getAccessToken() = %v, want %v", got, accessToken) 43 | } 44 | }) 45 | 46 | t.Run("GetConfig error", func(t *testing.T) { 47 | accessToken = "" 48 | 49 | monkey.Patch(configs.GetConfig, func(key string) (string, error) { 50 | return "", errors.New("GetConfig error") 51 | }) 52 | defer monkey.Unpatch(configs.GetConfig) 53 | 54 | got := getAccessToken() 55 | if got != "" { 56 | t.Errorf("getAccessToken() = %v, want %v", got, "") 57 | } 58 | }) 59 | 60 | t.Run("get from config", func(t *testing.T) { 61 | accessToken = "" 62 | 63 | want := "123" 64 | monkey.Patch(configs.GetConfig, func(key string) (string, error) { 65 | return want, nil 66 | }) 67 | defer monkey.Unpatch(configs.GetConfig) 68 | 69 | got := getAccessToken() 70 | if got != want { 71 | t.Errorf("getAccessToken() = %v, want %v", got, want) 72 | } 73 | }) 74 | } 75 | 76 | func Test_getSecret(t *testing.T) { 77 | t.Run("get from secret", func(t *testing.T) { 78 | secret = "123" 79 | got := getSecret() 80 | if got != secret { 81 | t.Errorf("getSecret() = %v, want %v", got, secret) 82 | } 83 | }) 84 | 85 | t.Run("GetConfig error", func(t *testing.T) { 86 | secret = "" 87 | 88 | monkey.Patch(configs.GetConfig, func(key string) (string, error) { 89 | return "", errors.New("GetConfig error") 90 | }) 91 | defer monkey.Unpatch(configs.GetConfig) 92 | 93 | got := getSecret() 94 | if got != "" { 95 | t.Errorf("getSecret() = %v, want %v", got, "") 96 | } 97 | }) 98 | 99 | t.Run("get from config", func(t *testing.T) { 100 | secret = "" 101 | 102 | want := "123" 103 | monkey.Patch(configs.GetConfig, func(key string) (string, error) { 104 | return want, nil 105 | }) 106 | defer monkey.Unpatch(configs.GetConfig) 107 | 108 | got := getSecret() 109 | if got != want { 110 | t.Errorf("getSecret() = %v, want %v", got, want) 111 | } 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /internal/security/security_test.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "testing" 7 | "time" 8 | 9 | "bou.ke/monkey" 10 | ) 11 | 12 | const ( 13 | timestamp = "1582163555000" 14 | accessToken = "1c53e149ba5de6597ca2442f0e901fd86156780b8ac141e4a75afdc44c85ca4f" 15 | secret = "SECb90923e19e58b466481e9e7b7a5b4f108a4531abde590ad3967fb29f0eae5c68" 16 | signed = "BQKsG%2BQOCl%2BbYJOLc6pxDHxjVquzlZPWgvRzeN2J5zY%3D" 17 | ) 18 | 19 | func TestURL(t *testing.T) { 20 | monkey.Patch(strconv.FormatInt, func(i int64, base int) string { 21 | return timestamp 22 | }) 23 | 24 | defer monkey.Unpatch(strconv.FormatInt) 25 | 26 | got, err := URL(accessToken, secret) 27 | if err != nil { 28 | t.Errorf("URL() error = %v", err) 29 | } 30 | 31 | want := fmt.Sprintf("https://oapi.dingtalk.com/robot/send?access_token=%v&sign=%v×tamp=%v", accessToken, signed, timestamp) 32 | if got != want { 33 | t.Errorf("URL() = %v, want %v", got, want) 34 | } 35 | } 36 | 37 | func TestURLWithTimestamp(t *testing.T) { 38 | type args struct { 39 | accessToken string 40 | secret string 41 | } 42 | tests := []struct { 43 | name string 44 | args args 45 | want string 46 | wantErr bool 47 | }{ 48 | { 49 | name: "without sign", 50 | args: args{ 51 | accessToken: accessToken, 52 | }, 53 | want: fmt.Sprintf("https://oapi.dingtalk.com/robot/send?access_token=%v", accessToken), 54 | wantErr: false, 55 | }, 56 | { 57 | name: "with sign", 58 | args: args{ 59 | accessToken: accessToken, 60 | secret: secret, 61 | }, 62 | want: fmt.Sprintf("https://oapi.dingtalk.com/robot/send?access_token=%v&sign=%v×tamp=%v", accessToken, signed, timestamp), 63 | wantErr: false, 64 | }, 65 | } 66 | for _, tt := range tests { 67 | t.Run(tt.name, func(t *testing.T) { 68 | got, err := URLWithTimestamp(timestamp, tt.args.accessToken, tt.args.secret) 69 | if (err != nil) != tt.wantErr { 70 | t.Errorf("URLWithTimestamp() error = %v, wantErr %v", err, tt.wantErr) 71 | return 72 | } 73 | if got != tt.want { 74 | t.Errorf("URLWithTimestamp() = %v, want %v", got, tt.want) 75 | } 76 | }) 77 | } 78 | } 79 | 80 | func TestValidate(t *testing.T) { 81 | validateTimestamp := strconv.FormatInt(time.Now().Add(60*time.Second).Unix(), 10) 82 | 83 | result, err := sign(validateTimestamp, secret) 84 | if err != nil { 85 | t.Error(err) 86 | } 87 | 88 | _, err = Validate(result, strconv.FormatInt(time.Now().Add(-3601*time.Second).Unix(), 10), secret) 89 | if err == nil { 90 | t.Error("this should be err, but not") 91 | } 92 | 93 | _, err = Validate(result, strconv.FormatInt(time.Now().Add(3601*time.Second).Unix(), 10), secret) 94 | if err == nil { 95 | t.Error("this should be err, but not") 96 | } 97 | 98 | b, err := Validate(result, validateTimestamp, secret) 99 | if err != nil { 100 | t.Error(err) 101 | } else { 102 | if !b { 103 | t.Error("token is not the same") 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /cmd/dingtalk/actionCard.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/CatchZeng/dingtalk/pkg/dingtalk" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var actionCardCmd = &cobra.Command{ 11 | Use: "actionCard", 12 | Short: "send actionCard message with DingTalk robot", 13 | Long: `send actionCard message with DingTalk robot`, 14 | Args: cobra.MinimumNArgs(0), 15 | Run: runActionCardCmd, 16 | } 17 | 18 | func runActionCardCmd(_ *cobra.Command, args []string) { 19 | if len(actionCardVars.Title) < 1 { 20 | log.Fatal("title can not be empty") 21 | return 22 | } 23 | 24 | if len(actionCardVars.Text) < 1 { 25 | log.Fatal("text can not be empty") 26 | return 27 | } 28 | 29 | var isOverallJump = false 30 | if len(actionCardVars.SingleTitle) < 1 { 31 | if len(btnTitles) < 1 { 32 | log.Fatal("btns can not be empty when singleTitle is empty") 33 | return 34 | } 35 | } else { 36 | isOverallJump = true 37 | if len(actionCardVars.SingleURL) < 1 { 38 | log.Fatal("singleURL can not be empty") 39 | return 40 | } 41 | } 42 | 43 | client, err := newClient() 44 | if err != nil { 45 | log.Fatal(err.Error()) 46 | return 47 | } 48 | 49 | msg := dingtalk.NewActionCardMessage() 50 | if isOverallJump { 51 | msg.SetOverallJump( 52 | actionCardVars.Title, 53 | actionCardVars.Text, 54 | actionCardVars.SingleTitle, 55 | actionCardVars.SingleURL, 56 | actionCardVars.BtnOrientation, 57 | actionCardVars.HideAvatar) 58 | } else { 59 | if len(btnTitles) != len(btnActionURLs) { 60 | log.Fatal("btnTitles & btnActionURLs count must be equal") 61 | return 62 | } 63 | 64 | for i := 0; i < len(btnTitles); i++ { 65 | actionCardVars.Btns = append(actionCardVars.Btns, dingtalk.Btn{ 66 | Title: btnTitles[i], 67 | ActionURL: btnActionURLs[i], 68 | }) 69 | } 70 | msg.SetIndependentJump( 71 | actionCardVars.Title, 72 | actionCardVars.Text, 73 | actionCardVars.Btns, 74 | actionCardVars.BtnOrientation, 75 | actionCardVars.HideAvatar) 76 | } 77 | req, _, err := client.Send(msg) 78 | if debug { 79 | log.Print(req) 80 | } 81 | if err != nil { 82 | log.Fatal(err.Error()) 83 | } 84 | } 85 | 86 | var actionCardVars dingtalk.ActionCard 87 | var btnTitles, btnActionURLs []string 88 | 89 | func init() { 90 | rootCmd.AddCommand(actionCardCmd) 91 | actionCardCmd.Flags().StringVarP(&actionCardVars.Title, "title", "i", "", "title") 92 | actionCardCmd.Flags().StringVarP(&actionCardVars.Text, "text", "e", "", "text") 93 | actionCardCmd.Flags().StringVarP(&actionCardVars.SingleTitle, "singleTitle", "n", "", "singleTitle") 94 | actionCardCmd.Flags().StringVarP(&actionCardVars.SingleURL, "singleURL", "u", "", "singleURL") 95 | actionCardCmd.Flags().StringSliceVarP(&btnTitles, "btnTitles", "b", []string{}, "btnTitles") 96 | actionCardCmd.Flags().StringSliceVarP(&btnActionURLs, "btnActionURLs", "c", []string{}, "btnActionURLs") 97 | actionCardCmd.Flags().StringVarP(&actionCardVars.BtnOrientation, "btnOrientation", "o", "", "btnOrientation") 98 | actionCardCmd.Flags().StringVarP(&actionCardVars.HideAvatar, "hideAvatar", "d", "", "hideAvatar") 99 | } 100 | -------------------------------------------------------------------------------- /cmd/dingtalk/actionCard_test.go: -------------------------------------------------------------------------------- 1 | package dingtalk 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "log" 7 | "os" 8 | "strings" 9 | "testing" 10 | 11 | "bou.ke/monkey" 12 | "github.com/CatchZeng/dingtalk/pkg/dingtalk" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func Test_runActionCardCmd(t *testing.T) { 17 | fakeExit := func(int) { 18 | log.Print("fake exit") 19 | } 20 | patch := monkey.Patch(os.Exit, fakeExit) 21 | defer patch.Unpatch() 22 | 23 | t.Run("title is empty", func(t *testing.T) { 24 | var buf bytes.Buffer 25 | log.SetOutput(&buf) 26 | defer func() { 27 | log.SetOutput(os.Stderr) 28 | }() 29 | 30 | runActionCardCmd(&cobra.Command{}, []string{}) 31 | got := buf.String() 32 | 33 | want := "title can not be empty" 34 | 35 | if !strings.Contains(got, want) { 36 | t.Errorf("runActionCardCmd() = %v, want %v", got, want) 37 | } 38 | }) 39 | 40 | t.Run("text is empty", func(t *testing.T) { 41 | var buf bytes.Buffer 42 | log.SetOutput(&buf) 43 | defer func() { 44 | log.SetOutput(os.Stderr) 45 | }() 46 | 47 | actionCardVars.Title = "123" 48 | 49 | runActionCardCmd(&cobra.Command{}, []string{}) 50 | got := buf.String() 51 | 52 | want := "text can not be empty" 53 | 54 | if !strings.Contains(got, want) { 55 | t.Errorf("runActionCardCmd() = %v, want %v", got, want) 56 | } 57 | }) 58 | 59 | t.Run("singleTitle is empty", func(t *testing.T) { 60 | var buf bytes.Buffer 61 | log.SetOutput(&buf) 62 | defer func() { 63 | log.SetOutput(os.Stderr) 64 | }() 65 | 66 | actionCardVars.Title = "123" 67 | actionCardVars.Text = "123" 68 | actionCardVars.SingleTitle = "" 69 | btnTitles = []string{} 70 | 71 | runActionCardCmd(&cobra.Command{}, []string{}) 72 | got := buf.String() 73 | 74 | want := "btns can not be empty when singleTitle is empty" 75 | 76 | if !strings.Contains(got, want) { 77 | t.Errorf("runActionCardCmd() = %v, want %v", got, want) 78 | } 79 | }) 80 | 81 | t.Run("singleTitle is not empty", func(t *testing.T) { 82 | var buf bytes.Buffer 83 | log.SetOutput(&buf) 84 | defer func() { 85 | log.SetOutput(os.Stderr) 86 | }() 87 | 88 | actionCardVars.Title = "123" 89 | actionCardVars.Text = "123" 90 | actionCardVars.SingleTitle = "123" 91 | actionCardVars.SingleURL = "" 92 | 93 | runActionCardCmd(&cobra.Command{}, []string{}) 94 | got := buf.String() 95 | 96 | want := "singleURL can not be empty" 97 | 98 | if !strings.Contains(got, want) { 99 | t.Errorf("runActionCardCmd() = %v, want %v", got, want) 100 | } 101 | }) 102 | 103 | t.Run("new client error", func(t *testing.T) { 104 | var buf bytes.Buffer 105 | log.SetOutput(&buf) 106 | defer func() { 107 | log.SetOutput(os.Stderr) 108 | }() 109 | 110 | actionCardVars.Title = "123" 111 | actionCardVars.Text = "123" 112 | actionCardVars.SingleTitle = "123" 113 | actionCardVars.SingleURL = "123" 114 | msg := "new client error" 115 | 116 | monkey.Patch(newClient, func() (*dingtalk.Client, error) { 117 | return nil, errors.New(msg) 118 | }) 119 | defer monkey.Unpatch(newClient) 120 | 121 | runActionCardCmd(&cobra.Command{}, []string{}) 122 | 123 | got := buf.String() 124 | 125 | if !strings.Contains(got, msg) { 126 | t.Errorf("runActionCardCmd() = %v, want %v", got, msg) 127 | } 128 | }) 129 | 130 | t.Run("btnTitles & btnActionURLs different count ", func(t *testing.T) { 131 | var buf bytes.Buffer 132 | log.SetOutput(&buf) 133 | defer func() { 134 | log.SetOutput(os.Stderr) 135 | }() 136 | 137 | actionCardVars.Title = "123" 138 | actionCardVars.Text = "123" 139 | actionCardVars.SingleTitle = "" 140 | actionCardVars.SingleURL = "" 141 | btnTitles = []string{"1"} 142 | btnActionURLs = []string{"1", "2"} 143 | msg := "btnTitles & btnActionURLs count must be equal" 144 | 145 | client := &dingtalk.Client{} 146 | monkey.Patch(newClient, func() (*dingtalk.Client, error) { 147 | return client, nil 148 | }) 149 | defer monkey.Unpatch(newClient) 150 | 151 | runActionCardCmd(&cobra.Command{}, []string{}) 152 | 153 | got := buf.String() 154 | 155 | if !strings.Contains(got, msg) { 156 | t.Errorf("runActionCardCmd() = %v, want %v", got, msg) 157 | } 158 | }) 159 | 160 | t.Run("client send", func(t *testing.T) { 161 | actionCardVars.Title = "123" 162 | actionCardVars.Text = "123" 163 | actionCardVars.SingleTitle = "123" 164 | actionCardVars.SingleURL = "123" 165 | client := &dingtalk.Client{} 166 | 167 | monkey.Patch(newClient, func() (*dingtalk.Client, error) { 168 | return client, nil 169 | }) 170 | defer monkey.Unpatch(newClient) 171 | 172 | runActionCardCmd(&cobra.Command{}, []string{}) 173 | }) 174 | } 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dingtalk 2 | 3 | ![Go](https://github.com/CatchZeng/dingtalk/workflows/Go/badge.svg) 4 | [![codecov](https://codecov.io/gh/CatchZeng/dingtalk/branch/master/graph/badge.svg)](https://codecov.io/gh/CatchZeng/dingtalk) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/CatchZeng/dingtalk)](https://goreportcard.com/report/github.com/CatchZeng/dingtalk) 6 | [![Release](https://img.shields.io/github/release/CatchZeng/dingtalk.svg)](https://github.com/CatchZeng/dingtalk/releases) 7 | [![GoDoc](https://godoc.org/github.com/CatchZeng/dingtalk?status.svg)](https://pkg.go.dev/github.com/CatchZeng/dingtalk?tab=doc) 8 | 9 | [English](https://github.com/CatchZeng/dingtalk/blob/master/READMEEN.md) 10 | 11 | > DingTalk(dingding) 是钉钉机器人的 go 实现。支持 **Docker、Jenkinsfile、命令行模式,module 模式**;支持**加签**安全设置,支持**链式语法**创建消息;支持**文本、链接、Markdown、ActionCard、FeedCard** 消息类型。 12 | 13 | > 注:使用飞书的小伙伴,可以使用[飞书(feishu)版](https://github.com/CatchZeng/feishu)。 14 | 15 | ## 文档 16 | 17 | [钉钉文档](https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq) 18 | 19 | ## 特性 20 | 21 | - [x] 支持[Docker](https://github.com/CatchZeng/dingtalk#Docker) 22 | 23 | - [x] 支持[Jenkinsfile](https://github.com/CatchZeng/dingtalk#Jenkinsfile) 24 | 25 | - [x] 支持[module](https://github.com/CatchZeng/dingtalk#%E4%BD%9C%E4%B8%BA-module) 26 | 27 | - [x] 支持[命令行模式](https://github.com/CatchZeng/dingtalk#%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7) 28 | 29 | - [x] 支持[配置文件](https://github.com/CatchZeng/dingtalk#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6) 30 | 31 | - [x] 支持[环境变量](https://github.com/CatchZeng/dingtalk#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F) 32 | 33 | - [x] 支持加签 34 | 35 | 36 | 37 | - [x] Text 消息 38 | 39 | 40 | 41 | - [x] Link 消息 42 | 43 | 44 | 45 | - [x] Markdown 消息 46 | 47 | 48 | 49 | - [x] ActionCard 消息 50 | 51 | 52 | 53 | 54 | 55 | - [x] FeedCard 消息 56 | 57 | 58 | 59 | ## 安装 60 | 61 | ## Docker 安装 62 | 63 | ```shell 64 | docker pull catchzeng/dingtalk 65 | ``` 66 | 67 | ### 二进制安装 68 | 69 | 到 [releases](https://github.com/CatchZeng/dingtalk/releases/) 下载相应平台的二进制可执行文件,然后加入到 PATH 环境变量即可。 70 | 71 | ### go install 安装 72 | 73 | ```sh 74 | # Go 1.16+ 75 | go install github.com/CatchZeng/dingtalk@v1.5.0 76 | 77 | # Go version < 1.16 78 | go get -u github.com/CatchZeng/dingtalk@v1.5.0 79 | ``` 80 | 81 | ## 使用方法 82 | 83 | ### 配置文件 84 | 85 | 可以在 `$/HOME/.dingtalk` 下创建 `config.yaml` 填入 `access_token` 和 `secret` 默认值。 86 | 87 | ```yaml 88 | access_token: "1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f" 89 | secret: "SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68" 90 | ``` 91 | 92 | ### 环境变量 93 | 94 | ```sh 95 | $ export ACCESS_TOKEN="1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f" 96 | $ export SECRET="SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68" 97 | $ dingtalk link -i "标题" -e "信息" -u "https://makeoptim.com/" -p "https://makeoptim.com/assets/img/logo.png" -a 98 | ``` 99 | 100 | 你也可以为环境变量设置一个**前缀** 101 | 102 | ```sh 103 | $ export DINGTALK_ENV_PREFIX="DINGTALK_" 104 | $ export DINGTALK_ACCESS_TOKEN="1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f" 105 | $ export DINGTALK_SECRET="SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68" 106 | $ dingtalk link -i "标题" -e "信息" -u "https://makeoptim.com/" -p "https://makeoptim.com/assets/img/logo.png" -a 107 | ``` 108 | 109 | ### Docker 110 | 111 | ```shell 112 | docker run catchzeng/dingtalk dingtalk text -t 1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f -s SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68 -c "docker test" 113 | ``` 114 | 115 | ### Jenkinsfile 116 | 117 | ```shell 118 | pipeline { 119 | agent { 120 | docker { 121 | image 'catchzeng/dingtalk:latest' 122 | } 123 | } 124 | environment { 125 | DING_TOKEN = '1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f' 126 | DING_SECRET = 'SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68' 127 | } 128 | stages { 129 | stage('notify') { 130 | steps { 131 | sh 'dingtalk link -t ${DING_TOKEN} -s ${DING_SECRET} -i "标题" -e "信息" -u "https://makeoptim.com/" -p "https://makeoptim.com/assets/img/logo.png" -a' 132 | } 133 | } 134 | } 135 | } 136 | ``` 137 | 138 | ### 作为 module 139 | 140 | ```sh 141 | go get github.com/CatchZeng/dingtalk 142 | ``` 143 | 144 | ```go 145 | package main 146 | 147 | import ( 148 | "log" 149 | 150 | "github.com/CatchZeng/dingtalk/pkg/dingtalk" 151 | ) 152 | 153 | func main() { 154 | accessToken := "1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f" 155 | secret := "SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68" 156 | client := dingtalk.NewClient(accessToken, secret) 157 | 158 | msg := dingtalk.NewTextMessage().SetContent("测试文本&at 某个人").SetAt([]string{"177010xxx60"}, false) 159 | client.Send(msg) 160 | } 161 | ``` 162 | 163 | ### 命令行工具 164 | 165 | #### Demo 166 | 167 | ```shell 168 | $ dingtalk text -t 1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f -s SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68 -c "测试命令行 & at 某个人" -m "177010xxx60","177010xxx61" 169 | ``` 170 | 171 | ```shell 172 | $ dingtalk markdown -D -i "杭州天气" -e '## 杭州天气 @150XXXXXXXX 173 | > 9度,西北风1级,空气良89,相对温度73% 174 | > ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png) 175 | > ###### 10点20分发布 [天气](https://www.dingtalk.com)' -t 1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f -s SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68 176 | 177 | {"msgtype":"markdown","markdown":{"title":"杭州天气","text":"## 杭州天气 @150XXXXXXXX\n \u003e 9度,西北风1级,空气良89,相对温度73%\n \u003e ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png)\n \u003e ###### 10点20分发布 [天气](https://www.dingtalk.com)"},"at":{"atMobiles":[],"isAtAll":false}} 178 | ``` 179 | 180 | > -D 参数:打印发送的消息内容 181 | 182 | #### Help 183 | 184 | ```shell 185 | $ dingtalk -h 186 | dingtalk is a command line tool for DingTalk 187 | 188 | Usage: 189 | dingtalk [command] 190 | 191 | Available Commands: 192 | actionCard send actionCard message with DingTalk robot 193 | feedCard send feedCard message with DingTalk robot 194 | help Help about any command 195 | link send link message with DingTalk robot 196 | markdown send markdown message with DingTalk robot 197 | text send text message with DingTalk robot 198 | version dingtalk version 199 | 200 | Flags: 201 | -t, --access_token string access_token 202 | -m, --atMobiles strings atMobiles 203 | -D, --debug debug 204 | -h, --help help for dingtalk 205 | -a, --isAtAll isAtAll 206 | -s, --secret string secret 207 | 208 | Use "dingtalk [command] --help" for more information about a command. 209 | ``` 210 | 211 | ## Stargazers 212 | 213 | [![Stargazers over time](https://starchart.cc/CatchZeng/dingtalk.svg)](https://starchart.cc/CatchZeng/dingtalk) 214 | -------------------------------------------------------------------------------- /READMEEN.md: -------------------------------------------------------------------------------- 1 | # dingtalk 2 | 3 | ![Go](https://github.com/CatchZeng/dingtalk/workflows/Go/badge.svg) 4 | [![codecov](https://codecov.io/gh/CatchZeng/dingtalk/branch/master/graph/badge.svg)](https://codecov.io/gh/CatchZeng/dingtalk) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/CatchZeng/dingtalk)](https://goreportcard.com/report/github.com/CatchZeng/dingtalk) 6 | [![Release](https://img.shields.io/github/release/CatchZeng/dingtalk.svg)](https://github.com/CatchZeng/dingtalk/releases) 7 | [![GoDoc](https://godoc.org/github.com/CatchZeng/dingtalk?status.svg)](https://pkg.go.dev/github.com/CatchZeng/dingtalk?tab=doc) 8 | 9 | [中文](https://github.com/CatchZeng/dingtalk/blob/master/README.md) 10 | 11 | > DingTalk (dingding) is the go implementation of the DingTalk robot. Support **Docker, Jenkinsfile,command line mode, module mode**, **signature security settings, chain syntax** to create messages, support **text, link, markdown、ActionCard、FeedCard** message types. 12 | 13 | ## Doc 14 | 15 | [ding-doc](https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq) 16 | 17 | ## Feature 18 | 19 | - [x] Support [Docker](https://github.com/CatchZeng/dingtalk#Docker) 20 | 21 | - [x] Support [Jenkinsfile](https://github.com/CatchZeng/dingtalk#Jenkinsfile) 22 | 23 | - [x] Support [module](https://github.com/CatchZeng/dingtalk/blob/master/READMEEN.md#use-as-module) 24 | 25 | - [x] Support [Command Line Mode](https://github.com/CatchZeng/dingtalk/blob/master/READMEEN.md#use-as-command-line-tool) 26 | 27 | - [x] Support [config.yaml](https://github.com/CatchZeng/dingtalk/blob/master/READMEEN.md#config.yaml) 28 | 29 | - [x] Support [environment variables](https://github.com/CatchZeng/dingtalk#environment%20variables) 30 | 31 | - [x] Support sign 32 | 33 | 34 | 35 | - [x] Text message 36 | 37 | 38 | 39 | - [x] Link message 40 | 41 | 42 | 43 | - [x] Markdown message 44 | 45 | 46 | 47 | - [x] ActionCard message 48 | 49 | 50 | 51 | 52 | 53 | - [x] FeedCard message 54 | 55 | 56 | 57 | ## Install 58 | 59 | ## with Docker 60 | 61 | ```shell 62 | docker pull catchzeng/dingtalk 63 | ``` 64 | 65 | ### binary 66 | 67 | Go to [releases](https://github.com/CatchZeng/dingtalk/releases/) to download the binary executable file of the corresponding platform, and then add it to the PATH environment variable. 68 | 69 | ### with go install 70 | 71 | ```sh 72 | # Go 1.16+ 73 | go install github.com/CatchZeng/dingtalk@v1.5.0 74 | 75 | # Go version < 1.16 76 | go get -u github.com/CatchZeng/dingtalk@v1.5.0 77 | ``` 78 | 79 | ## Usage 80 | 81 | ### config.yaml 82 | 83 | You can create `config.yaml` under `$/HOME/.dingtalk` and fill in the default values of `access_token` and `secret`. 84 | 85 | ```yaml 86 | access_token: "1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f" 87 | secret: "SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68" 88 | ``` 89 | 90 | ### environment variables 91 | 92 | ```sh 93 | $ export ACCESS_TOKEN=1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f 94 | $ export SECRET=SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68 95 | $ dingtalk link -i "标题" -e "信息" -u "https://makeoptim.com/" -p "https://makeoptim.com/assets/img/logo.png" -a 96 | ``` 97 | 98 | You can also set a **prefix** for environment variables. 99 | 100 | ```sh 101 | $ export DINGTALK_ENV_PREFIX="DINGTALK_" 102 | $ export DINGTALK_ACCESS_TOKEN="1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f" 103 | $ export DINGTALK_SECRET="SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68" 104 | $ dingtalk link -i "标题" -e "信息" -u "https://makeoptim.com/" -p "https://makeoptim.com/assets/img/logo.png" -a 105 | ``` 106 | 107 | ### Docker 108 | 109 | ```shell 110 | docker run catchzeng/dingtalk dingtalk text -t 1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f -s SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68 -c "docker test" 111 | ``` 112 | 113 | ### Jenkinsfile 114 | 115 | ```shell 116 | pipeline { 117 | agent { 118 | docker { 119 | image 'catchzeng/dingtalk:latest' 120 | } 121 | } 122 | environment { 123 | DING_TOKEN = '1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f' 124 | DING_SECRET = 'SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68' 125 | } 126 | stages { 127 | stage('notify') { 128 | steps { 129 | sh 'dingtalk link -t ${DING_TOKEN} -s ${DING_SECRET} -i "标题" -e "信息" -u "https://makeoptim.com/" -p "https://makeoptim.com/assets/img/logo.png" -a' 130 | } 131 | } 132 | } 133 | } 134 | ``` 135 | 136 | ### Use as module 137 | 138 | ```sh 139 | go get github.com/CatchZeng/dingtalk 140 | ``` 141 | 142 | ```go 143 | package main 144 | 145 | import ( 146 | "log" 147 | 148 | "github.com/CatchZeng/dingtalk/pkg/dingtalk" 149 | ) 150 | 151 | func main() { 152 | accessToken := "1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f" 153 | secret := "SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68" 154 | client := dingtalk.NewClient(accessToken, secret) 155 | 156 | msg := dingtalk.NewTextMessage().SetContent("测试文本&at 某个人").SetAt([]string{"177010xxx60"}, false) 157 | client.Send(msg) 158 | } 159 | ``` 160 | 161 | ### Use as command line tool 162 | 163 | #### Demo 164 | 165 | ```shell 166 | dingtalk text -t 1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f -s SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68 -c "测试命令行 & at 某个人" -m "177010xxx60","177010xxx61" 167 | ``` 168 | 169 | ```shell 170 | $ dingtalk markdown -D -i "杭州天气" -e '## 杭州天气 @150XXXXXXXX 171 | > 9度,西北风1级,空气良89,相对温度73% 172 | > ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png) 173 | > ###### 10点20分发布 [天气](https://www.dingtalk.com)' -t 1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f -s SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68 174 | 175 | {"msgtype":"markdown","markdown":{"title":"杭州天气","text":"## 杭州天气 @150XXXXXXXX\n \u003e 9度,西北风1级,空气良89,相对温度73%\n \u003e ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png)\n \u003e ###### 10点20分发布 [天气](https://www.dingtalk.com)"},"at":{"atMobiles":[],"isAtAll":false}} 176 | ``` 177 | 178 | > -D: print the message content 179 | 180 | #### Help 181 | 182 | ```shell 183 | dingtalk is a command line tool for DingTalk 184 | 185 | Usage: 186 | dingtalk [command] 187 | 188 | Available Commands: 189 | actionCard send actionCard message with DingTalk robot 190 | feedCard send feedCard message with DingTalk robot 191 | help Help about any command 192 | link send link message with DingTalk robot 193 | markdown send markdown message with DingTalk robot 194 | text send text message with DingTalk robot 195 | version dingtalk version 196 | 197 | Flags: 198 | -t, --access_token string access_token 199 | -m, --atMobiles strings atMobiles 200 | -D, --debug debug 201 | -h, --help help for dingtalk 202 | -a, --isAtAll isAtAll 203 | -s, --secret string secret 204 | 205 | Use "dingtalk [command] --help" for more information about a command. 206 | ``` 207 | 208 | ## Stargazers 209 | 210 | [![Stargazers over time](https://starchart.cc/CatchZeng/dingtalk.svg)](https://starchart.cc/CatchZeng/dingtalk) 211 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | bou.ke/monkey v1.0.2 h1:kWcnsrCNUatbxncxR/ThdYqbytgOIArtYWqcQLQzKLI= 2 | bou.ke/monkey v1.0.2/go.mod h1:OqickVX3tNx6t33n1xvtTtu85YN5s6cKwVug+oHMaIA= 3 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 5 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 6 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 7 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 8 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 9 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 10 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 11 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 12 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 13 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 14 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 15 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 16 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 17 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 18 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 19 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 20 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 21 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 22 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 23 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 24 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 25 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 26 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 27 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 28 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 29 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 30 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 31 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 32 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 33 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 34 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 35 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 36 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 37 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 38 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 39 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 40 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 41 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 42 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 43 | github.com/CatchZeng/gutils v0.1.4 h1:Xj1oqB0Rg3F6rymxW1d+ajPl0wHtw2KvDlcquhnCNAc= 44 | github.com/CatchZeng/gutils v0.1.4/go.mod h1:Uz8tJTZDM9XWGTFQt3oIYs+zY3/2fOy0TVQaQTgPtWg= 45 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 46 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 47 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 48 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 49 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 50 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 51 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 52 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 53 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 54 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 55 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 56 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 57 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 58 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 59 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 60 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 61 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 62 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 63 | github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= 64 | github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= 65 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 66 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 67 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 68 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 69 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 70 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 71 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 72 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 73 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 74 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 75 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 76 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 77 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 78 | github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= 79 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 80 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 81 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 82 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 83 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 84 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 85 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 86 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 87 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 88 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 89 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 90 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 91 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 92 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 93 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 94 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 95 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 96 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 97 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 98 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 99 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 100 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 101 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 102 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 103 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 104 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 105 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 106 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 107 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 108 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 109 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 110 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 111 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 112 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 113 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 114 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 115 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 116 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 117 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 118 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 119 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 120 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 121 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 122 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 123 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 124 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 125 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 126 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 127 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 128 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 129 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 130 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 131 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 132 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 133 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 134 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 135 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 136 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 137 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 138 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 139 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 140 | github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= 141 | github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= 142 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 143 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 144 | github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= 145 | github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 146 | github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= 147 | github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 148 | github.com/pelletier/go-toml/v2 v2.0.0-beta.8 h1:dy81yyLYJDwMTifq24Oi/IslOslRrDSb3jwDggjz3Z0= 149 | github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= 150 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 151 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 152 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 153 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 154 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 155 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 156 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 157 | github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= 158 | github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= 159 | github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= 160 | github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 161 | github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= 162 | github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= 163 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 164 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 165 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 166 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 167 | github.com/spf13/viper v1.11.0 h1:7OX/1FS6n7jHD1zGrZTM7WtY13ZELRyosK4k93oPr44= 168 | github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk= 169 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 170 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 171 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 172 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 173 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 174 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 175 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 176 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 177 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 178 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 179 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 180 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 181 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 182 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 183 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 184 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 185 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 186 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 187 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 188 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 189 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 190 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 191 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 192 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 193 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 194 | golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 195 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 196 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 197 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 198 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 199 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 200 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 201 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 202 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 203 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 204 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 205 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 206 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 207 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 208 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 209 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 210 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 211 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 212 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 213 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 214 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 215 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 216 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 217 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 218 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 219 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 220 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 221 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 222 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 223 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 224 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 225 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 226 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 227 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 228 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 229 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 230 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 231 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 232 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 233 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 234 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 235 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 236 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 237 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 238 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 239 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 240 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 241 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 242 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 243 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 244 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 245 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 246 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 247 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 248 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 249 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 250 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 251 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 252 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 253 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 254 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 255 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 256 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 257 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 258 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 259 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 260 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 261 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 262 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 263 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 264 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 265 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 266 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 267 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 268 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 269 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 270 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 271 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 272 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 273 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 274 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 275 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 276 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 277 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 278 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 279 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 280 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 281 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 282 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 283 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 284 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 285 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 286 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 287 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 288 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 289 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 290 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 291 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 292 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 293 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 294 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 295 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 296 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 297 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 298 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 299 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 300 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 301 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 302 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 303 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 304 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 305 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 306 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 307 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 308 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 309 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 310 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 311 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 312 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 313 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= 314 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 315 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 316 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 317 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 318 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 319 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 320 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 321 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 322 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 323 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 324 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 325 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 326 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 327 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 328 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 329 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 330 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 331 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 332 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 333 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 334 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 335 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 336 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 337 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 338 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 339 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 340 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 341 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 342 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 343 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 344 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 345 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 346 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 347 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 348 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 349 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 350 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 351 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 352 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 353 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 354 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 355 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 356 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 357 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 358 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 359 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 360 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 361 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 362 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 363 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 364 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 365 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 366 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 367 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 368 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 369 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 370 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 371 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 372 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 373 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 374 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 375 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 376 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 377 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 378 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 379 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 380 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 381 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 382 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 383 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 384 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 385 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 386 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 387 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 388 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 389 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 390 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 391 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 392 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 393 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 394 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 395 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 396 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 397 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 398 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 399 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 400 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 401 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 402 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 403 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 404 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 405 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 406 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 407 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 408 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 409 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 410 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 411 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 412 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 413 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 414 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 415 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 416 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 417 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 418 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 419 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 420 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 421 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 422 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 423 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 424 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 425 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 426 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 427 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 428 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 429 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 430 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 431 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 432 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 433 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 434 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 435 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 436 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 437 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 438 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 439 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 440 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 441 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 442 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 443 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 444 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 445 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 446 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 447 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 448 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 449 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 450 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 451 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 452 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 453 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 454 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 455 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 456 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 457 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 458 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 459 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 460 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 461 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 462 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 463 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 464 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 465 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 466 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 467 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 468 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 469 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 470 | gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= 471 | gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 472 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 473 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 474 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 475 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 476 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 477 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 478 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 479 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 480 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 481 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 482 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 483 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 484 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 485 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 486 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 487 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 488 | --------------------------------------------------------------------------------