├── .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 |
--------------------------------------------------------------------------------