├── LICENSE
├── README.md
├── api.go
├── bot.go
├── bot_test.go
├── file.go
├── file_test.go
├── go.mod
├── image.go
├── image_test.go
├── markdown.go
├── markdown_test.go
├── md
└── helper.go
├── news.go
├── news_test.go
├── templatecard.go
├── text.go
└── text_test.go
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 electricbubble
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WeCom-Bot-API
2 |
3 | 企业微信-群机器人-API
4 |
5 | ## Installation
6 |
7 | ```shell
8 | go get github.com/electricbubble/wecom-bot-api
9 | ```
10 |
11 | ## Usage
12 |
13 | #### 纯文本消息
14 |
15 | ```go
16 | package main
17 |
18 | import (
19 | botApi "github.com/electricbubble/wecom-bot-api"
20 | )
21 |
22 | func main() {
23 | botKey := "WeCom_Bot_Key" // 只填 key= 后边的内容
24 | phoneNumber := "Phone_Number"
25 | userid := "Userid"
26 |
27 | bot := botApi.NewWeComBot(botKey)
28 |
29 | // 仅发送文本内容
30 | _ = bot.PushTextMessage("hi")
31 |
32 | // 通过群成员 `手机号码` 进行 `@` 提醒
33 | _ = bot.PushTextMessage("hi again", botApi.MentionByMobile(phoneNumber))
34 |
35 | // 通过群成员 `userid` 进行 `@` 提醒
36 | _ = bot.PushTextMessage("hi again", botApi.MentionByUserid(userid))
37 |
38 | // @全部成员
39 | _ = bot.PushTextMessage("hi again",
40 | botApi.MentionAllByMobile(),
41 | // botApi.MentionAllByUserid(),
42 | )
43 | }
44 |
45 | ```
46 |
47 | #### Markdown 消息
48 |
49 | ```go
50 | package main
51 |
52 | import (
53 | "bytes"
54 | botApi "github.com/electricbubble/wecom-bot-api"
55 | "github.com/electricbubble/wecom-bot-api/md"
56 | )
57 |
58 | func main() {
59 | botKey := "WeCom_Bot_Key" // 只填 key= 后边的内容
60 | userid := "Userid"
61 |
62 | bot := botApi.NewWeComBot(botKey)
63 |
64 | content := bytes.NewBufferString(md.Heading(1, "H1"))
65 | content.WriteString("实时新增用户反馈" + md.WarningText("132例") + ",请相关同事注意。\n")
66 | content.WriteString(md.QuoteText("类型:" + md.CommentText("用户反馈")))
67 | content.WriteString(md.QuoteText("普通用户反馈:" + md.CommentText("117例")))
68 | content.WriteString(md.QuoteText("VIP用户反馈:" + md.CommentText("15例")))
69 | // 👆效果等同于👇
70 | /*
71 | # H1
72 | 实时新增用户反馈 132例,请相关同事注意。\n
73 | > 类型:用户反馈
74 | > 普通用户反馈:117例
75 | > VIP用户反馈:15例
76 | */
77 |
78 | // 仅发送 `markdown` 格式的文本
79 | _ = bot.PushMarkdownMessage(content.String())
80 |
81 | // 通过群成员 `userid` 进行 `@` 提醒
82 | _ = bot.PushMarkdownMessage(
83 | md.Heading(2, "H2") + md.Bold("hi") + "\n" + "> again\n" +
84 | md.MentionByUserid(userid),
85 | )
86 | }
87 |
88 | ```
89 |
90 | #### 图片消息
91 |
92 | ```go
93 | package main
94 |
95 | import (
96 | botApi "github.com/electricbubble/wecom-bot-api"
97 | "io/ioutil"
98 | "os"
99 | "path"
100 | )
101 |
102 | func main() {
103 | botKey := "WeCom_Bot_Key" // 只填 key= 后边的内容
104 | bot := botApi.NewWeComBot(botKey)
105 |
106 | userHomeDir, _ := os.UserHomeDir()
107 | filename := path.Join(userHomeDir, "Pictures", "IMG_5246.jpg")
108 | readFile, _ := ioutil.ReadFile(filename)
109 |
110 | // 发送 图片消息
111 | _ = bot.PushImageMessage(readFile)
112 | }
113 |
114 | ```
115 |
116 | #### 图文消息
117 |
118 | ```go
119 | package main
120 |
121 | import (
122 | botApi "github.com/electricbubble/wecom-bot-api"
123 | "os"
124 | )
125 |
126 | func main() {
127 | botKey := "WeCom_Bot_Key" // 只填 key= 后边的内容
128 | bot := botApi.NewWeComBot(botKey)
129 |
130 | article := botApi.NewArticle("中秋节礼品领取", "www.qq.com",
131 | botApi.ArticleDescription("今年中秋节公司有豪礼相送"),
132 | botApi.ArticlePicUrl("http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png"),
133 | )
134 | article2 := botApi.NewArticle("图文标题2", "www.qq.com",
135 | botApi.ArticleDescription("图文描述2"),
136 | )
137 | article3 := botApi.NewArticle("图文标题3", "www.qq.com",
138 | botApi.ArticleDescription("图文描述3"),
139 | )
140 | _, _ = article2, article3
141 |
142 | // 发送 `1条图文` 消息
143 | _ = bot.PushNewsMessage(article)
144 |
145 | // 发送 `多条图文` 消息 (一个图文消息支持 `1~8条` 图文)
146 | _ = bot.PushNewsMessage(article, article2, article3)
147 | }
148 |
149 | ```
150 |
151 | #### [文本通知模版卡片](https://work.weixin.qq.com/api/doc/90000/90136/91770#%E6%96%87%E6%9C%AC%E9%80%9A%E7%9F%A5%E6%A8%A1%E7%89%88%E5%8D%A1%E7%89%87)
152 |
153 | ```go
154 | package main
155 |
156 | import (
157 | botApi "github.com/electricbubble/wecom-bot-api"
158 | "os"
159 | )
160 |
161 | func main() {
162 | botKey := "WeCom_Bot_Key" // 只填 key= 后边的内容
163 | bot := botApi.NewWeComBot(botKey)
164 |
165 | // "media_id":"38BHOWH1SHSCZImMcuPmG2TuJSpYikh0AxznKJYSUJAJaFJvDeRu60NTAuj_IKLoR"
166 | media := botApi.Media{ID: "38BHOWH1SHSCZImMcuPmG2TuJSpYikh0AxznKJYSUJAJaFJvDeRu60NTAuj_IKLoR"}
167 |
168 | rawUrl := "https://work.weixin.qq.com/api/doc/90000/90136/91770#%E6%96%87%E6%9C%AC%E9%80%9A%E7%9F%A5%E6%A8%A1%E7%89%88%E5%8D%A1%E7%89%87"
169 |
170 | // botApi.TemplateCardActionApp("APPID", "/index.html")
171 |
172 | _ = bot.PushTemplateCardTextNotice(
173 | botApi.TemplateCardMainTitle("一级标题", "标题辅助信息"), botApi.TemplateCardActionUrl(rawUrl),
174 | botApi.TemplateCardSource("https://wework.qpic.cn/wwpic/252813_jOfDHtcISzuodLa_1629280209/0", "企业微信"),
175 | botApi.TemplateCardEmphasisContent("关键数据标题", "关键数据描述"),
176 | botApi.TemplateCardSubTitleText("二级普通文本"),
177 | botApi.TemplateCardHorizontalContent("二级标题(text)", botApi.TemplateCardHorizontalContentText("二级文本")),
178 | botApi.TemplateCardHorizontalContent("二级标题(url)", botApi.TemplateCardHorizontalContentUrl(rawUrl, "api地址")),
179 | botApi.TemplateCardHorizontalContent("二级标题(media)", botApi.TemplateCardHorizontalContentMedia("IMG_5246.jpg", media)),
180 | botApi.TemplateCardJump("跳转指引", botApi.TemplateCardJumpUrl(rawUrl)),
181 | botApi.TemplateCardJump("企业微信官网", botApi.TemplateCardJumpUrl("https://work.weixin.qq.com")),
182 | )
183 | }
184 |
185 | ```
186 |
187 | #### [图文展示模版卡片](https://work.weixin.qq.com/api/doc/90000/90136/91770#%E5%9B%BE%E6%96%87%E5%B1%95%E7%A4%BA%E6%A8%A1%E7%89%88%E5%8D%A1%E7%89%87)
188 |
189 | ```go
190 | package main
191 |
192 | import (
193 | botApi "github.com/electricbubble/wecom-bot-api"
194 | "os"
195 | )
196 |
197 | func main() {
198 | botKey := "WeCom_Bot_Key" // 只填 key= 后边的内容
199 | bot := botApi.NewWeComBot(botKey)
200 |
201 | // "media_id":"38BHOWH1SHSCZImMcuPmG2TuJSpYikh0AxznKJYSUJAJaFJvDeRu60NTAuj_IKLoR"
202 | media := botApi.Media{ID: "38BHOWH1SHSCZImMcuPmG2TuJSpYikh0AxznKJYSUJAJaFJvDeRu60NTAuj_IKLoR"}
203 |
204 | rawUrl := "https://work.weixin.qq.com/api/doc/90000/90136/91770#%E5%9B%BE%E6%96%87%E5%B1%95%E7%A4%BA%E6%A8%A1%E7%89%88%E5%8D%A1%E7%89%87"
205 | imgUrl := "https://wework.qpic.cn/wwpic/354393_4zpkKXd7SrGMvfg_1629280616/0"
206 |
207 | // botApi.TemplateCardActionApp("APPID", "/index.html")
208 |
209 | _ = bot.PushTemplateCardNewsNotice(
210 | botApi.TemplateCardMainTitle("一级标题", "标题辅助信息"), botApi.TemplateCardImage(imgUrl), botApi.TemplateCardActionUrl(rawUrl),
211 | botApi.TemplateCardSource("https://wework.qpic.cn/wwpic/252813_jOfDHtcISzuodLa_1629280209/0", "企业微信"),
212 | botApi.TemplateCardVerticalContent("卡片二级标题", "二级普通文本"),
213 | botApi.TemplateCardHorizontalContent("二级标题(text)", botApi.TemplateCardHorizontalContentText("二级文本")),
214 | botApi.TemplateCardHorizontalContent("二级标题(url)", botApi.TemplateCardHorizontalContentUrl(rawUrl, "api地址")),
215 | botApi.TemplateCardHorizontalContent("二级标题(media)", botApi.TemplateCardHorizontalContentMedia("IMG_5246.jpg", media)),
216 | botApi.TemplateCardJump("跳转指引", botApi.TemplateCardJumpUrl(rawUrl)),
217 | botApi.TemplateCardJump("企业微信官网", botApi.TemplateCardJumpUrl("https://work.weixin.qq.com")),
218 | )
219 | }
220 |
221 | ```
222 |
223 | #### 文件消息
224 |
225 | ```go
226 | package main
227 |
228 | import (
229 | botApi "github.com/electricbubble/wecom-bot-api"
230 | "os"
231 | "path"
232 | )
233 |
234 | func main() {
235 | botKey := "WeCom_Bot_Key" // 只填 key= 后边的内容
236 | bot := botApi.NewWeComBot(botKey)
237 |
238 | userHomeDir, _ := os.UserHomeDir()
239 | filename := path.Join(userHomeDir, "Pictures", "IMG_5246.jpg")
240 |
241 | // 必须先通过企业微信上传文件接口, 获取 `media_id` (仅三天内有效)
242 | // https://work.weixin.qq.com/api/doc/90000/90136/91770#%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E6%8E%A5%E5%8F%A3
243 | media, _ := bot.UploadFile(filename)
244 | // 发送 文件消息
245 | _ = bot.PushFileMessage(media)
246 | }
247 |
248 | ```
--------------------------------------------------------------------------------
/api.go:
--------------------------------------------------------------------------------
1 | package wecom_bot_api
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "io/ioutil"
9 | "log"
10 | "mime/multipart"
11 | "net/http"
12 | "net/textproto"
13 | "os"
14 | "path"
15 | "strings"
16 | "time"
17 | )
18 |
19 | // BotSendUrl 企业微信群机器人 webhook
20 | var BotSendUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=%s"
21 |
22 | // UploadMediaUrl 企业微信上传文件接口 `url`, `type` 固定传 `file`
23 | var UploadMediaUrl = "https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=%s&type=file"
24 |
25 | type WeComBot interface {
26 | PushTextMessage(content string, opts ...TextMsgOption) error
27 | PushMarkdownMessage(content string) error
28 | PushImageMessage(img []byte) error
29 | PushNewsMessage(art Article, articles ...Article) error
30 | PushFileMessage(media Media) error
31 | PushTemplateCardTextNotice(mainTitle TemplateCardMainTitleOption, cardAction TemplateCardAction, opts ...TemplateCardOption) error
32 | PushTemplateCardNewsNotice(mainTitle TemplateCardMainTitleOption, cardImage TemplateCardImageOption, cardAction TemplateCardAction, opts ...TemplateCardOption) error
33 | UploadFile(filename string) (media Media, err error)
34 | }
35 |
36 | var HTTPClient = http.DefaultClient
37 |
38 | func newRequest(method string, rawUrl string, rawBody []byte) (request *http.Request, err error) {
39 | debugLog(fmt.Sprintf("--> %s %s\n%s", method, rawUrl, rawBody))
40 |
41 | if request, err = http.NewRequest(method, rawUrl, bytes.NewBuffer(rawBody)); err != nil {
42 | return nil, err
43 | }
44 | request.Header.Set("Content-Type", "application/json;charset=UTF-8")
45 |
46 | return
47 | }
48 |
49 | func newUploadRequest(method string, rawUrl string, filename string) (request *http.Request, err error) {
50 | bodyBuffer := bytes.NewBufferString("")
51 | writer := multipart.NewWriter(bodyBuffer)
52 |
53 | mediaFile, err := os.Open(filename)
54 | if err != nil {
55 | return nil, err
56 | }
57 | defer func() { _ = mediaFile.Close() }()
58 |
59 | h := make(textproto.MIMEHeader)
60 | h.Set("Content-Disposition",
61 | fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
62 | "media", escapeQuotes(path.Base(filename))))
63 | h.Set("Content-Type", "application/octet-stream")
64 | part, err := writer.CreatePart(h)
65 | if err != nil {
66 | return nil, err
67 | }
68 | _, _ = io.Copy(part, mediaFile)
69 |
70 | _ = writer.Close()
71 |
72 | debugLog(fmt.Sprintf("--> %s %s [MEDIA_DATA: Don't display]", method, rawUrl))
73 |
74 | if request, err = http.NewRequest(method, rawUrl, bodyBuffer); err != nil {
75 | return nil, err
76 | }
77 | request.Header.Set("Content-Type", writer.FormDataContentType())
78 |
79 | return
80 | }
81 |
82 | func executeHTTP(req *http.Request) (rawResp []byte, err error) {
83 | start := time.Now()
84 | var resp *http.Response
85 | if resp, err = HTTPClient.Do(req); err != nil {
86 | return nil, err
87 | }
88 | defer func() {
89 | _, _ = io.Copy(ioutil.Discard, resp.Body)
90 | _ = resp.Body.Close()
91 | }()
92 |
93 | rawResp, err = ioutil.ReadAll(resp.Body)
94 | debugLog(fmt.Sprintf("<-- %s %s %d %s\n%s\n", req.Method, req.URL.String(), resp.StatusCode, time.Since(start), rawResp))
95 | if err != nil {
96 | return nil, err
97 | }
98 |
99 | var reply = new(struct {
100 | ErrCode int `json:"errcode"`
101 | ErrMsg string `json:"errmsg"`
102 | })
103 | if err = json.Unmarshal(rawResp, reply); err != nil {
104 | return nil, fmt.Errorf("unknown response: %w\nraw response: %s", err, rawResp)
105 | }
106 | if reply.ErrMsg != "ok" {
107 | return nil, fmt.Errorf("unknown response: %s", rawResp)
108 | }
109 |
110 | return
111 | }
112 |
113 | var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
114 |
115 | func escapeQuotes(s string) string {
116 | return quoteEscaper.Replace(s)
117 | }
118 |
119 | var debugFlag = false
120 |
121 | func SetDebug(debug bool) {
122 | debugFlag = debug
123 | }
124 |
125 | func debugLog(msg string) {
126 | if !debugFlag {
127 | return
128 | }
129 | log.Println("[DEBUG-WeCom-Bot-API] " + msg)
130 | }
131 |
--------------------------------------------------------------------------------
/bot.go:
--------------------------------------------------------------------------------
1 | package wecom_bot_api
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/http"
7 | )
8 |
9 | type weComBot struct {
10 | webhook string
11 | key string
12 | }
13 |
14 | func NewWeComBot(key string) WeComBot {
15 | bot := new(weComBot)
16 | bot.webhook = fmt.Sprintf(BotSendUrl, key)
17 |
18 | bot.key = key
19 | return bot
20 | }
21 |
22 | func (b *weComBot) PushTextMessage(content string, opts ...TextMsgOption) (err error) {
23 | msg := newTextMsg(content, opts...)
24 | return b.pushMsg(msg)
25 | }
26 |
27 | func (b *weComBot) PushMarkdownMessage(content string) (err error) {
28 | msg := newMarkdownMsg(content)
29 | return b.pushMsg(msg)
30 | }
31 |
32 | func (b *weComBot) PushImageMessage(img []byte) (err error) {
33 | msg := newImageMsg(img)
34 | return b.pushMsg(msg)
35 | }
36 |
37 | func (b *weComBot) PushNewsMessage(art Article, articles ...Article) (err error) {
38 | msg := newNewsMsg(art, articles...)
39 | return b.pushMsg(msg)
40 | }
41 |
42 | func (b *weComBot) PushFileMessage(media Media) error {
43 | msg := newFileMsg(media.ID)
44 | return b.pushMsg(msg)
45 | }
46 |
47 | func (b *weComBot) PushTemplateCardTextNotice(mainTitle TemplateCardMainTitleOption, cardAction TemplateCardAction, opts ...TemplateCardOption) error {
48 | tplCardMsg := newTemplateCardMsg(newTemplateCardText(mainTitle, cardAction, opts...))
49 | return b.pushMsg(tplCardMsg)
50 | }
51 |
52 | func (b *weComBot) PushTemplateCardNewsNotice(mainTitle TemplateCardMainTitleOption, cardImage TemplateCardImageOption, cardAction TemplateCardAction, opts ...TemplateCardOption) error {
53 | tplCardMsg := newTemplateCardMsg(newTemplateCardNews(mainTitle, cardImage, cardAction, opts...))
54 | return b.pushMsg(tplCardMsg)
55 | }
56 |
57 | func (b *weComBot) pushMsg(msg interface{}) (err error) {
58 | var bsJSON []byte
59 | if bsJSON, err = json.Marshal(msg); err != nil {
60 | return err
61 | }
62 | var req *http.Request
63 | if req, err = newRequest(http.MethodPost, b.webhook, bsJSON); err != nil {
64 | return err
65 | }
66 | _, err = executeHTTP(req)
67 | return
68 | }
69 |
70 | func (b *weComBot) UploadFile(filename string) (media Media, err error) {
71 | var req *http.Request
72 | if req, err = newUploadRequest(http.MethodPost, fmt.Sprintf(UploadMediaUrl, b.key), filename); err != nil {
73 | return Media{}, err
74 | }
75 | var rawResp []byte = nil
76 | if rawResp, err = executeHTTP(req); err != nil {
77 | return Media{}, err
78 | }
79 |
80 | var reply = new(struct {
81 | ErrCode int `json:"errcode"`
82 | ErrMsg string `json:"errmsg"`
83 | Type string `json:"type"`
84 | MediaId string `json:"media_id"`
85 | CreatedAt string `json:"created_at"`
86 | })
87 | if err = json.Unmarshal(rawResp, reply); err != nil {
88 | return Media{}, fmt.Errorf("unknown response: %w\nraw response: %s", err, rawResp)
89 | }
90 | media = Media{ID: reply.MediaId}
91 | return
92 | }
93 |
--------------------------------------------------------------------------------
/bot_test.go:
--------------------------------------------------------------------------------
1 | package wecom_bot_api
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "github.com/electricbubble/wecom-bot-api/md"
8 | "io/ioutil"
9 | "net/http"
10 | "os"
11 | "path"
12 | "testing"
13 | )
14 |
15 | var botKey = ""
16 | var phoneNumber = ""
17 | var userid = ""
18 | var bot WeComBot
19 |
20 | func setup() {
21 | botKey = os.Getenv("WeCom_Bot_Key")
22 |
23 | phoneNumber = os.Getenv("Phone_Number")
24 | userid = os.Getenv("Userid")
25 |
26 | bot = NewWeComBot(botKey)
27 | }
28 |
29 | func Test_weComBot_PushTextMessage(t *testing.T) {
30 | setup()
31 |
32 | // err := bot.PushTextMessage("广州今日天气:29度,大部分多云,降雨概率:60%",
33 | // MentionByUserid("wangqing"), MentionAllByUserid(),
34 | // MentionByMobile("13800001111"), MentionAllByMobile(),
35 | // )
36 | err := bot.PushTextMessage("hi again",
37 | MentionByMobile(phoneNumber),
38 | MentionByUserid(userid),
39 | )
40 | if err != nil {
41 | t.Fatal(err)
42 | }
43 | }
44 |
45 | func Test_weComBot_PushMarkdownMessage(t *testing.T) {
46 | setup()
47 | SetDebug(true)
48 |
49 | // err := bot.PushMarkdownMessage(md.Heading(1, "H1") + "实时新增用户反馈" + md.WarningText("132例") + ",请相关同事注意。\n" +
50 | // md.QuoteText("类型:"+md.CommentText("用户反馈")) +
51 | // md.QuoteText("普通用户反馈:"+md.CommentText("117例")) +
52 | // md.QuoteText("VIP用户反馈:"+md.CommentText("15例")),
53 | // )
54 | err := bot.PushMarkdownMessage(
55 | md.Heading(1, "H1") + "实时新增用户反馈" + md.WarningText("132例") + ",请相关同事注意。\n" +
56 | md.QuoteText("类型:"+md.CommentText("用户反馈")) +
57 | md.QuoteText("普通用户反馈:"+md.CommentText("117例")) +
58 | md.QuoteText("VIP用户反馈:"+md.CommentText("15例")) +
59 | md.MentionByUserid(userid),
60 | )
61 | if err != nil {
62 | t.Fatal(err)
63 | }
64 | }
65 |
66 | func Test_weComBot_PushImageMessage(t *testing.T) {
67 | setup()
68 | // SetDebug(true)
69 |
70 | userHomeDir, _ := os.UserHomeDir()
71 | filename := path.Join(userHomeDir, "Pictures", "IMG_5246.jpg")
72 |
73 | readFile, err := ioutil.ReadFile(filename)
74 | if err != nil {
75 | t.Fatal(err)
76 | }
77 |
78 | err = bot.PushImageMessage(readFile)
79 | if err != nil {
80 | t.Fatal(err)
81 | }
82 | }
83 |
84 | func Test_weComBot_PushNewsMessage(t *testing.T) {
85 | setup()
86 | // SetDebug(true)
87 |
88 | article := NewArticle("中秋节礼品领取", "www.qq.com",
89 | ArticleDescription("今年中秋节公司有豪礼相送"),
90 | ArticlePicUrl("http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png"),
91 | )
92 | article2 := NewArticle("图文标题2", "www.qq.com",
93 | ArticleDescription("图文描述2"),
94 | )
95 | article3 := NewArticle("图文标题3", "www.qq.com",
96 | ArticleDescription("图文描述3"),
97 | )
98 | _, _ = article2, article3
99 | // err := bot.PushNewsMessage(article)
100 | err := bot.PushNewsMessage(article, article2, article3)
101 | if err != nil {
102 | t.Fatal(err)
103 | }
104 | }
105 |
106 | func Test_weComBot_PushFileMessage(t *testing.T) {
107 | setup()
108 | SetDebug(true)
109 |
110 | userHomeDir, _ := os.UserHomeDir()
111 | filename := path.Join(userHomeDir, "Pictures", "IMG_5246.jpg")
112 |
113 | media, err := bot.UploadFile(filename)
114 | if err != nil {
115 | t.Fatal(err)
116 | }
117 | err = bot.PushFileMessage(media)
118 | if err != nil {
119 | t.Fatal(err)
120 | }
121 | }
122 |
123 | func Test_weComBot_UploadFile(t *testing.T) {
124 | setup()
125 | SetDebug(true)
126 |
127 | userHomeDir, _ := os.UserHomeDir()
128 | filename := path.Join(userHomeDir, "Pictures", "IMG_5246.jpg")
129 |
130 | media, err := bot.UploadFile(filename)
131 | if err != nil {
132 | t.Fatal(err)
133 | }
134 | t.Log(media)
135 | }
136 |
137 | func TestTemplateCard(t *testing.T) {
138 | setup()
139 |
140 | var (
141 | req *http.Request
142 | err error
143 | )
144 |
145 | msg := bytes.NewBufferString(`{
146 | "msgtype":"template_card",
147 | "template_card":{
148 | "card_type":"text_notice",
149 | "source":{
150 | "icon_url":"https://wework.qpic.cn/wwpic/252813_jOfDHtcISzuodLa_1629280209/0",
151 | "desc":"企业微信"
152 | },
153 | "main_title":{
154 | "title":"欢迎使用企业微信",
155 | "desc":"您的好友正在邀请您加入企业微信"
156 | },
157 | "emphasis_content":{
158 | "title":"100",
159 | "desc":"数据含义"
160 | },
161 | "sub_title_text":"下载企业微信还能抢红包!",
162 | "horizontal_content_list":[
163 | {
164 | "keyname":"邀请人",
165 | "value":"张三"
166 | },
167 | {
168 | "keyname":"企微官网",
169 | "value":"点击访问",
170 | "type":1,
171 | "url":"https://work.weixin.qq.com/?from=openApi"
172 | }
173 | ],
174 | "jump_list":[
175 | {
176 | "type":1,
177 | "url":"https://work.weixin.qq.com/?from=openApi",
178 | "title":"企业微信官网"
179 | }
180 | ],
181 | "card_action":{
182 | "type":1,
183 | "url":"https://work.weixin.qq.com/?from=openApi",
184 | "appid":"APPID",
185 | "pagepath":"PAGEPATH"
186 | }
187 | }
188 | }
189 | `,
190 | ).Bytes()
191 |
192 | rawUrl := "https://work.weixin.qq.com/api/doc/90000/90136/91770#%E6%96%87%E6%9C%AC%E9%80%9A%E7%9F%A5%E6%A8%A1%E7%89%88%E5%8D%A1%E7%89%87"
193 |
194 | tplCardText := newTemplateCardText(
195 | TemplateCardMainTitle("一级标题", "标题辅助信息"), TemplateCardActionUrl(rawUrl),
196 | // TemplateCardSource("https://wework.qpic.cn/wwpic/252813_jOfDHtcISzuodLa_1629280209/0", "企业微信"),
197 | TemplateCardEmphasisContent("关键数据标题", "关键数据描述"),
198 | TemplateCardSubTitleText("二级普通文本"),
199 | TemplateCardHorizontalContent("二级标题(text)", TemplateCardHorizontalContentText("二级文本")),
200 | TemplateCardHorizontalContent("二级标题(url)", TemplateCardHorizontalContentUrl(rawUrl, "api地址")),
201 | TemplateCardJump("跳转指引", TemplateCardJumpUrl(rawUrl)),
202 | TemplateCardJump("企业微信官网", TemplateCardJumpUrl("https://work.weixin.qq.com")),
203 | )
204 | tplCardMsg := newTemplateCardMsg(tplCardText)
205 |
206 | rawUrl = "https://work.weixin.qq.com/api/doc/90000/90136/91770#%E5%9B%BE%E6%96%87%E5%B1%95%E7%A4%BA%E6%A8%A1%E7%89%88%E5%8D%A1%E7%89%87"
207 | imgUrl := "https://wework.qpic.cn/wwpic/354393_4zpkKXd7SrGMvfg_1629280616/0"
208 | tplCardNews := newTemplateCardNews(
209 | TemplateCardMainTitle("一级标题", "标题辅助信息"), TemplateCardImage(imgUrl), TemplateCardActionUrl(rawUrl),
210 | TemplateCardSource("https://wework.qpic.cn/wwpic/252813_jOfDHtcISzuodLa_1629280209/0", "企业微信"),
211 | TemplateCardVerticalContent("卡片二级标题", "二级普通文本"),
212 | TemplateCardHorizontalContent("二级标题(text)", TemplateCardHorizontalContentText("二级文本")),
213 | TemplateCardHorizontalContent("二级标题(url)", TemplateCardHorizontalContentUrl(rawUrl, "api地址")),
214 | TemplateCardJump("跳转指引", TemplateCardJumpUrl(rawUrl)),
215 | TemplateCardJump("企业微信官网", TemplateCardJumpUrl("https://work.weixin.qq.com")),
216 | )
217 | tplCardMsg = newTemplateCardMsg(tplCardNews)
218 |
219 | bsData, err := json.MarshalIndent(tplCardMsg, "", " ")
220 | if err != nil {
221 | t.Fatal(err)
222 | }
223 |
224 | msg = bsData
225 |
226 | fmt.Println(string(msg))
227 |
228 | // return
229 |
230 | if req, err = newRequest(http.MethodPost,
231 | fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=%s", botKey),
232 | msg,
233 | ); err != nil {
234 | t.Fatal(err)
235 | }
236 | rawResp, err := executeHTTP(req)
237 | if err != nil {
238 | t.Fatal(err)
239 | }
240 |
241 | t.Log(string(rawResp))
242 | }
243 |
244 | func Test_weComBot_PushTemplateCardTextNotice(t *testing.T) {
245 | setup()
246 | SetDebug(true)
247 |
248 | // userHomeDir, _ := os.UserHomeDir()
249 | // filename := path.Join(userHomeDir, "Pictures", "IMG_5246.jpg")
250 | //
251 | // media, err := bot.UploadFile(filename)
252 | // if err != nil {
253 | // t.Fatal(err)
254 | // }
255 |
256 | // "media_id":"38BHOWH1SHSCZImMcuPmG2TuJSpYikh0AxznKJYSUJAJaFJvDeRu60NTAuj_IKLoR"
257 | media := Media{ID: "38BHOWH1SHSCZImMcuPmG2TuJSpYikh0AxznKJYSUJAJaFJvDeRu60NTAuj_IKLoR"}
258 |
259 | rawUrl := "https://work.weixin.qq.com/api/doc/90000/90136/91770#%E6%96%87%E6%9C%AC%E9%80%9A%E7%9F%A5%E6%A8%A1%E7%89%88%E5%8D%A1%E7%89%87"
260 |
261 | err := bot.PushTemplateCardTextNotice(
262 | TemplateCardMainTitle("一级标题", "标题辅助信息"), TemplateCardActionUrl(rawUrl),
263 | TemplateCardSource("https://wework.qpic.cn/wwpic/252813_jOfDHtcISzuodLa_1629280209/0", "企业微信"),
264 | TemplateCardEmphasisContent("关键数据标题", "关键数据描述"),
265 | TemplateCardSubTitleText("二级普通文本"),
266 | TemplateCardHorizontalContent("二级标题(text)", TemplateCardHorizontalContentText("二级文本")),
267 | TemplateCardHorizontalContent("二级标题(url)", TemplateCardHorizontalContentUrl(rawUrl, "api地址")),
268 | TemplateCardHorizontalContent("二级标题(media)", TemplateCardHorizontalContentMedia("IMG_5246.jpg", media)),
269 | TemplateCardJump("跳转指引", TemplateCardJumpUrl(rawUrl)),
270 | TemplateCardJump("企业微信官网", TemplateCardJumpUrl("https://work.weixin.qq.com")),
271 | )
272 | if err != nil {
273 | t.Fatal(err)
274 | }
275 | }
276 |
277 | func Test_weComBot_PushTemplateCardNewsNotice(t *testing.T) {
278 | setup()
279 | SetDebug(true)
280 |
281 | // "media_id":"38BHOWH1SHSCZImMcuPmG2TuJSpYikh0AxznKJYSUJAJaFJvDeRu60NTAuj_IKLoR"
282 | media := Media{ID: "38BHOWH1SHSCZImMcuPmG2TuJSpYikh0AxznKJYSUJAJaFJvDeRu60NTAuj_IKLoR"}
283 |
284 | rawUrl := "https://work.weixin.qq.com/api/doc/90000/90136/91770#%E5%9B%BE%E6%96%87%E5%B1%95%E7%A4%BA%E6%A8%A1%E7%89%88%E5%8D%A1%E7%89%87"
285 | imgUrl := "https://wework.qpic.cn/wwpic/354393_4zpkKXd7SrGMvfg_1629280616/0"
286 |
287 | err := bot.PushTemplateCardNewsNotice(
288 | TemplateCardMainTitle("一级标题", "标题辅助信息"), TemplateCardImage(imgUrl), TemplateCardActionUrl(rawUrl),
289 | TemplateCardSource("https://wework.qpic.cn/wwpic/252813_jOfDHtcISzuodLa_1629280209/0", "企业微信"),
290 | TemplateCardVerticalContent("卡片二级标题", "二级普通文本"),
291 | TemplateCardHorizontalContent("二级标题(text)", TemplateCardHorizontalContentText("二级文本")),
292 | TemplateCardHorizontalContent("二级标题(url)", TemplateCardHorizontalContentUrl(rawUrl, "api地址")),
293 | TemplateCardHorizontalContent("二级标题(media)", TemplateCardHorizontalContentMedia("IMG_5246.jpg", media)),
294 | TemplateCardJump("跳转指引", TemplateCardJumpUrl(rawUrl)),
295 | TemplateCardJump("企业微信官网", TemplateCardJumpUrl("https://work.weixin.qq.com")),
296 | )
297 | if err != nil {
298 | t.Fatal(err)
299 | }
300 | }
301 |
--------------------------------------------------------------------------------
/file.go:
--------------------------------------------------------------------------------
1 | package wecom_bot_api
2 |
3 | type fileMsg struct {
4 | // 消息类型, 固定为 `file`
5 | MsgType string `json:"msgtype"`
6 | File Media `json:"file"`
7 | }
8 |
9 | type Media struct {
10 | // 文件id, 通过文件上传接口获取
11 | ID string `json:"media_id"`
12 | }
13 |
14 | func newFileMsg(mediaID string) fileMsg {
15 | msg := fileMsg{
16 | MsgType: "file",
17 | File: Media{ID: mediaID},
18 | }
19 | return msg
20 | }
21 |
--------------------------------------------------------------------------------
/file_test.go:
--------------------------------------------------------------------------------
1 | package wecom_bot_api
2 |
3 | import (
4 | "encoding/json"
5 | "testing"
6 | )
7 |
8 | func Test_newFileMsg(t *testing.T) {
9 | msg := newFileMsg("3a8asd892asd8asd")
10 |
11 | marshal, err := json.Marshal(&msg)
12 | if err != nil {
13 | t.Fatal(err)
14 | }
15 | t.Log(string(marshal))
16 | }
17 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/electricbubble/wecom-bot-api
2 |
3 | go 1.15
4 |
--------------------------------------------------------------------------------
/image.go:
--------------------------------------------------------------------------------
1 | package wecom_bot_api
2 |
3 | import (
4 | "crypto/md5"
5 | "encoding/base64"
6 | "encoding/hex"
7 | )
8 |
9 | type imageMsg struct {
10 | // 消息类型, 固定为 `image`
11 | MsgType string `json:"msgtype"`
12 | Image imageData `json:"image"`
13 | }
14 |
15 | type imageData struct {
16 | // 图片内容的 `base64` 编码
17 | Base64 string `json:"base64"`
18 |
19 | // 图片内容( `base64` 编码前)的 `md5` 值
20 | Md5 string `json:"md5"`
21 | }
22 |
23 | // 注:图片(base64编码前)最大不能超过2M,支持JPG,PNG格式
24 |
25 | func newImageMsg(img []byte) imageMsg {
26 | encodeToString := base64.StdEncoding.EncodeToString(img)
27 | hash := md5.New()
28 | hash.Write(img)
29 | toString := hex.EncodeToString(hash.Sum(nil))
30 |
31 | msg := imageMsg{
32 | MsgType: "image",
33 | Image: imageData{Base64: encodeToString, Md5: toString},
34 | }
35 | return msg
36 | }
37 |
--------------------------------------------------------------------------------
/image_test.go:
--------------------------------------------------------------------------------
1 | package wecom_bot_api
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "os"
7 | "path"
8 | "testing"
9 | )
10 |
11 | func Test_newImageMsg(t *testing.T) {
12 | userHomeDir, _ := os.UserHomeDir()
13 | filename := path.Join(userHomeDir, "Pictures", "IMG_5246.jpg")
14 |
15 | readFile, err := ioutil.ReadFile(filename)
16 | if err != nil {
17 | t.Fatal(err)
18 | }
19 |
20 | msg := newImageMsg(readFile)
21 |
22 | marshal, err := json.Marshal(&msg)
23 | if err != nil {
24 | t.Fatal(err)
25 | }
26 | t.Log(string(marshal))
27 | }
28 |
--------------------------------------------------------------------------------
/markdown.go:
--------------------------------------------------------------------------------
1 | package wecom_bot_api
2 |
3 | type markdownMsg struct {
4 | // 消息类型, 固定为 `markdown`
5 | MsgType string `json:"msgtype"`
6 | Markdown markdownData `json:"markdown"`
7 | }
8 |
9 | type markdownData struct {
10 | // `markdown` 内容, 最长不超过 4096 个字节, 必须是 `utf8` 编码
11 | Content string `json:"content"`
12 | }
13 |
14 | func newMarkdownMsg(content string) markdownMsg {
15 | msg := markdownMsg{
16 | MsgType: "markdown",
17 | Markdown: markdownData{Content: content},
18 | }
19 | return msg
20 | }
21 |
--------------------------------------------------------------------------------
/markdown_test.go:
--------------------------------------------------------------------------------
1 | package wecom_bot_api
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/electricbubble/wecom-bot-api/md"
6 | "testing"
7 | )
8 |
9 | func Test_newMarkdownMessage(t *testing.T) {
10 | msg := newMarkdownMsg("实时新增用户反馈" + md.WarningText("132例") + ",请相关同事注意。\n" +
11 | md.QuoteText("类型:"+md.CommentText("用户反馈")) +
12 | md.QuoteText("普通用户反馈:"+md.CommentText("117例")) +
13 | md.QuoteText("VIP用户反馈:"+md.CommentText("15例")),
14 | )
15 |
16 | marshal, err := json.Marshal(&msg)
17 | if err != nil {
18 | t.Fatal(err)
19 | }
20 | t.Log(string(marshal))
21 | }
22 |
--------------------------------------------------------------------------------
/md/helper.go:
--------------------------------------------------------------------------------
1 | package md
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | func Heading(lv int, s string) string {
9 | if lv <= 0 {
10 | lv = 1
11 | }
12 | if lv > 6 {
13 | lv = 6
14 | }
15 | return fmt.Sprintf("%s %s\n", strings.Repeat("#", lv), s)
16 | }
17 |
18 | func Bold(s string) string {
19 | return fmt.Sprintf("**%s**", s)
20 | }
21 |
22 | func Link(text, Url string) string {
23 | return fmt.Sprintf("[%s](%s)", text, Url)
24 | }
25 |
26 | func QuoteText(s string) string {
27 | return fmt.Sprintf("> %s\n", s)
28 | }
29 |
30 | func QuoteCode(s string) string {
31 | return fmt.Sprintf("`%s`", s)
32 | }
33 |
34 | func InfoText(s string) string {
35 | return fmt.Sprintf(`%s`, s)
36 | }
37 |
38 | func CommentText(s string) string {
39 | return fmt.Sprintf(`%s`, s)
40 | }
41 |
42 | func WarningText(s string) string {
43 | return fmt.Sprintf(`%s`, s)
44 | }
45 |
46 | func MentionByUserid(userid string) string {
47 | return fmt.Sprintf(`<@%s>`, userid)
48 | }
49 |
--------------------------------------------------------------------------------
/news.go:
--------------------------------------------------------------------------------
1 | package wecom_bot_api
2 |
3 | type newsMsg struct {
4 | // 消息类型, 固定为 `news`
5 | MsgType string `json:"msgtype"`
6 |
7 | // 图文消息, 一个图文消息支持1到8条图文
8 | News newsData `json:"news"`
9 | }
10 |
11 | type newsData struct {
12 | Articles []Article `json:"articles"`
13 | }
14 |
15 | type Article struct {
16 | // **必填** 标题, 不超过 128 个字节, 超过会自动截断
17 | Title string `json:"title"`
18 |
19 | // **选填** 描述, 不超过 512 个字节, 超过会自动截断
20 | Description string `json:"description,omitempty"`
21 |
22 | // **必填** 点击后跳转的链接
23 | Url string `json:"url"`
24 |
25 | // **选填** 图文消息的图片链接, 支持 `JPG`、`PNG` 格式, 较好的效果为大图 `1068*455`, 小图 `150*150`
26 | PicUrl string `json:"picurl,omitempty"`
27 | }
28 |
29 | func newNewsMsg(art Article, articles ...Article) newsMsg {
30 | articles = append([]Article{art}, articles...)
31 | msg := newsMsg{
32 | MsgType: "news",
33 | News: newsData{Articles: articles},
34 | }
35 |
36 | return msg
37 | }
38 |
39 | func NewArticle(title, Url string, opts ...ArticleOption) Article {
40 | article := Article{
41 | Title: title,
42 | Url: Url,
43 | }
44 |
45 | for _, opt := range opts {
46 | opt(&article)
47 | }
48 |
49 | return article
50 | }
51 |
52 | type ArticleOption func(d *Article)
53 |
54 | func ArticleDescription(desc string) ArticleOption {
55 | return func(d *Article) {
56 | d.Description = desc
57 | }
58 | }
59 | func ArticlePicUrl(picUrl string) ArticleOption {
60 | return func(d *Article) {
61 | d.PicUrl = picUrl
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/news_test.go:
--------------------------------------------------------------------------------
1 | package wecom_bot_api
2 |
3 | import (
4 | "encoding/json"
5 | "testing"
6 | )
7 |
8 | func Test_newNewsMessage(t *testing.T) {
9 | article := NewArticle("中秋节礼品领取", "www.qq.com",
10 | ArticleDescription("今年中秋节公司有豪礼相送"),
11 | ArticlePicUrl("http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png"),
12 | )
13 | msg := newNewsMsg(article)
14 | // msg := newNewsMsg(article, article)
15 |
16 | marshal, err := json.Marshal(&msg)
17 | if err != nil {
18 | t.Fatal(err)
19 | }
20 | t.Log(string(marshal))
21 | }
22 |
--------------------------------------------------------------------------------
/templatecard.go:
--------------------------------------------------------------------------------
1 | package wecom_bot_api
2 |
3 | type templateCardMsg struct {
4 | // 消息类型, 固定为 `template_card`
5 | MsgType string `json:"msgtype"`
6 | TemplateCard templateCard `json:"template_card"`
7 | }
8 |
9 | func newTemplateCardMsg(templateCard *templateCard) *templateCardMsg {
10 | return &templateCardMsg{
11 | MsgType: "template_card",
12 | TemplateCard: *templateCard,
13 | }
14 | }
15 |
16 | type templateCard struct {
17 | // 模版卡片类型
18 | // 文本通知模版卡片类型: `text_notice`
19 | // 图文展示模版卡片类型: `news_notice`
20 | CardType string `json:"card_type"`
21 | Source *templateCardSource `json:"source,omitempty"`
22 | MainTitle templateCardMainTitle `json:"main_title"`
23 | EmphasisContent *templateCardEmphasisContent `json:"emphasis_content,omitempty"`
24 |
25 | CardImage *templateCardImage `json:"card_image,omitempty"`
26 | VerticalContentList []templateCardVerticalContent `json:"vertical_content_list,omitempty"`
27 |
28 | // 二级普通文本
29 | // 建议不超过112个字
30 | SubTitleText string `json:"sub_title_text,omitempty"`
31 |
32 | // 二级标题+文本列表,该字段可为空数组
33 | //
34 | // 但有数据的话需确认对应字段是否必填
35 | //
36 | // 列表长度不超过6
37 | HorizontalContentList []templateCardHorizontalContent `json:"horizontal_content_list"`
38 |
39 | // 跳转指引样式的列表
40 | //
41 | // 该字段可为空数组
42 | //
43 | // 但有数据的话需确认对应字段是否必填
44 | //
45 | // 列表长度不超过3
46 | JumpList []templateCardJump `json:"jump_list"`
47 |
48 | // 整体卡片的点击跳转事件
49 | //
50 | // text_notice 模版卡片中该字段为必填项
51 | CardAction templateCardAction `json:"card_action"`
52 | }
53 |
54 | func newTemplateCardText(mainTitle TemplateCardMainTitleOption, cardAction TemplateCardAction, opts ...TemplateCardOption) *templateCard {
55 | var _mainTitle templateCardMainTitle
56 | mainTitle(&_mainTitle)
57 | var _cardAction templateCardAction
58 | cardAction(&_cardAction)
59 | tplCard := templateCard{
60 | CardType: "text_notice",
61 | Source: nil,
62 | MainTitle: _mainTitle,
63 | EmphasisContent: nil,
64 | VerticalContentList: make([]templateCardVerticalContent, 0, 4),
65 | SubTitleText: "",
66 | HorizontalContentList: make([]templateCardHorizontalContent, 0, 6),
67 | JumpList: make([]templateCardJump, 0, 3),
68 | CardAction: _cardAction,
69 | }
70 | for _, opt := range opts {
71 | opt(&tplCard)
72 | }
73 | return &tplCard
74 | }
75 |
76 | func newTemplateCardNews(mainTitle TemplateCardMainTitleOption, cardImage TemplateCardImageOption, cardAction TemplateCardAction, opts ...TemplateCardOption) *templateCard {
77 | var (
78 | _mainTitle templateCardMainTitle
79 | _cardAction templateCardAction
80 | _cardImage templateCardImage
81 | )
82 | mainTitle(&_mainTitle)
83 | cardAction(&_cardAction)
84 | cardImage(&_cardImage)
85 | tplCard := templateCard{
86 | CardType: "news_notice",
87 | Source: nil,
88 | MainTitle: _mainTitle,
89 | EmphasisContent: nil,
90 | CardImage: &_cardImage,
91 | VerticalContentList: make([]templateCardVerticalContent, 0, 4),
92 | SubTitleText: "",
93 | HorizontalContentList: make([]templateCardHorizontalContent, 0, 6),
94 | JumpList: make([]templateCardJump, 0, 3),
95 | CardAction: _cardAction,
96 | }
97 | for _, opt := range opts {
98 | opt(&tplCard)
99 | }
100 | return &tplCard
101 | }
102 |
103 | type TemplateCardMainTitleOption func(mt *templateCardMainTitle)
104 |
105 | func TemplateCardMainTitle(title string, desc string) TemplateCardMainTitleOption {
106 | return func(mt *templateCardMainTitle) {
107 | mt.Title = title
108 | mt.Desc = desc
109 | }
110 | }
111 |
112 | type TemplateCardOption func(tc *templateCard)
113 |
114 | // func TemplateCardSourceIconUrl(iconUrl string) TemplateCardOption {
115 | // return func(tc *templateCard) {
116 | // tc.Source.IconUrl = iconUrl
117 | // }
118 | // }
119 | //
120 | // func TemplateCardSourceDesc(desc string) TemplateCardOption {
121 | // return func(tc *templateCard) {
122 | // tc.Source.Desc = desc
123 | // }
124 | // }
125 |
126 | func TemplateCardSource(iconUrl, desc string) TemplateCardOption {
127 | return func(tc *templateCard) {
128 | if tc.Source == nil {
129 | tc.Source = new(templateCardSource)
130 | }
131 | tc.Source.IconUrl = iconUrl
132 | tc.Source.Desc = desc
133 | }
134 | }
135 |
136 | // func TemplateCardEmphasisContentTitle(title string) TemplateCardOption {
137 | // return func(tc *templateCard) {
138 | // tc.EmphasisContent.Title = title
139 | // }
140 | // }
141 | //
142 | // func TemplateCardEmphasisContentDesc(desc string) TemplateCardOption {
143 | // return func(tc *templateCard) {
144 | // tc.EmphasisContent.Desc = desc
145 | // }
146 | // }
147 |
148 | func TemplateCardEmphasisContent(title, desc string) TemplateCardOption {
149 | return func(tc *templateCard) {
150 | if tc.EmphasisContent == nil {
151 | tc.EmphasisContent = new(templateCardEmphasisContent)
152 | }
153 | tc.EmphasisContent.Title = title
154 | tc.EmphasisContent.Desc = desc
155 | }
156 | }
157 |
158 | func TemplateCardSubTitleText(subTitle string) TemplateCardOption {
159 | return func(tc *templateCard) {
160 | tc.SubTitleText = subTitle
161 | }
162 | }
163 |
164 | // TemplateCardHorizontalContent 二级标题+文本列表
165 | // 列表长度不超过6
166 | func TemplateCardHorizontalContent(keyName string, opt TemplateCardHorizontalContentOption) TemplateCardOption {
167 | return func(tc *templateCard) {
168 | horizontalContent := templateCardHorizontalContent{KeyName: keyName}
169 | if opt != nil {
170 | opt(&horizontalContent)
171 | }
172 | tc.HorizontalContentList = append(tc.HorizontalContentList, horizontalContent)
173 | }
174 | }
175 |
176 | type TemplateCardHorizontalContentOption func(hc *templateCardHorizontalContent)
177 |
178 | func TemplateCardHorizontalContentText(text string) TemplateCardHorizontalContentOption {
179 | return func(hc *templateCardHorizontalContent) {
180 | hc.Type = 0
181 | hc.Value = text
182 | }
183 | }
184 |
185 | func TemplateCardHorizontalContentUrl(rawUrl string, text string) TemplateCardHorizontalContentOption {
186 | return func(hc *templateCardHorizontalContent) {
187 | hc.Type = 1
188 | hc.Url = rawUrl
189 | hc.Value = text
190 | }
191 | }
192 |
193 | func TemplateCardHorizontalContentMedia(filename string, media Media) TemplateCardHorizontalContentOption {
194 | return func(hc *templateCardHorizontalContent) {
195 | hc.Type = 2
196 | hc.Value = filename
197 | hc.MediaId = media.ID
198 | }
199 | }
200 |
201 | // TemplateCardJump 跳转指引样式的列表
202 | // 列表长度不超过3
203 | func TemplateCardJump(title string, opt TemplateCardJumpOption) TemplateCardOption {
204 | return func(tc *templateCard) {
205 | jump := templateCardJump{
206 | Type: 0,
207 | Title: title,
208 | }
209 | if opt != nil {
210 | opt(&jump)
211 | }
212 | tc.JumpList = append(tc.JumpList, jump)
213 | }
214 | }
215 |
216 | type TemplateCardJumpOption func(j *templateCardJump)
217 |
218 | func TemplateCardJumpUrl(rawUrl string) TemplateCardJumpOption {
219 | return func(j *templateCardJump) {
220 | j.Type = 1
221 | j.Url = rawUrl
222 | }
223 | }
224 |
225 | func TemplateCardJumpApp(appID string, pagePath string) TemplateCardJumpOption {
226 | return func(j *templateCardJump) {
227 | j.Type = 2
228 | j.AppID = appID
229 | j.PagePath = pagePath
230 | }
231 | }
232 |
233 | type TemplateCardAction func(act *templateCardAction)
234 |
235 | func TemplateCardActionUrl(rawUrl string) TemplateCardAction {
236 | return func(act *templateCardAction) {
237 | act.Type = 1
238 | act.Url = rawUrl
239 | }
240 | }
241 |
242 | func TemplateCardActionApp(appID string, pagePath string) TemplateCardAction {
243 | return func(act *templateCardAction) {
244 | act.Type = 2
245 | act.Appid = appID
246 | act.PagePath = pagePath
247 | }
248 | }
249 |
250 | type TemplateCardImageOption func(img *templateCardImage)
251 |
252 | // TemplateCardImage
253 | // aspectRatio 图片的宽高比
254 | // 宽高比要小于2.25
255 | // 大于1.3
256 | // 不填该参数默认1.3
257 | func TemplateCardImage(rawUrl string, aspectRatio ...float64) TemplateCardImageOption {
258 | return func(img *templateCardImage) {
259 | img.Url = rawUrl
260 | if len(aspectRatio) != 0 {
261 | img.AspectRatio = aspectRatio[0]
262 | }
263 | }
264 | }
265 |
266 | // TemplateCardVerticalContent 卡片二级垂直内容
267 | // 列表长度不超过4
268 | func TemplateCardVerticalContent(title, desc string) TemplateCardOption {
269 | return func(tc *templateCard) {
270 | tc.VerticalContentList = append(tc.VerticalContentList, templateCardVerticalContent{
271 | Title: title,
272 | Desc: desc,
273 | })
274 | }
275 | }
276 |
277 | type (
278 | // templateCardEmphasisContent 关键数据样式
279 | templateCardEmphasisContent struct {
280 | Title string `json:"title,omitempty"` // 关键数据样式的数据内容,建议不超过10个字
281 | Desc string `json:"desc,omitempty"` // 关键数据样式的数据描述内容,建议不超过15个字
282 | }
283 |
284 | // templateCardImage 图片样式
285 | templateCardImage struct {
286 | Url string `json:"url"` // 图片的 url
287 | AspectRatio float64 `json:"aspect_ratio,omitempty"` // 图片的宽高比,宽高比要小于2.25,大于1.3,不填该参数默认1.3
288 | }
289 |
290 | // templateCardVerticalContent 卡片二级垂直内容,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过4
291 | templateCardVerticalContent struct {
292 | Title string `json:"title"` // 卡片二级标题,建议不超过26个字
293 | Desc string `json:"desc,omitempty"` // 二级普通文本,建议不超过112个字
294 | }
295 | )
296 |
297 | type (
298 | // templateCardSource 卡片来源样式信息,不需要来源样式可不填写
299 | templateCardSource struct {
300 | IconUrl string `json:"icon_url,omitempty"` // 来源图片的url
301 | Desc string `json:"desc,omitempty"` // 来源图片的描述,建议不超过13个字
302 | }
303 |
304 | // templateCardMainTitle 模版卡片的主要内容,包括一级标题和标题辅助信息
305 | templateCardMainTitle struct {
306 | Title string `json:"title"` // 一级标题,建议不超过26个字
307 | Desc string `json:"desc,omitempty"` // 标题辅助信息,建议不超过30个字
308 | }
309 |
310 | // templateCardHorizontalContent 二级标题+文本列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过6
311 | templateCardHorizontalContent struct {
312 | Type int `json:"type,omitempty"` // 链接类型,0或不填代表是普通文本,1 代表跳转url,2 代表下载附件
313 | KeyName string `json:"keyname"` // 二级标题,建议不超过5个字
314 | Value string `json:"value,omitempty"` // 二级文本,如果horizontal_content_list.type是2,该字段代表文件名称(要包含文件类型),建议不超过26个字
315 | Url string `json:"url,omitempty"` // 链接跳转的url,horizontal_content_list.type是1时必填
316 | MediaId string `json:"media_id,omitempty"` // 附件的media_id,horizontal_content_list.type是2时必填
317 | }
318 |
319 | // templateCardJump 跳转指引样式的列表,该字段可为空数组,但有数据的话需确认对应字段是否必填,列表长度不超过3
320 | templateCardJump struct {
321 | Type int `json:"type,omitempty"` // 跳转链接类型,0 或不填代表不是链接,1 代表跳转 url,2 代表跳转小程序
322 | Title string `json:"title"` // 跳转链接样式的文案内容,建议不超过 13个字
323 | Url string `json:"url,omitempty"` // 跳转链接的 url,jump_list.type 是 1 时必填
324 | AppID string `json:"appid,omitempty"` // 跳转链接的小程序的 appid,jump_list.type 是 2 时必填
325 | PagePath string `json:"pagepath,omitempty"` // 跳转链接的小程序的 pagepath,jump_list.type 是 2 时选填
326 | }
327 |
328 | // templateCardAction 整体卡片的点击跳转事件,text_notice 模版卡片中该字段为必填项
329 | templateCardAction struct {
330 | Type int `json:"type"` // 卡片跳转类型,0 或不填代表不是链接,1 代表跳转url,2 代表打开小程序。text_notice模版卡片中该字段取值范围为[1,2]
331 | Url string `json:"url,omitempty"` // 跳转事件的 url,card_action.type 是 1 时必填
332 | Appid string `json:"appid,omitempty"` // 跳转事件的小程序的 appid,card_action.type 是 2 时必填
333 | PagePath string `json:"pagepath,omitempty"` // 跳转事件的小程序的 pagepath,card_action.type 是 2 时选填
334 | }
335 | )
336 |
--------------------------------------------------------------------------------
/text.go:
--------------------------------------------------------------------------------
1 | package wecom_bot_api
2 |
3 | type textMsg struct {
4 | // 消息类型, 固定为 `text`
5 | MsgType string `json:"msgtype"`
6 | Text *textData `json:"text"`
7 | }
8 |
9 | type textData struct {
10 | // **必填**
11 | //
12 | // 文本内容, 最长不超过 2048 个字节, 必须是 `utf8` 编码
13 | Content string `json:"content"`
14 |
15 | // **选填**
16 | //
17 | // `userid` 列表, 提醒群中的指定成员 (@某个成员), `@all` 表示提醒所有人,
18 | //
19 | // 如果获取不到 `userid`, 可以使用 `mentioned_mobile_list`
20 | MentionedList []string `json:"mentioned_list,omitempty"`
21 |
22 | // **选填**
23 | //
24 | // 手机号列表, 提醒手机号对应的群成员 (@某个成员), `@all`表示提醒所有人
25 | MentionedMobileList []string `json:"mentioned_mobile_list,omitempty"`
26 | }
27 |
28 | func newTextMsg(content string, opts ...TextMsgOption) textMsg {
29 | msg := textMsg{
30 | MsgType: "text",
31 | Text: &textData{
32 | Content: content,
33 | MentionedList: make([]string, 0),
34 | MentionedMobileList: make([]string, 0),
35 | },
36 | }
37 |
38 | for _, opt := range opts {
39 | opt(msg.Text)
40 | }
41 |
42 | return msg
43 | }
44 |
45 | type TextMsgOption func(d *textData)
46 |
47 | // MentionByUserid 通过 `userid` @某个成员
48 | func MentionByUserid(userid string) TextMsgOption {
49 | return func(d *textData) {
50 | d.MentionedList = append(d.MentionedList, userid)
51 | }
52 | }
53 |
54 | // MentionAllByUserid `@all` 提醒所有人, 等同于 MentionAllByMobile
55 | func MentionAllByUserid() TextMsgOption {
56 | return func(d *textData) {
57 | d.MentionedList = append(d.MentionedList, "@all")
58 | }
59 | }
60 |
61 | // MentionByMobile 通过 `手机号码` @某个成员
62 | func MentionByMobile(mobile string) TextMsgOption {
63 | return func(d *textData) {
64 | d.MentionedMobileList = append(d.MentionedMobileList, mobile)
65 | }
66 | }
67 |
68 | // MentionAllByMobile `@all` 提醒所有人, 等同于 MentionAllByUserid
69 | func MentionAllByMobile() TextMsgOption {
70 | return func(d *textData) {
71 | d.MentionedMobileList = append(d.MentionedMobileList, "@all")
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/text_test.go:
--------------------------------------------------------------------------------
1 | package wecom_bot_api
2 |
3 | import (
4 | "encoding/json"
5 | "testing"
6 | )
7 |
8 | func TestNewTextMessage(t *testing.T) {
9 | msg := newTextMsg("广州今日天气:29度,大部分多云,降雨概率:60%",
10 | MentionByUserid("wangqing"), MentionAllByUserid(),
11 | MentionByMobile("13800001111"), MentionAllByMobile(),
12 | )
13 |
14 | marshal, err := json.Marshal(&msg)
15 | if err != nil {
16 | t.Fatal(err)
17 | }
18 | t.Log(string(marshal))
19 | }
20 |
--------------------------------------------------------------------------------