├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── bot.go ├── bot_test.go ├── go.mod ├── go.sum ├── image.go ├── image_test.go ├── markdown.go ├── markdown_test.go ├── media.go ├── media_test.go ├── message.go ├── news.go ├── news_test.go ├── template_card.go ├── template_card_test.go ├── text.go ├── text_test.go └── wxwork.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: 1.19 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Test 28 | run: go test -v ./... 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | 19 | ### JetBrains template 20 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 21 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 22 | 23 | # User-specific stuff 24 | .idea/**/workspace.xml 25 | .idea/**/tasks.xml 26 | .idea/**/usage.statistics.xml 27 | .idea/**/dictionaries 28 | .idea/**/shelf 29 | 30 | # Generated files 31 | .idea/**/contentModel.xml 32 | 33 | # Sensitive or high-churn files 34 | .idea/**/dataSources/ 35 | .idea/**/dataSources.ids 36 | .idea/**/dataSources.local.xml 37 | .idea/**/sqlDataSources.xml 38 | .idea/**/dynamic.xml 39 | .idea/**/uiDesigner.xml 40 | .idea/**/dbnavigator.xml 41 | 42 | # Gradle 43 | .idea/**/gradle.xml 44 | .idea/**/libraries 45 | 46 | # Gradle and Maven with auto-import 47 | # When using Gradle or Maven with auto-import, you should exclude module files, 48 | # since they will be recreated, and may cause churn. Uncomment if using 49 | # auto-import. 50 | # .idea/modules.xml 51 | # .idea/*.iml 52 | # .idea/modules 53 | # *.iml 54 | # *.ipr 55 | 56 | # CMake 57 | cmake-build-*/ 58 | 59 | # Mongo Explorer plugin 60 | .idea/**/mongoSettings.xml 61 | 62 | # File-based project format 63 | *.iws 64 | 65 | # IntelliJ 66 | out/ 67 | 68 | # mpeltonen/sbt-idea plugin 69 | .idea_modules/ 70 | 71 | # JIRA plugin 72 | atlassian-ide-plugin.xml 73 | 74 | # Cursive Clojure plugin 75 | .idea/replstate.xml 76 | 77 | # Crashlytics plugin (for Android Studio and IntelliJ) 78 | com_crashlytics_export_strings.xml 79 | crashlytics.properties 80 | crashlytics-build.properties 81 | fabric.properties 82 | 83 | # Editor-based Rest Client 84 | .idea/httpRequests 85 | 86 | # Android studio 3.1+ serialized cache file 87 | .idea/caches/build_file_checksums.ser 88 | .idea/ 89 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Vimsucks 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. 22 | -------------------------------------------------------------------------------- /bot.go: -------------------------------------------------------------------------------- 1 | package wxworkbot 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | func init() { 14 | } 15 | 16 | const ( 17 | defaultWebHookUrlTemplate = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=%s" 18 | ) 19 | 20 | var ( 21 | ErrUnsupportedMessage = errors.New("尚不支持的消息类型") 22 | ) 23 | 24 | type WxWorkBot struct { 25 | Key string 26 | WebHookUrl string 27 | Client *http.Client 28 | } 29 | 30 | // New 创建一个新的机器人实例 31 | func New(botKey string) *WxWorkBot { 32 | bot := WxWorkBot{ 33 | Key: botKey, 34 | // 直接拼接出接口 URL 35 | WebHookUrl: fmt.Sprintf(defaultWebHookUrlTemplate, botKey), 36 | // 默认 5 秒超时 37 | Client: &http.Client{Timeout: 5 * time.Second}, 38 | } 39 | return &bot 40 | } 41 | 42 | // 发送消息,允许的参数类型:Text、Markdown、Image、News 43 | func (bot *WxWorkBot) Send(msg interface{}) error { 44 | msgBytes, err := marshalMessage(msg) 45 | if err != nil { 46 | return err 47 | } 48 | webHookUrl := bot.WebHookUrl 49 | if len(webHookUrl) == 0 { 50 | webHookUrl = fmt.Sprintf(defaultWebHookUrlTemplate, bot.Key) 51 | } 52 | req, err := http.NewRequest(http.MethodPost, webHookUrl, bytes.NewBuffer(msgBytes)) 53 | if err != nil { 54 | return err 55 | } 56 | req.Header.Set("Content-Type", "application/json") 57 | resp, err := bot.Client.Do(req) 58 | if err != nil { 59 | return err 60 | } 61 | body, _ := ioutil.ReadAll(resp.Body) 62 | defer resp.Body.Close() 63 | var wxWorkResp wxWorkResponse 64 | err = json.Unmarshal(body, &wxWorkResp) 65 | if err != nil { 66 | return err 67 | } 68 | if wxWorkResp.ErrorCode != 0 && wxWorkResp.ErrorMessage != "" { 69 | return errors.New(string(body)) 70 | } 71 | return nil 72 | } 73 | 74 | // 防止 < > 等 HTML 字符在 json.marshal 时被 escape 75 | func marshal(msg interface{}) ([]byte, error) { 76 | buf := bytes.NewBuffer([]byte{}) 77 | jsonEncoder := json.NewEncoder(buf) 78 | jsonEncoder.SetEscapeHTML(false) 79 | jsonEncoder.SetIndent("", "") 80 | err := jsonEncoder.Encode(msg) 81 | if err != nil { 82 | return nil, nil 83 | } 84 | return buf.Bytes(), nil 85 | } 86 | 87 | // 将消息包装成企信接口要求的格式,返回 json bytes 88 | func marshalMessage(msg interface{}) ([]byte, error) { 89 | if text, ok := msg.(Text); ok { 90 | textMsg := textMessage{ 91 | message: message{MsgType: "text"}, 92 | Text: text, 93 | } 94 | return marshal(textMsg) 95 | } 96 | if textMsg, ok := msg.(textMessage); ok { 97 | textMsg.MsgType = "text" 98 | return marshal(textMsg) 99 | } 100 | if markdown, ok := msg.(Markdown); ok { 101 | markdownMsg := markdownMessage{ 102 | message: message{MsgType: "markdown"}, 103 | Markdown: markdown, 104 | } 105 | return marshal(markdownMsg) 106 | } 107 | if markdownMsg, ok := msg.(markdownMessage); ok { 108 | markdownMsg.MsgType = "markdown" 109 | return marshal(markdownMsg) 110 | } 111 | if image, ok := msg.(Image); ok { 112 | imageMsg := imageMessage{ 113 | message: message{MsgType: "image"}, 114 | Image: image, 115 | } 116 | return marshal(imageMsg) 117 | } 118 | if imageMsg, ok := msg.(imageMessage); ok { 119 | imageMsg.MsgType = "image" 120 | return marshal(imageMsg) 121 | } 122 | if news, ok := msg.(News); ok { 123 | newsMsg := newsMessage{ 124 | message: message{MsgType: "news"}, 125 | News: news, 126 | } 127 | return marshal(newsMsg) 128 | } 129 | if newsMsg, ok := msg.(newsMessage); ok { 130 | newsMsg.MsgType = "news" 131 | return marshal(newsMsg) 132 | } 133 | if templateCard, ok := msg.(TemplateCard); ok { 134 | templateCardMsg := templateCardMessage{ 135 | message: message{MsgType: "template_card"}, 136 | TemplateCard: templateCard, 137 | } 138 | return marshal(templateCardMsg) 139 | } 140 | if templateCardMsg, ok := msg.(templateCardMessage); ok { 141 | templateCardMsg.MsgType = "template_card" 142 | return marshal(templateCardMsg) 143 | } 144 | return nil, ErrUnsupportedMessage 145 | } 146 | -------------------------------------------------------------------------------- /bot_test.go: -------------------------------------------------------------------------------- 1 | package wxworkbot 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "net/http" 6 | "os" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func GetTestBotKey() string { 12 | return os.Getenv("WXWORK_BOT_KEY") 13 | } 14 | 15 | func TestMarshalUnsupportedMessage(t *testing.T) { 16 | text := struct { 17 | Foo string 18 | }{ 19 | Foo: "bar", 20 | } 21 | _, err := marshalMessage(text) 22 | assert.Equal(t, ErrUnsupportedMessage, err) 23 | } 24 | 25 | func TestSendWithInvalidBotKey(t *testing.T) { 26 | textMsg := textMessage{ 27 | Text: Text{ 28 | Content: "广州今日天气:29度,大部分多云,降雨概率:60%", 29 | MentionedList: []string{"wangqing", "@all"}, 30 | MentionedMobileList: []string{"13800001111", "@all"}, 31 | }, 32 | } 33 | bot := New("这是一个错误的 BOT KEY") 34 | err := bot.Send(textMsg) 35 | assert.NotNil(t, err) 36 | } 37 | 38 | func TestWithCustomHttpClient(t *testing.T) { 39 | botKey := GetTestBotKey() 40 | if len(botKey) == 0 { 41 | return 42 | } 43 | bot := WxWorkBot{ 44 | Key: botKey, 45 | Client: &http.Client{ 46 | Timeout: 1 * time.Second, 47 | }, 48 | } 49 | err := bot.Send(Text{ 50 | Content: "广州今日天气:29度,大部分多云,降雨概率:60%", 51 | MentionedList: []string{"wangqing", "@all"}, 52 | MentionedMobileList: []string{"13800001111", "@all"}, 53 | }) 54 | assert.Nil(t, err) 55 | } 56 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vimsucks/wxwork-bot-go 2 | 3 | go 1.12 4 | 5 | require github.com/stretchr/testify v1.4.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 7 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 11 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 12 | -------------------------------------------------------------------------------- /image.go: -------------------------------------------------------------------------------- 1 | package wxworkbot 2 | 3 | type imageMessage struct { 4 | message 5 | Image Image `json:"image"` 6 | } 7 | 8 | type Image struct { 9 | Base64 string `json:"base64"` 10 | MD5 string `json:"md5"` 11 | } 12 | -------------------------------------------------------------------------------- /image_test.go: -------------------------------------------------------------------------------- 1 | package wxworkbot 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/stretchr/testify/assert" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestUnmarshalImageMessage(t *testing.T) { 11 | jsonString := ` 12 | { 13 | "msgtype": "image", 14 | "image": { 15 | "base64": "DATA", 16 | "md5": "MD5" 17 | } 18 | } 19 | ` 20 | var imageMsg imageMessage 21 | err := json.Unmarshal([]byte(jsonString), &imageMsg) 22 | assert.Nil(t, err) 23 | assert.Equal(t, imageMsg.MsgType, "image") 24 | assert.Equal(t, imageMsg.Image.Base64, "DATA") 25 | assert.Equal(t, imageMsg.Image.MD5, "MD5") 26 | } 27 | 28 | func TestMarshalImage(t *testing.T) { 29 | image := Image{ 30 | Base64: "DATA", 31 | MD5: "MD5", 32 | } 33 | msgBytes, err := marshalMessage(image) 34 | assert.Nil(t, err) 35 | expected := `{"msgtype":"image","image":{"base64":"DATA","md5":"MD5"}}` 36 | msg := strings.TrimSuffix(string(msgBytes), "\n") 37 | assert.Equal(t, expected, msg) 38 | } 39 | 40 | func TestMarshalImageMessage(t *testing.T) { 41 | imageMsg := imageMessage{ 42 | Image: Image{ 43 | Base64: "DATA", 44 | MD5: "MD5", 45 | }, 46 | } 47 | msgBytes, err := marshalMessage(imageMsg) 48 | assert.Nil(t, err) 49 | expected := `{"msgtype":"image","image":{"base64":"DATA","md5":"MD5"}}` 50 | msg := strings.TrimSuffix(string(msgBytes), "\n") 51 | assert.Equal(t, expected, msg) 52 | } 53 | -------------------------------------------------------------------------------- /markdown.go: -------------------------------------------------------------------------------- 1 | package wxworkbot 2 | 3 | type markdownMessage struct { 4 | message 5 | Markdown Markdown `json:"markdown"` 6 | } 7 | 8 | type Markdown struct { 9 | Content string `json:"content"` 10 | } 11 | -------------------------------------------------------------------------------- /markdown_test.go: -------------------------------------------------------------------------------- 1 | package wxworkbot 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/stretchr/testify/assert" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestUnmarshalMarkdownMessage(t *testing.T) { 11 | jsonString := ` 12 | { 13 | "msgtype": "markdown", 14 | "markdown": { 15 | "content": "233" 16 | } 17 | }` 18 | var markdownMsg markdownMessage 19 | err := json.Unmarshal([]byte(jsonString), &markdownMsg) 20 | assert.Nil(t, err) 21 | assert.Equal(t, markdownMsg.MsgType, "markdown") 22 | assert.Equal(t, markdownMsg.Markdown.Content, 23 | "233") 24 | } 25 | 26 | func TestMarshalMarkdown(t *testing.T) { 27 | markdown := Markdown{ 28 | Content: "233", 29 | } 30 | msgBytes, err := marshalMessage(markdown) 31 | assert.Nil(t, err) 32 | expected := `{"msgtype":"markdown","markdown":{"content":"233"}}` 33 | msg := strings.TrimSuffix(string(msgBytes), "\n") 34 | assert.Equal(t, expected, msg) 35 | } 36 | 37 | func TestMarshalMarkdownMessage(t *testing.T) { 38 | markdownMsg := markdownMessage{ 39 | Markdown: Markdown{ 40 | Content: "233", 41 | }, 42 | } 43 | msgBytes, err := marshalMessage(markdownMsg) 44 | assert.Nil(t, err) 45 | expected := `{"msgtype":"markdown","markdown":{"content":"233"}}` 46 | msg := strings.TrimSuffix(string(msgBytes), "\n") 47 | assert.Equal(t, expected, msg) 48 | } 49 | -------------------------------------------------------------------------------- /media.go: -------------------------------------------------------------------------------- 1 | package wxworkbot 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "mime/multipart" 11 | "net/http" 12 | "net/textproto" 13 | "strings" 14 | ) 15 | 16 | type uploadedMediaResponse struct { 17 | wxWorkResponse 18 | UploadedMedia 19 | } 20 | 21 | type UploadedMedia struct { 22 | Type string `json:"type"'` 23 | MediaID string `json:"media_id"` 24 | CreatedAt string `json:"created_at"` 25 | } 26 | 27 | func uploadApiUrl(key *string) string { 28 | return fmt.Sprintf( 29 | "https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=%s&type=file", 30 | *key, 31 | ) 32 | } 33 | 34 | var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") 35 | 36 | func escapeQuotes(s string) string { 37 | return quoteEscaper.Replace(s) 38 | } 39 | 40 | func (bot *WxWorkBot) UploadMedia(fileName string, fileBytes *[]byte) (*UploadedMedia, error) { 41 | body := &bytes.Buffer{} 42 | writer := multipart.NewWriter(body) 43 | 44 | h := make(textproto.MIMEHeader) 45 | h.Set("Content-Disposition", 46 | fmt.Sprintf(`form-data; name="media"; filename="%s"; filelength=%d`, 47 | escapeQuotes(fileName), len(*fileBytes))) 48 | h.Set("Content-Type", "application/octet-stream") 49 | 50 | part, err := writer.CreatePart(h) 51 | if err != nil { 52 | return nil, err 53 | } 54 | io.Copy(part, bytes.NewReader(*fileBytes)) 55 | writer.Close() 56 | 57 | req, err := http.NewRequest(http.MethodPost, uploadApiUrl(&bot.Key), body) 58 | req.Header.Add("Content-Type", writer.FormDataContentType()) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | resp, err := bot.Client.Do(req) 64 | if err != nil { 65 | return nil, err 66 | } 67 | respBody, _ := ioutil.ReadAll(resp.Body) 68 | defer resp.Body.Close() 69 | var wxWorkResp uploadedMediaResponse 70 | err = json.Unmarshal(respBody, &wxWorkResp) 71 | if err != nil { 72 | return nil, err 73 | } 74 | if wxWorkResp.ErrorCode != 0 && wxWorkResp.ErrorMessage != "" { 75 | return nil, errors.New(string(respBody)) 76 | } 77 | return &wxWorkResp.UploadedMedia, nil 78 | } 79 | -------------------------------------------------------------------------------- /media_test.go: -------------------------------------------------------------------------------- 1 | package wxworkbot 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestUploadMediaAndSendTemplateCardMessage(t *testing.T) { 9 | botKey := GetTestBotKey() 10 | if len(botKey) == 0 { 11 | return 12 | } 13 | bot := New(botKey) 14 | fileBytes := []byte("Here is a string....") 15 | media, err := bot.UploadMedia( 16 | "test.txt", 17 | &fileBytes, 18 | ) 19 | assert.Nil(t, err) 20 | assert.NotNil(t, media) 21 | 22 | err = bot.Send(TemplateCard{ 23 | CardType: TemplateCardTypeText, 24 | Source: &TemplateCardSource{ 25 | IconUrl: strRef("https://wework.qpic.cn/wwpic/252813_jOfDHtcISzuodLa_1629280209/0"), 26 | Desc: strRef("企业微信"), 27 | DescColor: TemplateCardSourceDescColorRed, 28 | }, 29 | MainTitle: TemplateCardMainTitle{ 30 | Title: strRef("下载 test.txt"), 31 | }, 32 | HorizontalContentList: &[]TemplateCardHorizontalContent{ 33 | { 34 | KeyName: "test.txt", 35 | Value: strRef("点击下载"), 36 | Type: TemplateCardHorizontalContentTypeAttachment, 37 | MediaID: &media.MediaID, 38 | }, 39 | }, 40 | CardAction: TemplateCardAction{ 41 | Type: TemplateCardActionTypeUrl, 42 | Url: strRef("https://baidu.com"), 43 | }, 44 | }) 45 | assert.Nil(t, err) 46 | } 47 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package wxworkbot 2 | 3 | type message struct { 4 | MsgType string `json:"msgtype"` 5 | } 6 | -------------------------------------------------------------------------------- /news.go: -------------------------------------------------------------------------------- 1 | package wxworkbot 2 | 3 | type newsMessage struct { 4 | message 5 | News News `json:"news"` 6 | } 7 | 8 | type News struct { 9 | Articles []NewsArticle `json:"articles"` 10 | } 11 | 12 | type NewsArticle struct { 13 | Title string `json:"title"` 14 | Description string `json:"description"` 15 | URL string `json:"url"` 16 | PicURL string `json:"picurl"` 17 | } 18 | -------------------------------------------------------------------------------- /news_test.go: -------------------------------------------------------------------------------- 1 | package wxworkbot 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/stretchr/testify/assert" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestUnmarshalNewsMessage(t *testing.T) { 11 | jsonString := ` 12 | { 13 | "msgtype": "news", 14 | "news": { 15 | "articles" : [ 16 | { 17 | "title" : "中秋节礼品领取", 18 | "description" : "今年中秋节公司有豪礼相送", 19 | "url" : "URL", 20 | "picurl" : "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png" 21 | } 22 | ] 23 | } 24 | } 25 | ` 26 | var newsMsg newsMessage 27 | err := json.Unmarshal([]byte(jsonString), &newsMsg) 28 | assert.Nil(t, err) 29 | assert.Equal(t, newsMsg.MsgType, "news") 30 | assert.NotEmpty(t, newsMsg.News.Articles) 31 | article := newsMsg.News.Articles[0] 32 | assert.Equal(t, article.Title, "中秋节礼品领取") 33 | assert.Equal(t, article.Description, "今年中秋节公司有豪礼相送") 34 | assert.Equal(t, article.URL, "URL") 35 | assert.Equal(t, article.PicURL, "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png") 36 | } 37 | func TestMarshalNews(t *testing.T) { 38 | news := News{ 39 | Articles: []NewsArticle{ 40 | { 41 | Title: "中秋节礼品领取", 42 | Description: "今年中秋节公司有豪礼相送", 43 | URL: "URL", 44 | PicURL: "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png", 45 | }, 46 | }, 47 | } 48 | msgBytes, err := marshalMessage(news) 49 | assert.Nil(t, err) 50 | expected := `{"msgtype":"news","news":{"articles":[{"title":"中秋节礼品领取","description":"今年中秋节公司有豪礼相送","url":"URL","picurl":"http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png"}]}}` 51 | msg := strings.TrimSuffix(string(msgBytes), "\n") 52 | assert.Equal(t, expected, msg) 53 | } 54 | 55 | func TestMarshalNewsMessage(t *testing.T) { 56 | newsMsg := newsMessage{ 57 | News: News{ 58 | Articles: []NewsArticle{ 59 | { 60 | Title: "中秋节礼品领取", 61 | Description: "今年中秋节公司有豪礼相送", 62 | URL: "URL", 63 | PicURL: "http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png", 64 | }, 65 | }}, 66 | } 67 | msgBytes, err := marshalMessage(newsMsg) 68 | assert.Nil(t, err) 69 | expected := `{"msgtype":"news","news":{"articles":[{"title":"中秋节礼品领取","description":"今年中秋节公司有豪礼相送","url":"URL","picurl":"http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png"}]}}` 70 | msg := strings.TrimSuffix(string(msgBytes), "\n") 71 | assert.Equal(t, expected, msg) 72 | } 73 | -------------------------------------------------------------------------------- /template_card.go: -------------------------------------------------------------------------------- 1 | package wxworkbot 2 | 3 | const ( 4 | // TemplateCardTypeText 文本通知类型的模板卡片 5 | TemplateCardTypeText = "text_notice" 6 | // TemplateCardTypeNews 图文展示类型的模板卡片 7 | TemplateCardTypeNews = "news_notice" 8 | ) 9 | 10 | type templateCardMessage struct { 11 | message 12 | TemplateCard TemplateCard `json:"template_card"` 13 | } 14 | 15 | type TemplateCard struct { 16 | CardType string `json:"card_type"` 17 | Source *TemplateCardSource `json:"source"` 18 | MainTitle TemplateCardMainTitle `json:"main_title"` 19 | CardImage *TemplateCardImage `json:"card_image"` 20 | ImageTextArea *TemplateCardImageTextArea `json:"image_text_area"` 21 | EmphasisContent *TemplateCardEmphasisContent `json:"emphasis_content"` 22 | QuoteArea *TemplateCardQuoteArea `json:"quote_area"` 23 | SubTitleText *string `json:"sub_title_text"` 24 | HorizontalContentList *[]TemplateCardHorizontalContent `json:"horizontal_content_list"` 25 | JumpList *[]TemplateCardJump `json:"jump_list"` 26 | CardAction TemplateCardAction `json:"card_action"` 27 | } 28 | 29 | type TemplateCardSourceDescColor int 30 | 31 | // 来源文字的颜色,目前支持:0(默认) 灰色,1 黑色,2 红色,3 绿色 32 | const ( 33 | TemplateCardSourceDescColorGrey = 0 34 | TemplateCardSourceDescColorBlack = 1 35 | TemplateCardSourceDescColorRed = 2 36 | TemplateCardSourceDescColorGreen = 3 37 | ) 38 | 39 | type TemplateCardSource struct { 40 | IconUrl *string `json:"icon_url"` 41 | Desc *string `json:"desc"` 42 | DescColor int `json:"desc_color"` 43 | } 44 | 45 | type TemplateCardMainTitle struct { 46 | Title *string `json:"title"` 47 | Desc *string `json:"desc"` 48 | } 49 | 50 | type TemplateCardEmphasisContent struct { 51 | Title *string `json:"title"` 52 | Desc *string `json:"desc"` 53 | } 54 | 55 | type TemplateCardQuoteAreaType int 56 | 57 | // 引用文献样式区域点击事件,0或不填代表没有点击事件,1 代表跳转url,2 代表跳转小程序 58 | const ( 59 | TemplateCardQuoteAreaTypeNone TemplateCardQuoteAreaType = 0 60 | TemplateCardQuoteAreaTypeUrl TemplateCardQuoteAreaType = 1 61 | TemplateCardQuoteAreaTypeMiniApp TemplateCardQuoteAreaType = 2 62 | ) 63 | 64 | type TemplateCardQuoteArea struct { 65 | Type TemplateCardQuoteAreaType `json:"type"` 66 | Url *string `json:"url"` 67 | AppID *string `json:"appid"` 68 | PagePath *string `json:"pagepath"` 69 | Title *string `json:"title"` 70 | QuoteText *string `json:"quote_text"` 71 | } 72 | 73 | type TemplateCardHorizontalContentType int 74 | 75 | // 链接类型,0或不填代表是普通文本,1 代表跳转url,2 代表下载附件,3 代表@员工 76 | const ( 77 | TemplateCardHorizontalContentTypeText TemplateCardHorizontalContentType = 0 78 | TemplateCardHorizontalContentTypeUrl TemplateCardHorizontalContentType = 1 79 | TemplateCardHorizontalContentTypeAttachment TemplateCardHorizontalContentType = 2 80 | TemplateCardHorizontalContentTypeMention TemplateCardHorizontalContentType = 3 81 | ) 82 | 83 | type TemplateCardHorizontalContent struct { 84 | KeyName string `json:"keyname"` 85 | Value *string `json:"value"` 86 | Type TemplateCardHorizontalContentType `json:"type"` 87 | Url *string `json:"url"` 88 | MediaID *string `json:"media_id"` 89 | UserID *string `json:"userid"` 90 | } 91 | 92 | type TemplateCardJumpType int 93 | 94 | // 跳转链接类型,0或不填代表不是链接,1 代表跳转url,2 代表跳转小程序 95 | const ( 96 | TemplateCardJumpTypeNone TemplateCardJumpType = 0 97 | TemplateCardJumpTypeUrl TemplateCardJumpType = 1 98 | TemplateCardJumpTypeMiniApp TemplateCardJumpType = 2 99 | ) 100 | 101 | type TemplateCardJump struct { 102 | Type TemplateCardJumpType `json:"type"` 103 | Url *string `json:"url"` 104 | Title string `json:"title"` 105 | AppID *string `json:"appid"` 106 | PagePath *string `json:"pagepath"` 107 | } 108 | type TemplateCardActionType int 109 | 110 | // 卡片跳转类型,1 代表跳转url,2 代表打开小程序。text_notice模版卡片中该字段取值范围为[1,2] 111 | const ( 112 | TemplateCardActionTypeUrl TemplateCardActionType = 1 113 | TemplateCardActionTypeMiniApp TemplateCardActionType = 2 114 | ) 115 | 116 | type TemplateCardAction struct { 117 | Type TemplateCardActionType `json:"type"` 118 | Url *string `json:"url"` 119 | AppID *string `json:"appid"` 120 | PagePath *string `json:"pagepath"` 121 | } 122 | 123 | type TemplateCardImage struct { 124 | Url string `json:"url"` 125 | // 图片的宽高比,宽高比要小于2.25,大于1.3,不填该参数默认1.3 126 | AspectRatio *float64 `json:"aspect_ratio"` 127 | } 128 | 129 | type TemplateCardImageTextAreaType int 130 | 131 | const ( 132 | TemplateCardImageTextAreaTypeNone TemplateCardImageTextAreaType = 0 133 | TemplateCardImageTextAreaTypeUrl TemplateCardImageTextAreaType = 1 134 | TemplateCardImageTextAreaTypeMiniApp TemplateCardImageTextAreaType = 2 135 | ) 136 | 137 | type TemplateCardImageTextArea struct { 138 | Type TemplateCardImageTextAreaType `json:"type"` 139 | Url *string `json:"url"` 140 | AppID *string `json:"appid"` 141 | PagePath *string `json:"pagepath"` 142 | Title *string `json:"title"` 143 | Desc *string `json:"desc"` 144 | ImageUrl string `json:"image_url"` 145 | } 146 | -------------------------------------------------------------------------------- /template_card_test.go: -------------------------------------------------------------------------------- 1 | package wxworkbot 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func strRef(s string) *string { 10 | return &s 11 | } 12 | func float64Ref(f float64) *float64 { 13 | return &f 14 | } 15 | 16 | func TestTextTemplateCard(t *testing.T) { 17 | jsonString := `{ 18 | "msgtype":"template_card", 19 | "template_card":{ 20 | "card_type":"text_notice", 21 | "source":{ 22 | "icon_url":"https://wework.qpic.cn/wwpic/252813_jOfDHtcISzuodLa_1629280209/0", 23 | "desc":"企业微信", 24 | "desc_color":0 25 | }, 26 | "main_title":{ 27 | "title":"欢迎使用企业微信", 28 | "desc":"您的好友正在邀请您加入企业微信" 29 | }, 30 | "emphasis_content":{ 31 | "title":"100", 32 | "desc":"数据含义" 33 | }, 34 | "quote_area":{ 35 | "type":1, 36 | "url":"https://work.weixin.qq.com/?from=openApi", 37 | "appid":"APPID", 38 | "pagepath":"PAGEPATH", 39 | "title":"引用文本标题", 40 | "quote_text":"Jack:企业微信真的很好用~\nBalian:超级好的一款软件!" 41 | }, 42 | "sub_title_text":"下载企业微信还能抢红包!", 43 | "horizontal_content_list":[ 44 | { 45 | "keyname":"邀请人", 46 | "value":"张三" 47 | }, 48 | { 49 | "keyname":"企微官网", 50 | "value":"点击访问", 51 | "type":1, 52 | "url":"https://work.weixin.qq.com/?from=openApi" 53 | }, 54 | { 55 | "keyname":"企微下载", 56 | "value":"企业微信.apk", 57 | "type":2, 58 | "media_id":"MEDIAID" 59 | } 60 | ], 61 | "jump_list":[ 62 | { 63 | "type":1, 64 | "url":"https://work.weixin.qq.com/?from=openApi", 65 | "title":"企业微信官网" 66 | }, 67 | { 68 | "type":2, 69 | "appid":"APPID", 70 | "pagepath":"PAGEPATH", 71 | "title":"跳转小程序" 72 | } 73 | ], 74 | "card_action":{ 75 | "type":1, 76 | "url":"https://work.weixin.qq.com/?from=openApi", 77 | "appid":"APPID", 78 | "pagepath":"PAGEPATH" 79 | } 80 | } 81 | } 82 | ` 83 | var templateCardMsg templateCardMessage 84 | err := json.Unmarshal([]byte(jsonString), &templateCardMsg) 85 | templateCard := templateCardMsg.TemplateCard 86 | assert.Nil(t, err) 87 | assert.Equal(t, TemplateCardTypeText, templateCard.CardType) 88 | 89 | assert.Equal(t, "https://wework.qpic.cn/wwpic/252813_jOfDHtcISzuodLa_1629280209/0", *templateCard.Source.IconUrl) 90 | assert.Equal(t, "企业微信", *templateCard.Source.Desc) 91 | assert.Equal(t, TemplateCardSourceDescColorGrey, templateCard.Source.DescColor) 92 | 93 | assert.Equal(t, "欢迎使用企业微信", *templateCard.MainTitle.Title) 94 | assert.Equal(t, "您的好友正在邀请您加入企业微信", *templateCard.MainTitle.Desc) 95 | 96 | assert.Equal(t, "100", *templateCard.EmphasisContent.Title) 97 | assert.Equal(t, "数据含义", *templateCard.EmphasisContent.Desc) 98 | 99 | assert.Equal(t, TemplateCardQuoteAreaTypeUrl, templateCard.QuoteArea.Type) 100 | assert.Equal(t, "https://work.weixin.qq.com/?from=openApi", *templateCard.QuoteArea.Url) 101 | assert.Equal(t, "APPID", *templateCard.QuoteArea.AppID) 102 | assert.Equal(t, "PAGEPATH", *templateCard.QuoteArea.PagePath) 103 | assert.Equal(t, "引用文本标题", *templateCard.QuoteArea.Title) 104 | assert.Equal(t, "Jack:企业微信真的很好用~\nBalian:超级好的一款软件!", *templateCard.QuoteArea.QuoteText) 105 | 106 | assert.Equal(t, "下载企业微信还能抢红包!", *templateCard.SubTitleText) 107 | 108 | assert.Equal(t, "邀请人", (*templateCard.HorizontalContentList)[0].KeyName) 109 | assert.Equal(t, "张三", *(*templateCard.HorizontalContentList)[0].Value) 110 | assert.Equal(t, TemplateCardHorizontalContentTypeText, (*templateCard.HorizontalContentList)[0].Type) 111 | assert.Nil(t, (*templateCard.HorizontalContentList)[0].Url) 112 | assert.Nil(t, (*templateCard.HorizontalContentList)[0].MediaID) 113 | assert.Nil(t, (*templateCard.HorizontalContentList)[0].UserID) 114 | 115 | assert.Equal(t, "企微官网", (*templateCard.HorizontalContentList)[1].KeyName) 116 | assert.Equal(t, "点击访问", *(*templateCard.HorizontalContentList)[1].Value) 117 | assert.Equal(t, TemplateCardHorizontalContentTypeUrl, (*templateCard.HorizontalContentList)[1].Type) 118 | assert.Equal(t, "https://work.weixin.qq.com/?from=openApi", *(*templateCard.HorizontalContentList)[1].Url) 119 | assert.Nil(t, (*templateCard.HorizontalContentList)[1].MediaID) 120 | assert.Nil(t, (*templateCard.HorizontalContentList)[1].UserID) 121 | 122 | assert.Equal(t, "企微下载", (*templateCard.HorizontalContentList)[2].KeyName) 123 | assert.Equal(t, "企业微信.apk", *(*templateCard.HorizontalContentList)[2].Value) 124 | assert.Equal(t, TemplateCardHorizontalContentTypeAttachment, (*templateCard.HorizontalContentList)[2].Type) 125 | assert.Nil(t, (*templateCard.HorizontalContentList)[2].Url) 126 | assert.Equal(t, "MEDIAID", *(*templateCard.HorizontalContentList)[2].MediaID) 127 | assert.Nil(t, (*templateCard.HorizontalContentList)[2].UserID) 128 | 129 | assert.Equal(t, TemplateCardJumpTypeUrl, (*templateCard.JumpList)[0].Type) 130 | assert.Equal(t, "https://work.weixin.qq.com/?from=openApi", *(*templateCard.JumpList)[0].Url) 131 | assert.Equal(t, "企业微信官网", (*templateCard.JumpList)[0].Title) 132 | assert.Nil(t, (*templateCard.JumpList)[0].AppID) 133 | assert.Nil(t, (*templateCard.JumpList)[0].PagePath) 134 | 135 | assert.Equal(t, TemplateCardJumpTypeMiniApp, (*templateCard.JumpList)[1].Type) 136 | assert.Nil(t, (*templateCard.JumpList)[1].Url) 137 | assert.Equal(t, "跳转小程序", (*templateCard.JumpList)[1].Title) 138 | assert.Equal(t, "APPID", *(*templateCard.JumpList)[1].AppID) 139 | assert.Equal(t, "PAGEPATH", *(*templateCard.JumpList)[1].PagePath) 140 | 141 | assert.Equal(t, TemplateCardActionTypeUrl, templateCard.CardAction.Type) 142 | assert.Equal(t, "https://work.weixin.qq.com/?from=openApi", *templateCard.CardAction.Url) 143 | assert.Equal(t, "APPID", *templateCard.CardAction.AppID) 144 | assert.Equal(t, "PAGEPATH", *templateCard.CardAction.PagePath) 145 | } 146 | 147 | func TestSendTextTemplateCardMessage(t *testing.T) { 148 | botKey := GetTestBotKey() 149 | if len(botKey) == 0 { 150 | return 151 | } 152 | bot := New(botKey) 153 | err := bot.Send(TemplateCard{ 154 | CardType: TemplateCardTypeText, 155 | Source: &TemplateCardSource{ 156 | IconUrl: strRef("https://wework.qpic.cn/wwpic/252813_jOfDHtcISzuodLa_1629280209/0"), 157 | Desc: strRef("企业微信"), 158 | DescColor: TemplateCardSourceDescColorGreen, 159 | }, 160 | MainTitle: TemplateCardMainTitle{ 161 | Title: strRef("欢迎使用企业微信"), 162 | Desc: strRef("您的好友正在邀请您加入企业微信"), 163 | }, 164 | EmphasisContent: &TemplateCardEmphasisContent{ 165 | Title: strRef("100"), 166 | Desc: strRef("数据含义"), 167 | }, 168 | QuoteArea: &TemplateCardQuoteArea{ 169 | Type: TemplateCardQuoteAreaTypeUrl, 170 | Url: strRef("https://work.weixin.qq.com/?from=openApi"), 171 | }, 172 | SubTitleText: strRef("下载企业微信还能抢红包!"), 173 | HorizontalContentList: &[]TemplateCardHorizontalContent{ 174 | { 175 | KeyName: "邀请人", 176 | Value: strRef("张三"), 177 | }, 178 | { 179 | KeyName: "企微官网", 180 | Value: strRef("点击访问"), 181 | Type: TemplateCardHorizontalContentTypeUrl, 182 | Url: strRef("https://work.weixin.qq.com/?from=openApi"), 183 | }, 184 | }, 185 | JumpList: &[]TemplateCardJump{ 186 | { 187 | Type: TemplateCardJumpTypeUrl, 188 | Url: strRef("https://work.weixin.qq.com/?from=openAp"), 189 | Title: "企业微信官网", 190 | }, 191 | }, 192 | CardAction: TemplateCardAction{ 193 | Type: TemplateCardActionTypeUrl, 194 | Url: strRef("https://baidu.com"), 195 | }, 196 | }) 197 | assert.Nil(t, err) 198 | } 199 | 200 | func TestNewsTemplateCard(t *testing.T) { 201 | jsonString := ` 202 | { 203 | "msgtype":"template_card", 204 | "template_card":{ 205 | "card_type":"news_notice", 206 | "source":{ 207 | "icon_url":"https://wework.qpic.cn/wwpic/252813_jOfDHtcISzuodLa_1629280209/0", 208 | "desc":"企业微信", 209 | "desc_color":0 210 | }, 211 | "main_title":{ 212 | "title":"欢迎使用企业微信", 213 | "desc":"您的好友正在邀请您加入企业微信" 214 | }, 215 | "card_image":{ 216 | "url":"https://wework.qpic.cn/wwpic/354393_4zpkKXd7SrGMvfg_1629280616/0", 217 | "aspect_ratio":2.25 218 | }, 219 | "image_text_area":{ 220 | "type":1, 221 | "url":"https://work.weixin.qq.com", 222 | "title":"欢迎使用企业微信", 223 | "desc":"您的好友正在邀请您加入企业微信", 224 | "image_url":"https://wework.qpic.cn/wwpic/354393_4zpkKXd7SrGMvfg_1629280616/0" 225 | }, 226 | "quote_area":{ 227 | "type":1, 228 | "url":"https://work.weixin.qq.com/?from=openApi", 229 | "appid":"APPID", 230 | "pagepath":"PAGEPATH", 231 | "title":"引用文本标题", 232 | "quote_text":"Jack:企业微信真的很好用~\nBalian:超级好的一款软件!" 233 | }, 234 | "vertical_content_list":[ 235 | { 236 | "title":"惊喜红包等你来拿", 237 | "desc":"下载企业微信还能抢红包!" 238 | } 239 | ], 240 | "horizontal_content_list":[ 241 | { 242 | "keyname":"邀请人", 243 | "value":"张三" 244 | }, 245 | { 246 | "keyname":"企微官网", 247 | "value":"点击访问", 248 | "type":1, 249 | "url":"https://work.weixin.qq.com/?from=openApi" 250 | }, 251 | { 252 | "keyname":"企微下载", 253 | "value":"企业微信.apk", 254 | "type":2, 255 | "media_id":"MEDIAID" 256 | } 257 | ], 258 | "jump_list":[ 259 | { 260 | "type":1, 261 | "url":"https://work.weixin.qq.com/?from=openApi", 262 | "title":"企业微信官网" 263 | }, 264 | { 265 | "type":2, 266 | "appid":"APPID", 267 | "pagepath":"PAGEPATH", 268 | "title":"跳转小程序" 269 | } 270 | ], 271 | "card_action":{ 272 | "type":1, 273 | "url":"https://work.weixin.qq.com/?from=openApi", 274 | "appid":"APPID", 275 | "pagepath":"PAGEPATH" 276 | } 277 | } 278 | } 279 | 280 | ` 281 | var templateCardMsg templateCardMessage 282 | err := json.Unmarshal([]byte(jsonString), &templateCardMsg) 283 | templateCard := templateCardMsg.TemplateCard 284 | assert.Nil(t, err) 285 | assert.Equal(t, TemplateCardTypeNews, templateCard.CardType) 286 | 287 | assert.Equal(t, "https://wework.qpic.cn/wwpic/252813_jOfDHtcISzuodLa_1629280209/0", *templateCard.Source.IconUrl) 288 | assert.Equal(t, "企业微信", *templateCard.Source.Desc) 289 | assert.Equal(t, TemplateCardSourceDescColorGrey, templateCard.Source.DescColor) 290 | 291 | assert.Equal(t, "欢迎使用企业微信", *templateCard.MainTitle.Title) 292 | assert.Equal(t, "您的好友正在邀请您加入企业微信", *templateCard.MainTitle.Desc) 293 | 294 | assert.Equal(t, "https://wework.qpic.cn/wwpic/354393_4zpkKXd7SrGMvfg_1629280616/0", templateCard.CardImage.Url) 295 | assert.Equal(t, 2.25, *templateCard.CardImage.AspectRatio) 296 | 297 | assert.Equal(t, TemplateCardImageTextAreaTypeUrl, templateCard.ImageTextArea.Type) 298 | assert.Equal(t, "https://work.weixin.qq.com", *templateCard.ImageTextArea.Url) 299 | assert.Equal(t, "欢迎使用企业微信", *templateCard.ImageTextArea.Title) 300 | assert.Equal(t, "您的好友正在邀请您加入企业微信", *templateCard.ImageTextArea.Desc) 301 | 302 | assert.Equal(t, TemplateCardQuoteAreaTypeUrl, templateCard.QuoteArea.Type) 303 | assert.Equal(t, "https://work.weixin.qq.com/?from=openApi", *templateCard.QuoteArea.Url) 304 | assert.Equal(t, "APPID", *templateCard.QuoteArea.AppID) 305 | assert.Equal(t, "PAGEPATH", *templateCard.QuoteArea.PagePath) 306 | assert.Equal(t, "引用文本标题", *templateCard.QuoteArea.Title) 307 | assert.Equal(t, "Jack:企业微信真的很好用~\nBalian:超级好的一款软件!", *templateCard.QuoteArea.QuoteText) 308 | 309 | assert.Equal(t, "邀请人", (*templateCard.HorizontalContentList)[0].KeyName) 310 | assert.Equal(t, "张三", *(*templateCard.HorizontalContentList)[0].Value) 311 | assert.Equal(t, TemplateCardHorizontalContentTypeText, (*templateCard.HorizontalContentList)[0].Type) 312 | assert.Nil(t, (*templateCard.HorizontalContentList)[0].Url) 313 | assert.Nil(t, (*templateCard.HorizontalContentList)[0].MediaID) 314 | assert.Nil(t, (*templateCard.HorizontalContentList)[0].UserID) 315 | 316 | assert.Equal(t, "企微官网", (*templateCard.HorizontalContentList)[1].KeyName) 317 | assert.Equal(t, "点击访问", *(*templateCard.HorizontalContentList)[1].Value) 318 | assert.Equal(t, TemplateCardHorizontalContentTypeUrl, (*templateCard.HorizontalContentList)[1].Type) 319 | assert.Equal(t, "https://work.weixin.qq.com/?from=openApi", *(*templateCard.HorizontalContentList)[1].Url) 320 | assert.Nil(t, (*templateCard.HorizontalContentList)[1].MediaID) 321 | assert.Nil(t, (*templateCard.HorizontalContentList)[1].UserID) 322 | 323 | assert.Equal(t, "企微下载", (*templateCard.HorizontalContentList)[2].KeyName) 324 | assert.Equal(t, "企业微信.apk", *(*templateCard.HorizontalContentList)[2].Value) 325 | assert.Equal(t, TemplateCardHorizontalContentTypeAttachment, (*templateCard.HorizontalContentList)[2].Type) 326 | assert.Nil(t, (*templateCard.HorizontalContentList)[2].Url) 327 | assert.Equal(t, "MEDIAID", *(*templateCard.HorizontalContentList)[2].MediaID) 328 | assert.Nil(t, (*templateCard.HorizontalContentList)[2].UserID) 329 | 330 | assert.Equal(t, TemplateCardJumpTypeUrl, (*templateCard.JumpList)[0].Type) 331 | assert.Equal(t, "https://work.weixin.qq.com/?from=openApi", *(*templateCard.JumpList)[0].Url) 332 | assert.Equal(t, "企业微信官网", (*templateCard.JumpList)[0].Title) 333 | assert.Nil(t, (*templateCard.JumpList)[0].AppID) 334 | assert.Nil(t, (*templateCard.JumpList)[0].PagePath) 335 | 336 | assert.Equal(t, TemplateCardJumpTypeMiniApp, (*templateCard.JumpList)[1].Type) 337 | assert.Nil(t, (*templateCard.JumpList)[1].Url) 338 | assert.Equal(t, "跳转小程序", (*templateCard.JumpList)[1].Title) 339 | assert.Equal(t, "APPID", *(*templateCard.JumpList)[1].AppID) 340 | assert.Equal(t, "PAGEPATH", *(*templateCard.JumpList)[1].PagePath) 341 | 342 | assert.Equal(t, TemplateCardActionTypeUrl, templateCard.CardAction.Type) 343 | assert.Equal(t, "https://work.weixin.qq.com/?from=openApi", *templateCard.CardAction.Url) 344 | assert.Equal(t, "APPID", *templateCard.CardAction.AppID) 345 | assert.Equal(t, "PAGEPATH", *templateCard.CardAction.PagePath) 346 | } 347 | 348 | func TestSendNewsTemplateCardMessage(t *testing.T) { 349 | botKey := GetTestBotKey() 350 | if len(botKey) == 0 { 351 | return 352 | } 353 | bot := New(botKey) 354 | err := bot.Send(TemplateCard{ 355 | CardType: TemplateCardTypeNews, 356 | Source: &TemplateCardSource{ 357 | IconUrl: strRef("https://wework.qpic.cn/wwpic/252813_jOfDHtcISzuodLa_1629280209/0"), 358 | Desc: strRef("企业微信"), 359 | DescColor: TemplateCardSourceDescColorGreen, 360 | }, 361 | MainTitle: TemplateCardMainTitle{ 362 | Title: strRef("欢迎使用企业微信"), 363 | Desc: strRef("您的好友正在邀请您加入企业微信"), 364 | }, 365 | CardImage: &TemplateCardImage{ 366 | Url: "https://wework.qpic.cn/wwpic/354393_4zpkKXd7SrGMvfg_1629280616/0", 367 | AspectRatio: float64Ref(2.25), 368 | }, 369 | ImageTextArea: &TemplateCardImageTextArea{ 370 | Type: TemplateCardImageTextAreaTypeUrl, 371 | Url: strRef("https://work.weixin.qq.com"), 372 | Title: strRef("欢迎使用企业微信"), 373 | Desc: strRef("您的好友正在邀请您加入企业微信"), 374 | ImageUrl: "https://wework.qpic.cn/wwpic/354393_4zpkKXd7SrGMvfg_1629280616/0", 375 | }, 376 | EmphasisContent: &TemplateCardEmphasisContent{ 377 | Title: strRef("100"), 378 | Desc: strRef("数据含义"), 379 | }, 380 | QuoteArea: &TemplateCardQuoteArea{ 381 | Type: TemplateCardQuoteAreaTypeUrl, 382 | Url: strRef("https://work.weixin.qq.com/?from=openApi"), 383 | }, 384 | SubTitleText: strRef("下载企业微信还能抢红包!"), 385 | HorizontalContentList: &[]TemplateCardHorizontalContent{ 386 | { 387 | KeyName: "邀请人", 388 | Value: strRef("张三"), 389 | }, 390 | { 391 | KeyName: "企微官网", 392 | Value: strRef("点击访问"), 393 | Type: TemplateCardHorizontalContentTypeUrl, 394 | Url: strRef("https://work.weixin.qq.com/?from=openApi"), 395 | }, 396 | }, 397 | JumpList: &[]TemplateCardJump{ 398 | { 399 | Type: TemplateCardJumpTypeUrl, 400 | Url: strRef("https://work.weixin.qq.com/?from=openAp"), 401 | Title: "企业微信官网", 402 | }, 403 | }, 404 | CardAction: TemplateCardAction{ 405 | Type: TemplateCardActionTypeUrl, 406 | Url: strRef("https://baidu.com"), 407 | }, 408 | }) 409 | assert.Nil(t, err) 410 | } 411 | -------------------------------------------------------------------------------- /text.go: -------------------------------------------------------------------------------- 1 | package wxworkbot 2 | 3 | type textMessage struct { 4 | message 5 | Text Text `json:"text"` 6 | } 7 | 8 | type Text struct { 9 | Content string `json:"content"` 10 | MentionedList []string `json:"mentioned_list"` 11 | MentionedMobileList []string `json:"mentioned_mobile_list"` 12 | } 13 | -------------------------------------------------------------------------------- /text_test.go: -------------------------------------------------------------------------------- 1 | package wxworkbot 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/stretchr/testify/assert" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestUnmarshalTextMessage(t *testing.T) { 11 | jsonString := ` 12 | { 13 | "msgtype": "text", 14 | "text": { 15 | "content": "广州今日天气:29度,大部分多云,降雨概率:60%", 16 | "mentioned_list":["wangqing","@all"], 17 | "mentioned_mobile_list":["13800001111","@all"] 18 | } 19 | }` 20 | var textMsg textMessage 21 | err := json.Unmarshal([]byte(jsonString), &textMsg) 22 | assert.Nil(t, err) 23 | assert.Equal(t, textMsg.MsgType, "text") 24 | assert.Equal(t, textMsg.Text.Content, "广州今日天气:29度,大部分多云,降雨概率:60%") 25 | assert.Equal(t, textMsg.Text.MentionedList, []string{"wangqing", "@all"}) 26 | assert.Equal(t, textMsg.Text.MentionedMobileList, []string{"13800001111", "@all"}) 27 | } 28 | 29 | func TestMarshalText(t *testing.T) { 30 | text := Text{ 31 | Content: "广州今日天气:29度,大部分多云,降雨概率:60%", 32 | MentionedList: []string{"wangqing", "@all"}, 33 | MentionedMobileList: []string{"13800001111", "@all"}, 34 | } 35 | msgBytes, err := marshalMessage(text) 36 | assert.Nil(t, err) 37 | expected := `{"msgtype":"text","text":{"content":"广州今日天气:29度,大部分多云,降雨概率:60%","mentioned_list":["wangqing","@all"],"mentioned_mobile_list":["13800001111","@all"]}}` 38 | msg := strings.TrimSuffix(string(msgBytes), "\n") 39 | assert.Equal(t, expected, msg) 40 | } 41 | 42 | func TestMarshalTextMessage(t *testing.T) { 43 | textMsg := textMessage{ 44 | Text: Text{ 45 | Content: "广州今日天气:29度,大部分多云,降雨概率:60%", 46 | MentionedList: []string{"wangqing", "@all"}, 47 | MentionedMobileList: []string{"13800001111", "@all"}, 48 | }, 49 | } 50 | msgBytes, err := marshalMessage(textMsg) 51 | assert.Nil(t, err) 52 | expected := `{"msgtype":"text","text":{"content":"广州今日天气:29度,大部分多云,降雨概率:60%","mentioned_list":["wangqing","@all"],"mentioned_mobile_list":["13800001111","@all"]}}` 53 | msg := strings.TrimSuffix(string(msgBytes), "\n") 54 | assert.Equal(t, expected, msg) 55 | } 56 | 57 | func TestSendTextMessage(t *testing.T) { 58 | botKey := GetTestBotKey() 59 | if len(botKey) == 0 { 60 | return 61 | } 62 | bot := New(botKey) 63 | err := bot.Send(Text{ 64 | Content: "测试发送文本消息", 65 | MentionedList: []string{"wangqing", "@all"}, 66 | MentionedMobileList: []string{"13800001111", "@all"}, 67 | }) 68 | assert.Nil(t, err) 69 | } 70 | -------------------------------------------------------------------------------- /wxwork.go: -------------------------------------------------------------------------------- 1 | package wxworkbot 2 | 3 | type wxWorkResponse struct { 4 | ErrorCode int `json:"errcode"` 5 | ErrorMessage string `json:"errmsg"` 6 | } 7 | --------------------------------------------------------------------------------