From f6d8b790ca3c463c3a9483006f458b3fea9b2e46 Mon Sep 17 00:00:00 2001 From: CatchZeng Date: Fri, 21 Feb 2020 20:39:45 +0800 Subject: [PATCH] feat: version 1.0.0 --- .vscode/launch.json | 17 +++++ Makefile | 19 +++++ README.md | 157 ++++++++++++++++++++++++++++++++++++++ READMEEN.md | 155 +++++++++++++++++++++++++++++++++++++ client/client.go | 72 +++++++++++++++++ cmd/link.go | 64 ++++++++++++++++ cmd/markdown.go | 56 ++++++++++++++ cmd/root.go | 45 +++++++++++ cmd/text.go | 49 ++++++++++++ cmd/version.go | 25 ++++++ cmd/version_test.go | 28 +++++++ go.mod | 8 ++ go.sum | 35 +++++++++ main.go | 12 +++ message/message.go | 156 +++++++++++++++++++++++++++++++++++++ security/security.go | 57 ++++++++++++++ security/security_test.go | 49 ++++++++++++ version/version.go | 51 +++++++++++++ version/version_test.go | 52 +++++++++++++ 19 files changed, 1107 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 Makefile create mode 100644 README.md create mode 100644 READMEEN.md create mode 100644 client/client.go create mode 100644 cmd/link.go create mode 100644 cmd/markdown.go create mode 100644 cmd/root.go create mode 100644 cmd/text.go create mode 100644 cmd/version.go create mode 100644 cmd/version_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 message/message.go create mode 100644 security/security.go create mode 100644 security/security_test.go create mode 100644 version/version.go create mode 100644 version/version_test.go diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c23774c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}", + "env": {}, + "args": [] + } + ] +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4e58ae3 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +SHELL := /bin/bash +BASEDIR = $(shell pwd) +export GO111MODULE=on +export GOPROXY=https://goproxy.cn,direct +export GOSUMDB=off + +all: fmt + echo 'make all' +fmt: + gofmt -w . +mod: + go mod tidy +build: + go build -o dingtalk main.go +utest: + go test -coverpkg=./... -coverprofile=coverage.data ./... +help: + @echo "make - compile the source code" + @echo "make clean - remove binary file and vim swp files" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b9c8987 --- /dev/null +++ b/README.md @@ -0,0 +1,157 @@ +# dingtalk + +[English](https://github.com/CatchZeng/dingtalk/blob/master/READMEEN.md) + +> DingTalk(dingding) 是钉钉机器人的 go 实现。支持`加签`安全设置,支持`链式语法`创建消息,支持文本、链接、Markdown消息类型 + +## 文档 + +[钉钉文档](https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq) + +## 特性 + +- [x] 支持加签 + + ![sign](https://dingtalkdoc.oss-cn-beijing.aliyuncs.com/images/0.0.210/1572261283991-f8e35f4d-6997-4a02-9704-843ee8f97464.png) + +- [x] Text 消息 + + ![text](https://img.alicdn.com/tfs/TB1jFpqaRxRMKJjy0FdXXaifFXa-497-133.png) + +- [x] Link 消息 + + ![link](https://dingtalkdoc.oss-cn-beijing.aliyuncs.com/images/0.0.210/1570679827267-6243216b-d1c3-48b7-9b1e-0f0b4211b50b.png) + +- [x] Markdown 消息 + + ![markdown](https://img.alicdn.com/tfs/TB1yL3taUgQMeJjy0FeXXXOEVXa-492-380.png) + +## 安装 + +```shell +go get github.com/CatchZeng/dingtalk +``` + +## 使用方法 + +### 作为 module + +```go +package main + +import ( + "log" + + "github.com/CatchZeng/dingtalk/client" + "github.com/CatchZeng/dingtalk/message" +) + +func main() { + dingTalk := client.DingTalk{ + AccessToken: "1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f", + Secret: "SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68", + } + + msg := message.NewTextMessage().SetContent("测试文本&at 某个人").SetAt([]string{"177010xxx60"}, false) + dingTalk.Send(msg) +} +``` + +### 命令行工具 + +#### Demo + +```shell +dingtalk text -t 1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f -s SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68 -c "测试命令行 & at 某个人" -m ["177010xxx60"] +``` + +#### Help + +- dingtalk + + ```shell + $ dingtalk -h + dingtalk is a command line tool for DingTalk + + Usage: + dingtalk [command] + + Available Commands: + help Help about any command + link send link message with DingTalk robot + markdown send markdown message with DingTalk robot + text send text message with DingTalk robot + + Flags: + -m, --atMobiles stringArray atMobiles + -h, --help help for dingtalk + -a, --isAtAll isAtAll + -s, --secret string secret + -t, --token string access_token + + Use "dingtalk [command] --help" for more information about a command. + ``` + +- text + + ```shell + $ dingtalk text -h + send text message with DingTalk robot + + Usage: + dingtalk text [flags] + + Flags: + -c, --content string content + -h, --help help for text + + Global Flags: + -m, --atMobiles stringArray atMobiles + -a, --isAtAll isAtAll + -s, --secret string secret + -t, --token string access_token + ``` + +- link + + ```shell + $ dingtalk link -h + send link message with DingTalk robot + + Usage: + dingtalk link [flags] + + Flags: + -h, --help help for link + -u, --messageURL string messageURL + -p, --picURL string picURL + -e, --text string text + -i, --title string title + + Global Flags: + -m, --atMobiles stringArray atMobiles + -a, --isAtAll isAtAll + -s, --secret string secret + -t, --token string access_token + ``` + +- markdown + + ```shell + $ dingtalk markdown -h + send markdown message with DingTalk robot + + Usage: + dingtalk markdown [flags] + + Flags: + -h, --help help for markdown + -e, --text string text + -i, --title string title + + Global Flags: + -m, --atMobiles stringArray atMobiles + -a, --isAtAll isAtAll + -s, --secret string secret + -t, --token string access_token + ``` diff --git a/READMEEN.md b/READMEEN.md new file mode 100644 index 0000000..b399fa8 --- /dev/null +++ b/READMEEN.md @@ -0,0 +1,155 @@ +# dingtalk + +[中文](https://github.com/CatchZeng/dingtalk/blob/master/README.md) + +> DingTalk (dingding) is the go implementation of the DingTalk robot. Support `signature` security settings,`chain syntax` to create messages, support text, link, markdown message types. + +## Doc + +[ding-doc](https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq) + +## Feature + +- [x] Support sign + + ![sign](https://dingtalkdoc.oss-cn-beijing.aliyuncs.com/images/0.0.210/1572261283991-f8e35f4d-6997-4a02-9704-843ee8f97464.png) + +- [x] Text message + + ![text](https://img.alicdn.com/tfs/TB1jFpqaRxRMKJjy0FdXXaifFXa-497-133.png) + +- [x] Link message + + ![link](https://dingtalkdoc.oss-cn-beijing.aliyuncs.com/images/0.0.210/1570679827267-6243216b-d1c3-48b7-9b1e-0f0b4211b50b.png) + +- [x] Markdown message + + ![markdown](https://img.alicdn.com/tfs/TB1yL3taUgQMeJjy0FeXXXOEVXa-492-380.png) + +## Usage + +### Use as module + +```shell +go get github.com/CatchZeng/dingtalk +``` + +```go +package main + +import ( + "log" + + "github.com/CatchZeng/dingtalk/client" + "github.com/CatchZeng/dingtalk/message" +) + +func main() { + dingTalk := client.DingTalk{ + AccessToken: "1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f", + Secret: "SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68", + } + + msg := message.NewTextMessage().SetContent("测试文本&at 某个人").SetAt([]string{"177010xxx60"}, false) + dingTalk.Send(msg) +} +``` + +### Use as command line tool + +#### Demo + +```shell +dingtalk text -t 1c53e149ba5de6597cxxxxxx0e901fdxxxxxx80b8ac141e4a75afdc44c85ca4f -s SECb90923e19e58b466481e9e7b7a5bxxxxxx4531axxxxxxad3967fb29f0eae5c68 -c "测试命令行 & at 某个人" -m ["177010xxx60"] +``` + +#### Help + +- dingtalk + + ```shell + $ dingtalk -h + dingtalk is a command line tool for DingTalk + + Usage: + dingtalk [command] + + Available Commands: + help Help about any command + link send link message with DingTalk robot + markdown send markdown message with DingTalk robot + text send text message with DingTalk robot + + Flags: + -m, --atMobiles stringArray atMobiles + -h, --help help for dingtalk + -a, --isAtAll isAtAll + -s, --secret string secret + -t, --token string access_token + + Use "dingtalk [command] --help" for more information about a command. + ``` + +- text + + ```shell + $ dingtalk text -h + send text message with DingTalk robot + + Usage: + dingtalk text [flags] + + Flags: + -c, --content string content + -h, --help help for text + + Global Flags: + -m, --atMobiles stringArray atMobiles + -a, --isAtAll isAtAll + -s, --secret string secret + -t, --token string access_token + ``` + +- link + + ```shell + $ dingtalk link -h + send link message with DingTalk robot + + Usage: + dingtalk link [flags] + + Flags: + -h, --help help for link + -u, --messageURL string messageURL + -p, --picURL string picURL + -e, --text string text + -i, --title string title + + Global Flags: + -m, --atMobiles stringArray atMobiles + -a, --isAtAll isAtAll + -s, --secret string secret + -t, --token string access_token + ``` + +- markdown + + ```shell + $ dingtalk markdown -h + send markdown message with DingTalk robot + + Usage: + dingtalk markdown [flags] + + Flags: + -h, --help help for markdown + -e, --text string text + -i, --title string title + + Global Flags: + -m, --atMobiles stringArray atMobiles + -a, --isAtAll isAtAll + -s, --secret string secret + -t, --token string access_token + ``` diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..675b58b --- /dev/null +++ b/client/client.go @@ -0,0 +1,72 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" + + "github.com/CatchZeng/dingtalk/message" + "github.com/CatchZeng/dingtalk/security" +) + +// DingTalk dingtalk client +type DingTalk struct { + AccessToken string + Secret string +} + +// Response response struct +type Response struct { + ErrMsg string `json:"errmsg"` + ErrCode int64 `json:"errcode"` +} + +const httpTimoutSecond = time.Duration(30) * time.Second + +// Send message +func (d *DingTalk) Send(message message.Message) (Response, error) { + res := Response{} + + reqBytes, err := message.ToByte() + if err != nil { + return res, err + } + + pushURL, err := security.GetDingTalkURL(d.AccessToken, d.Secret) + if err != nil { + return res, err + } + + req, err := http.NewRequest("POST", pushURL, bytes.NewReader(reqBytes)) + if err != nil { + return res, err + } + req.Header.Add("Accept-Charset", "utf8") + req.Header.Add("Content-Type", "application/json") + + client := new(http.Client) + client.Timeout = httpTimoutSecond + resp, err := client.Do(req) + if err != nil { + return res, err + } + + resultByte, err := ioutil.ReadAll(resp.Body) + if err != nil { + return res, err + } + + err = json.Unmarshal(resultByte, &res) + if err != nil { + return res, fmt.Errorf("unmarshal http response body from json error = %v", err) + } + + if res.ErrCode != 0 { + return res, fmt.Errorf("send message to dingtalk error = %s", res.ErrMsg) + } + + return res, nil +} diff --git a/cmd/link.go b/cmd/link.go new file mode 100644 index 0000000..7adfba5 --- /dev/null +++ b/cmd/link.go @@ -0,0 +1,64 @@ +package cmd + +import ( + "github.com/CatchZeng/dingtalk/client" + "github.com/CatchZeng/dingtalk/message" + "github.com/CatchZeng/gutils/log" + "github.com/spf13/cobra" +) + +var linkCmd = &cobra.Command{ + Use: "link", + Short: "send link message with DingTalk robot", + Long: `send link message with DingTalk robot`, + Args: cobra.MinimumNArgs(0), + Run: func(_ *cobra.Command, args []string) { + if !CheckToken() { + log.L(log.Red, "access_token can not be empty") + return + } + + if len(linkVars.title) < 1 { + log.L(log.Red, "title can not be empty") + return + } + + if len(linkVars.text) < 1 { + log.L(log.Red, "text can not be empty") + return + } + + if len(linkVars.messageURL) < 1 { + log.L(log.Red, "messageURL can not be empty") + return + } + + dingTalk := client.DingTalk{ + AccessToken: rootVars.accessToken, + Secret: rootVars.secret, + } + msg := message.NewLinkMessage(). + SetLink(linkVars.title, linkVars.text, linkVars.picURL, linkVars.messageURL) + if _, err := dingTalk.Send(msg); err != nil { + log.L(log.Red, err.Error()) + } + }, +} + +// LinkVars struct +type LinkVars struct { + title string + text string + picURL string + messageURL string +} + +var linkVars LinkVars + +func init() { + rootCmd.AddCommand(linkCmd) + linkCmd.Flags().StringVarP(&linkVars.title, "title", "i", "", "title") + linkCmd.Flags().StringVarP(&linkVars.text, "text", "e", "", "text") + linkCmd.Flags().StringVarP(&linkVars.picURL, "picURL", "p", "", "picURL") + linkCmd.Flags().StringVarP(&linkVars.messageURL, "messageURL", "u", "", "messageURL") +} diff --git a/cmd/markdown.go b/cmd/markdown.go new file mode 100644 index 0000000..32548c5 --- /dev/null +++ b/cmd/markdown.go @@ -0,0 +1,56 @@ +package cmd + +import ( + "github.com/CatchZeng/dingtalk/client" + "github.com/CatchZeng/dingtalk/message" + "github.com/CatchZeng/gutils/log" + "github.com/spf13/cobra" +) + +var markdownCmd = &cobra.Command{ + Use: "markdown", + Short: "send markdown message with DingTalk robot", + Long: `send markdown message with DingTalk robot`, + Args: cobra.MinimumNArgs(0), + Run: func(_ *cobra.Command, args []string) { + if !CheckToken() { + log.L(log.Red, "access_token can not be empty") + return + } + + if len(markdownVars.title) < 1 { + log.L(log.Red, "title can not be empty") + return + } + + if len(markdownVars.text) < 1 { + log.L(log.Red, "text can not be empty") + return + } + + dingTalk := client.DingTalk{ + AccessToken: rootVars.accessToken, + Secret: rootVars.secret, + } + msg := message.NewMarkdownMessage(). + SetMarkdown(markdownVars.title, markdownVars.text). + SetAt(rootVars.atMobiles, rootVars.isAtAll) + if _, err := dingTalk.Send(msg); err != nil { + log.L(log.Red, err.Error()) + } + }, +} + +// MarkdownVars struct +type MarkdownVars struct { + title string + text string +} + +var markdownVars MarkdownVars + +func init() { + rootCmd.AddCommand(markdownCmd) + markdownCmd.Flags().StringVarP(&markdownVars.title, "title", "i", "", "title") + markdownCmd.Flags().StringVarP(&markdownVars.text, "text", "e", "", "text") +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..9adefd6 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,45 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "dingtalk", + Short: "dingtalk is a command line tool for DingTalk", + Long: "dingtalk is a command line tool for DingTalk", +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +// CheckToken check token +func CheckToken() bool { + return len(rootVars.accessToken) > 0 +} + +// RootVars struct +type RootVars struct { + accessToken string + secret string + isAtAll bool + atMobiles []string +} + +var rootVars RootVars + +func init() { + rootCmd.PersistentFlags().StringVarP(&rootVars.accessToken, "token", "t", "", "access_token") + rootCmd.PersistentFlags().StringVarP(&rootVars.secret, "secret", "s", "", "secret") + rootCmd.PersistentFlags().BoolVarP(&rootVars.isAtAll, "isAtAll", "a", false, "isAtAll") + rootCmd.PersistentFlags().StringArrayVarP(&rootVars.atMobiles, "atMobiles", "m", []string{}, "atMobiles") +} diff --git a/cmd/text.go b/cmd/text.go new file mode 100644 index 0000000..b73f268 --- /dev/null +++ b/cmd/text.go @@ -0,0 +1,49 @@ +package cmd + +import ( + "github.com/CatchZeng/dingtalk/client" + "github.com/CatchZeng/dingtalk/message" + "github.com/CatchZeng/gutils/log" + "github.com/spf13/cobra" +) + +var textCmd = &cobra.Command{ + Use: "text", + Short: "send text message with DingTalk robot", + Long: `send text message with DingTalk robot`, + Args: cobra.MinimumNArgs(0), + Run: func(_ *cobra.Command, args []string) { + if !CheckToken() { + log.L(log.Red, "access_token can not be empty") + return + } + + if len(textVars.content) < 1 { + log.L(log.Red, "content can not be empty") + return + } + + dingTalk := client.DingTalk{ + AccessToken: rootVars.accessToken, + Secret: rootVars.secret, + } + msg := message.NewTextMessage(). + SetContent(textVars.content). + SetAt(rootVars.atMobiles, rootVars.isAtAll) + if _, err := dingTalk.Send(msg); err != nil { + log.L(log.Red, err.Error()) + } + }, +} + +// TextVars struct +type TextVars struct { + content string +} + +var textVars TextVars + +func init() { + rootCmd.AddCommand(textCmd) + textCmd.Flags().StringVarP(&textVars.content, "content", "c", "", "content") +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..6dae41e --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,25 @@ +package cmd + +import ( + "log" + + "github.com/CatchZeng/dingtalk/version" + "github.com/spf13/cobra" +) + +// versionCmd represents the version command +var versionCmd = &cobra.Command{ + Use: "version", + Short: "dingtalk version", + Long: `dingtalk version`, + Run: runVersionCmd, +} + +func runVersionCmd(_ *cobra.Command, _ []string) { + version := version.GetVersion() + log.Println(version) +} + +func init() { + rootCmd.AddCommand(versionCmd) +} diff --git a/cmd/version_test.go b/cmd/version_test.go new file mode 100644 index 0000000..b752d6f --- /dev/null +++ b/cmd/version_test.go @@ -0,0 +1,28 @@ +package cmd + +import ( + "bytes" + "log" + "os" + "strings" + "testing" + + "github.com/CatchZeng/dingtalk/version" + "github.com/spf13/cobra" +) + +func Test_runVersionCmd(t *testing.T) { + var buf bytes.Buffer + log.SetOutput(&buf) + defer func() { + log.SetOutput(os.Stderr) + }() + runVersionCmd(&cobra.Command{}, []string{}) + got := buf.String() + + want := version.GetVersion() + + if !strings.Contains(got, want) { + t.Errorf("runVersionCmd() = %v, want %v", got, want) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..13c8513 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/CatchZeng/dingtalk + +go 1.13 + +require ( + github.com/CatchZeng/gutils v0.0.3 + github.com/spf13/cobra v0.0.5 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e27d76c --- /dev/null +++ b/go.sum @@ -0,0 +1,35 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/CatchZeng/gutils v0.0.3 h1:jFiUixFWOz+pMCe+Y3n9vzkIWh0vnf6/XX97HVyFEF0= +github.com/CatchZeng/gutils v0.0.3/go.mod h1:Uz8tJTZDM9XWGTFQt3oIYs+zY3/2fOy0TVQaQTgPtWg= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..2209454 --- /dev/null +++ b/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "log" + + "github.com/CatchZeng/dingtalk/cmd" +) + +func main() { + log.SetFlags(0) + cmd.Execute() +} diff --git a/message/message.go b/message/message.go new file mode 100644 index 0000000..40ed8c0 --- /dev/null +++ b/message/message.go @@ -0,0 +1,156 @@ +package message + +import "encoding/json" + +// Doc:https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq #消息类型及数据格式 + +// MsgType message type enum +type MsgType string + +const ( + // MsgTypeText text + MsgTypeText MsgType = "text" + // MsgTypeMarkdown markdown + MsgTypeMarkdown MsgType = "markdown" + // MsgTypeLink link + MsgTypeLink MsgType = "link" +) + +// Message interface +type Message interface { + ToByte() ([]byte, error) +} + +// TextMessage text message struct +type TextMessage struct { + MsgType MsgType `json:"msgtype"` + Text Text `json:"text"` + At At `json:"at"` +} + +// NewTextMessage new message +func NewTextMessage() *TextMessage { + msg := TextMessage{} + return &msg +} + +// ToByte to byte +func (m *TextMessage) ToByte() ([]byte, error) { + m.MsgType = MsgTypeText + jsonByte, err := json.Marshal(m) + return jsonByte, err +} + +// SetContent set content +func (m *TextMessage) SetContent(content string) *TextMessage { + m.Text = Text{ + Content: content, + } + return m +} + +// SetAt set at +func (m *TextMessage) SetAt(atMobiles []string, isAtAll bool) *TextMessage { + m.At = At{ + AtMobiles: atMobiles, + IsAtAll: isAtAll, + } + return m +} + +// LinkMessage link message struct +type LinkMessage struct { + MsgType MsgType `json:"msgtype"` + Link Link `json:"link"` +} + +// NewLinkMessage new message +func NewLinkMessage() *LinkMessage { + msg := LinkMessage{} + return &msg +} + +// ToByte to byte +func (m *LinkMessage) ToByte() ([]byte, error) { + m.MsgType = MsgTypeLink + jsonByte, err := json.Marshal(m) + return jsonByte, err +} + +// SetLink set link +func (m *LinkMessage) SetLink( + title string, + text string, + picURL string, + messageURL string) *LinkMessage { + m.Link = Link{ + Title: title, + Text: text, + PicURL: picURL, + MessageURL: messageURL, + } + return m +} + +// MarkdownMessage markdown message struct +type MarkdownMessage struct { + MsgType MsgType `json:"msgtype"` + Markdown Markdown `json:"markdown"` + At At `json:"at"` +} + +// NewMarkdownMessage new message +func NewMarkdownMessage() *MarkdownMessage { + msg := MarkdownMessage{} + return &msg +} + +// ToByte to byte +func (m *MarkdownMessage) ToByte() ([]byte, error) { + m.MsgType = MsgTypeMarkdown + jsonByte, err := json.Marshal(m) + return jsonByte, err +} + +// SetMarkdown set markdown +func (m *MarkdownMessage) SetMarkdown(title string, text string) *MarkdownMessage { + m.Markdown = Markdown{ + Title: title, + Text: text, + } + return m +} + +// SetAt set at +func (m *MarkdownMessage) SetAt(atMobiles []string, isAtAll bool) *MarkdownMessage { + m.At = At{ + AtMobiles: atMobiles, + IsAtAll: isAtAll, + } + return m +} + +// Text text struct +type Text struct { + Content string `json:"content"` +} + +// Markdown markdown struct +type Markdown struct { + Title string `json:"title"` + Text string `json:"text"` +} + +// Link link struct +type Link struct { + Title string `json:"title"` + Text string `json:"text"` + PicURL string `json:"picUrl"` + MessageURL string `json:"messageUrl"` +} + +// At at struct +type At struct { + AtMobiles []string `json:"atMobiles"` + IsAtAll bool `json:"isAtAll"` +} diff --git a/security/security.go b/security/security.go new file mode 100644 index 0000000..3a09662 --- /dev/null +++ b/security/security.go @@ -0,0 +1,57 @@ +package security + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "fmt" + "io" + "net/url" + "strconv" + "time" +) + +// https://oapi.dingtalk.com/robot/send?access_token=xxx +const dingTalkOAPI = "oapi.dingtalk.com" + +var dingTalkURL url.URL = url.URL{ + Scheme: "https", + Host: dingTalkOAPI, + Path: "robot/send", +} + +var timestamp = strconv.FormatInt(time.Now().Unix()*1000, 10) + +// GetDingTalkURL get DingTalk URL with accessToken & secret +// If no signature is set, the secret is set to "" +// 如果没有加签,secret 设置为 "" 即可 +func GetDingTalkURL(accessToken string, secret string) (string, error) { + dtu := dingTalkURL + value := url.Values{} + value.Set("access_token", accessToken) + + if secret == "" { + dtu.RawQuery = value.Encode() + return dtu.String(), nil + } + + sign, err := sign(timestamp, secret) + if err != nil { + dtu.RawQuery = value.Encode() + return dtu.String(), err + } + + value.Set("timestamp", timestamp) + value.Set("sign", sign) + dtu.RawQuery = value.Encode() + return dtu.String(), nil +} + +func sign(timestamp string, secret string) (string, error) { + stringToSign := fmt.Sprintf("%s\n%s", timestamp, secret) + h := hmac.New(sha256.New, []byte(secret)) + if _, err := io.WriteString(h, stringToSign); err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil +} diff --git a/security/security_test.go b/security/security_test.go new file mode 100644 index 0000000..c289518 --- /dev/null +++ b/security/security_test.go @@ -0,0 +1,49 @@ +package security + +import "testing" + +func TestGetDingTalkURL(t *testing.T) { + // mock timestamp + timestamp = "1582163555000" + + type args struct { + accessToken string + secret string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "without sign", + args: args{ + accessToken: "1c53e149ba5de6597ca2442f0e901fd86156780b8ac141e4a75afdc44c85ca4f", + }, + want: "https://oapi.dingtalk.com/robot/send?access_token=1c53e149ba5de6597ca2442f0e901fd86156780b8ac141e4a75afdc44c85ca4f", + wantErr: false, + }, + { + name: "with sign", + args: args{ + accessToken: "1c53e149ba5de6597ca2442f0e901fd86156780b8ac141e4a75afdc44c85ca4f", + secret: "SECb90923e19e58b466481e9e7b7a5b4f108a4531abde590ad3967fb29f0eae5c68", + }, + want: "https://oapi.dingtalk.com/robot/send?access_token=1c53e149ba5de6597ca2442f0e901fd86156780b8ac141e4a75afdc44c85ca4f&sign=BQKsG%2BQOCl%2BbYJOLc6pxDHxjVquzlZPWgvRzeN2J5zY%3D×tamp=1582163555000", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetDingTalkURL(tt.args.accessToken, tt.args.secret) + if (err != nil) != tt.wantErr { + t.Errorf("GetDingTalkURL() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GetDingTalkURL() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/version/version.go b/version/version.go new file mode 100644 index 0000000..8b63bb3 --- /dev/null +++ b/version/version.go @@ -0,0 +1,51 @@ +package version + +import ( + "bytes" + "runtime" + "text/template" +) + +var ( + // Version for dingtalk + Version = "1.0.0" + // BuildTime for dingtalk + BuildTime = "2020/02/21" +) + +// Options for dingtalk +type Options struct { + GitCommit string + Version string + BuildTime string + GoVersion string + Os string + Arch string +} + +var versionTemplate = `Version: {{.Version}} +Go version: {{.GoVersion}} +Built: {{.BuildTime}} +OS/Arch: {{.Os}}/{{.Arch}}` + +// DefaultOps default options +var DefaultOps = Options{ + Version: Version, + BuildTime: BuildTime, + GoVersion: runtime.Version(), + Os: runtime.GOOS, + Arch: runtime.GOARCH, +} + +// GetVersion get version string +func GetVersion() string { + return GetVersionWithOps(DefaultOps) +} + +// GetVersionWithOps get version string with versionOptions +func GetVersionWithOps(options Options) string { + var doc bytes.Buffer + template, _ := template.New("version").Parse(versionTemplate) + template.Execute(&doc, options) + return doc.String() +} diff --git a/version/version_test.go b/version/version_test.go new file mode 100644 index 0000000..2203c5e --- /dev/null +++ b/version/version_test.go @@ -0,0 +1,52 @@ +package version + +import ( + "testing" +) + +func TestGetVersionWithOps(t *testing.T) { + tests := []struct { + name string + ops Options + want string + }{ + {name: "test1", + ops: Options{ + Version: "1.0.0", + BuildTime: "20190909", + GoVersion: "1.0", + Os: "Win", + Arch: "X86", + }, + want: `Version: 1.0.0 +Go version: 1.0 +Built: 20190909 +OS/Arch: Win/X86`}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetVersionWithOps(tt.ops); got != tt.want { + t.Errorf("\n%v\n%v", got, tt.want) + } + }) + } +} + +func TestGetVersion(t *testing.T) { + tests := []struct { + name string + want string + }{ + { + name: "get version with default option", + want: GetVersionWithOps(DefaultOps), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetVersion(); got != tt.want { + t.Errorf("GetVersion() = %v, want %v", got, tt.want) + } + }) + } +}