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