-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrobot.go
490 lines (446 loc) · 14.6 KB
/
robot.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
package dingtalk
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/url"
"time"
)
// RobotCustom 群机器人-自定义
//
// 官方文档: https://developers.dingtalk.com/document/app/custom-robot-access
type RobotCustom struct {
webhook string // 例: https://oapi.dingtalk.com/robot/send?access_token=xxx
secret string // (可选)例: SEC8a9fc6f36f447d7c497f8c8e08accde4c49b4b5a366fa3903f47e250d6746979
}
// NewRobotCustom 实例化
func NewRobotCustom() *RobotCustom {
return &RobotCustom{}
}
// SetWebhook 设置Token
func (rc *RobotCustom) SetWebhook(t string) *RobotCustom {
rc.webhook = t
return rc
}
// SetSecret 设置Secret
func (rc *RobotCustom) SetSecret(s string) *RobotCustom {
rc.secret = s
return rc
}
// SendText 发送Text消息
//
// 示例:
// robot.SendText("TEST: Text")
// robot.SendText("TEST: Text&AtAll", robot.AtAll())
// robot.SendText("TEST: Text&AtMobiles", robot.AtMobiles("19900001111"))
func (rc *RobotCustom) SendText(content string, opts ...RobotOption) error {
msg := &robotMsg{
MsgType: msgTypeText,
Text: &robotText{Content: content},
}
return rc.send(msg, opts...)
}
// SendLink 发送Link消息
//
// 示例:
// robot.SendLink(
// "TEST: Link",
// "link content",
// "https://github.com/shockerli",
// "https://www.wangbase.com/blogimg/asset/202101/bg2021011601.jpg",
// )
func (rc *RobotCustom) SendLink(title, text, msgURL, picURL string, opts ...RobotOption) error {
msg := &robotMsg{
MsgType: msgTypeLink,
Link: &robotLink{
Title: title,
Text: text,
MessageURL: msgURL,
PicURL: picURL,
},
}
return rc.send(msg, opts...)
}
// SendMarkdown 发送Markdown消息
//
// 示例:
// robot.SendMarkdown("TEST: Markdown", markdown)
// robot.SendMarkdown("TEST: Markdown&AtAll", markdown, robot.AtAll())
// robot.SendMarkdown("TEST: Markdown&AtMobiles", markdown, robot.AtMobiles("19900001111"))
func (rc *RobotCustom) SendMarkdown(title, text string, opts ...RobotOption) error {
msg := &robotMsg{
MsgType: msgTypeMarkdown,
Markdown: &robotMarkdown{
Title: title,
Text: text,
},
}
return rc.send(msg, opts...)
}
// SendActionCard 发送ActionCard消息
//
// 示例:
// robot.SendActionCard(
// "TEST: ActionCard&SingleCard",
// "SingleCard content",
// robot.SingleCard("阅读全文", "https://github.com/shockerli"),
// )
func (rc *RobotCustom) SendActionCard(title, text string, opts ...RobotOption) error {
msg := &robotMsg{
MsgType: msgTypeActionCard,
ActionCard: &robotActionCard{
Title: title,
Text: text,
HideAvatar: "0", // 默认展示
BtnOrientation: "1", // 默认横向排列
},
}
return rc.send(msg, opts...)
}
// SendFeedCard 发送FeedCard消息
//
// 示例:
// robot.SendFeedCard(
// robot.FeedCard("3月15日起,Chromium 不能再调用谷歌 API", "https://bodhi.fedoraproject.org/updates/FEDORA-2021-48866282e5%29", "https://www.wangbase.com/blogimg/asset/202101/bg2021012506.jpg"),
// robot.FeedCard("考古学家在英国发现两枚11世纪北宋时期的中国硬币", "https://www.caitlingreen.org/2020/12/another-medieval-chinese-coin-from-england.html", "https://www.wangbase.com/blogimg/asset/202101/bg2021012208.jpg"),
// )
func (rc *RobotCustom) SendFeedCard(opts ...RobotOption) error {
msg := &robotMsg{
MsgType: msgTypeFeedCard,
FeedCard: &robotFeedCard{
Links: []robotFeedCardLink{},
},
}
return rc.send(msg, opts...)
}
// 发送消息
func (rc *RobotCustom) send(msg *robotMsg, opts ...RobotOption) error {
// options
for _, opt := range opts {
opt(msg)
}
v, err := json.Marshal(msg)
if err != nil {
return err
}
var api = rc.webhook
var value = make(url.Values)
var now = time.Now().UnixNano() / 1e6 // 毫秒
if msg.outgoing.SessionWebhook != "" {
if msg.outgoing.SessionWebhookExpiredTime < now {
return fmt.Errorf("SessionWebhookExpiredTime is expired")
}
api = msg.outgoing.SessionWebhook
} else if rc.secret != "" {
value.Set("timestamp", fmt.Sprintf("%d", now))
value.Set("sign", rc.sign(now, rc.secret))
api = rc.webhook + "&" + value.Encode()
}
// 请求接口
data, err := request(api, v)
if err != nil {
return err
}
var response struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}
err = json.Unmarshal(data, &response)
if err != nil {
return err
}
if response.ErrCode != 0 {
return fmt.Errorf("群机器人消息发送失败: %v", response.ErrMsg)
}
return nil
}
// 签名算法
func (*RobotCustom) sign(ts int64, secret string) string {
h := hmac.New(sha256.New, []byte(secret))
_, _ = h.Write([]byte(fmt.Sprintf("%d\n%s", ts, secret)))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
// robot message definition
// 机器人消息类型
const (
msgTypeText = "text"
msgTypeLink = "link"
msgTypeMarkdown = "markdown"
msgTypeActionCard = "actionCard"
msgTypeFeedCard = "feedCard"
)
// 机器人消息结构
type robotMsg struct {
MsgType string `json:"msgtype"` // 消息类型
At *robotAt `json:"at,omitempty"`
Text *robotText `json:"text,omitempty"`
Link *robotLink `json:"link,omitempty"`
Markdown *robotMarkdown `json:"markdown,omitempty"`
ActionCard *robotActionCard `json:"actionCard,omitempty"`
FeedCard *robotFeedCard `json:"feedCard,omitempty"`
outgoing RobotOutgoing
}
// 消息@人的设置
// [NOTICE] 仅针对Text/Link/Markdown类型有效
type robotAt struct {
AtMobiles []string `json:"atMobiles,omitempty"` // 被@人的手机号
IsAtAll bool `json:"isAtAll,omitempty"` // 是否@所有人
}
// 消息类型: Text
type robotText struct {
Content string `json:"content"` // 消息内容
}
// 消息类型: Link
type robotLink struct {
Title string `json:"title"` // 消息标题
Text string `json:"text"` // 消息内容,如果太长只会部分展示
MessageURL string `json:"messageUrl"` // 点击消息跳转的UR
PicURL string `json:"picUrl,omitempty"` // 图片URL
}
// 消息类型: Markdown
type robotMarkdown struct {
Title string `json:"title"` // 首屏会话透出的展示内容
Text string `json:"text"` // Markdown格式的消息
}
// 消息类型: ActionCard
// * 整体跳转
// * 独立跳转
// [NOTICE]设置singleTitle和singleURL后,btns无效
type robotActionCard struct {
Title string `json:"title"` // 首屏会话透出的展示内容
Text string `json:"text"` // Markdown格式的消息
SingleTitle string `json:"singleTitle,omitempty"` // 单个按钮的标题
SingleURL string `json:"singleURL,omitempty"` // 点击singleTitle按钮触发的URL
HideAvatar string `json:"hideAvatar,omitempty"` // 0:显示图片,1:隐藏图片
BtnOrientation string `json:"btnOrientation,omitempty"` // 0:按钮竖直排列,1:按钮横向排列
Btns []robotActionCardBtn `json:"btns,omitempty"` // 独立跳转的按钮列表
}
type robotActionCardBtn struct {
Title string `json:"title"` // 按钮标题
ActionURL string `json:"actionURL"` // 点击按钮触发的URL
}
// 消息类型: FeedCard
type robotFeedCard struct {
Links []robotFeedCardLink `json:"links"`
}
type robotFeedCardLink struct {
Title string `json:"title"`
MessageURL string `json:"messageURL"` // 点击单条信息到跳转链接
PicURL string `json:"picURL"` // 单条信息后面图片的URL
}
// RobotOption 群机器人-消息配置项
type RobotOption func(*robotMsg)
// AtAll 设置是否@所有人
//
// 适用Text/Markdown类型
//
// 示例:
// robot.SendMarkdown("TEST: Markdown&AtAll", markdown, robot.AtAll())
func (rc *RobotCustom) AtAll() RobotOption {
return func(msg *robotMsg) {
if msg.MsgType != msgTypeText && msg.MsgType != msgTypeMarkdown {
return
}
if msg.At == nil {
msg.At = &robotAt{}
}
msg.At.IsAtAll = true
}
}
// AtMobiles 设置@人的手机号
//
// 适用Text/Markdown类型
//
// 示例:
// robot.SendMarkdown("TEST: Markdown&AtMobiles", markdown, robot.AtMobiles("19900001111"))
func (rc *RobotCustom) AtMobiles(m ...string) RobotOption {
return func(msg *robotMsg) {
if msg.MsgType != msgTypeText && msg.MsgType != msgTypeMarkdown {
return
}
if msg.At == nil {
msg.At = &robotAt{}
}
msg.At.AtMobiles = m
}
}
// HideAvatar 隐藏头像(0-显示, 1-隐藏, 默认0)
//
// 适用ActionCard类型
//
// 示例:
// robot.SendActionCard(
// "TEST: ActionCard&HideAvatar",
// "24565\n\n\n\nSingleCard content with image",
// robot.SingleCard("阅读全文", "https://github.com/shockerli"),
// robot.HideAvatar("1"),
// )
func (rc *RobotCustom) HideAvatar(v string) RobotOption {
return func(msg *robotMsg) {
if msg.MsgType != msgTypeActionCard {
return
}
msg.ActionCard.HideAvatar = v
}
}
// BtnOrientation 按钮排列(0-竖直排列, 1-横向排列, 默认1)
//
// 适用ActionCard类型
//
// 示例:
// robot.SendActionCard(
// "TEST: ActionCard&BtnOrientation",
// "BtnOrientation content",
// robot.MultiCard("内容不错", "https://github.com/shockerli"),
// robot.MultiCard("不感兴趣", "https://github.com/shockerli"),
// robot.BtnOrientation("0"),
// )
func (rc *RobotCustom) BtnOrientation(v string) RobotOption {
return func(msg *robotMsg) {
if msg.MsgType != msgTypeActionCard {
return
}
msg.ActionCard.BtnOrientation = v
}
}
// SingleCard 整体跳转配置
//
// 适用ActionCard类型
//
// 示例:
// robot.SendActionCard(
// "TEST: ActionCard&SingleCard",
// "SingleCard content",
// robot.SingleCard("阅读全文", "https://github.com/shockerli"),
// )
func (rc *RobotCustom) SingleCard(title, url string) RobotOption {
return func(msg *robotMsg) {
if msg.MsgType != msgTypeActionCard {
return
}
msg.ActionCard.SingleTitle = title
msg.ActionCard.SingleURL = url
}
}
// MultiCard 添加一个MultiCard项
//
// 适用ActionCard类型
//
// 示例:
// robot.SendActionCard(
// "TEST: ActionCard&MultiCard",
// "MultiCard content",
// robot.MultiCard("内容不错", "https://github.com/shockerli"),
// robot.MultiCard("不感兴趣", "https://github.com/shockerli"),
// )
func (rc *RobotCustom) MultiCard(title, url string) RobotOption {
return func(msg *robotMsg) {
if msg.MsgType != msgTypeActionCard {
return
}
msg.ActionCard.Btns = append(msg.ActionCard.Btns, robotActionCardBtn{
Title: title,
ActionURL: url,
})
}
}
// FeedCard 添加一个FeedCard项
//
// 适用FeedCard类型
//
// 示例:
// robot.SendFeedCard(
// robot.FeedCard("3月15日起,Chromium 不能再调用谷歌 API", "https://bodhi.fedoraproject.org/updates/FEDORA-2021-48866282e5%29", "https://www.wangbase.com/blogimg/asset/202101/bg2021012506.jpg"),
// robot.FeedCard("考古学家在英国发现两枚11世纪北宋时期的中国硬币", "https://www.caitlingreen.org/2020/12/another-medieval-chinese-coin-from-england.html", "https://www.wangbase.com/blogimg/asset/202101/bg2021012208.jpg"),
// )
func (rc *RobotCustom) FeedCard(title, msgURL, picURL string) RobotOption {
return func(msg *robotMsg) {
if msg.MsgType != msgTypeFeedCard {
return
}
msg.FeedCard.Links = append(msg.FeedCard.Links, robotFeedCardLink{
Title: title,
MessageURL: msgURL,
PicURL: picURL,
})
}
}
// WithOutgoing 通过Outgoing的临时消息接口发送
//
// 示例:
// og, err := robot.ParseOutgoing(bytes.NewBufferString(callbackBody))
// err = robot.SendText("callback", robot.WithOutgoing(og))
func (rc *RobotCustom) WithOutgoing(og RobotOutgoing) RobotOption {
return func(msg *robotMsg) {
msg.outgoing = og
}
}
// ParseOutgoing 解析Outgoing消息体
//
// 示例:
// robot.ParseOutgoing(callbackBody)
func (rc *RobotCustom) ParseOutgoing(r io.Reader) (og RobotOutgoing, err error) {
buf, err := ioutil.ReadAll(r)
if err != nil {
return
}
err = json.Unmarshal(buf, &og)
return
}
// RobotOutgoing Outgoing回调消息体
//
// 示例:
// "msgtype": "text"
// }
// {
// "conversationId": "ciddz7nmHDaX/7Niz+Gb5VVrw==",
// "sceneGroupCode": "project",
// "atUsers": [
// {
// "dingtalkId": "$:LWCP_v1:$0sIVIuw1HvQQ5gRAtFWzypo0+T1TgPOP"
// },
// {
// "dingtalkId": "$:LWCP_v1:$I3cyfTzrws4nCbY289cXbKCVcdd1wize"
// }
// ],
// "chatbotUserId": "$:LWCP_v1:$I3cyfTzrws4nCbY289cXbKCVcdd1wize",
// "msgId": "msgaKcioIqERkELm2T8TlE9CA==",
// "senderNick": "Jioby",
// "isAdmin": false,
// "sessionWebhookExpiredTime": 1612178396066,
// "createAt": 1612172996026,
// "conversationType": "2",
// "senderId": "$:LWCP_v1:$deZJcSfMzexC2YK+oLkk1g==",
// "conversationTitle": "xxx",
// "isInAtList": true,
// "sessionWebhook": "https://oapi.dingtalk.com/robot/sendBySession?session=eb18e18e8669b0a3cd7dff1388fe5e6a",
// "text": {
// "content": " 哈哈哈"
// },
// "msgtype": "text"
// }
type RobotOutgoing struct {
// 被@人的信息
AtUsers []struct {
DingTalkID string `json:"dingtalkId"` // 加密的人员ID
} `json:"atUsers"`
ChatBotUserID string `json:"chatbotUserId"` // 加密的机器人ID
ConversationID string `json:"conversationId"` // 加密的会话ID
ConversationTitle string `json:"conversationTitle"` // 会话标题(群聊时才有,即群名)
ConversationType string `json:"conversationType"` // 1-单聊、2-群聊
CreateAt int64 `json:"createAt"` // 消息的时间戳,单位ms
IsAdmin bool `json:"isAdmin"` // 是否为管理员发送的消息
IsInAtList bool `json:"isInAtList"` //
MsgID string `json:"msgId"` // 加密的消息ID
MsgType string `json:"msgtype"` // 消息类型: 目前只支持Text
SceneGroupCode string `json:"sceneGroupCode"` // 群组场景类型Code
SenderID string `json:"senderId"` // 加密的发送者ID
SenderNick string `json:"senderNick"` // 发送者昵称
SessionWebhook string `json:"sessionWebhook"` // 临时的发送消息接口
SessionWebhookExpiredTime int64 `json:"sessionWebhookExpiredTime"` // SessionWebhook可用的有效截止时间
Text robotText `json:"text"` // Text类型的消息体
}