集团是使用钉钉进行工作交流的, 发现群里有很多问题其实是重复的,就在想是不是可以使用钉钉的群机器人,虽然说的确是可以部分实现,但是感觉还是差点什么,而且公司内部很多东西也不方便放上去,所以就想开发一个群机器人,然后就看钉钉开发文档,发现是有这个功能的,就开始研究,官方文档使用的语言主要是Java,并没有c#或者asp.net的相关文档,这就意味着要从头开始开发, 所幸的是他是有c#的SDK开发包,开发包里是有DLL的,这样能省下不少事,废话不多说,上链接
https://open.dingtalk.com/document/resourcedownload/download-server-sdk
打开页面后往下拉,知道如图所示处
我下载的是.net版本,下载下来后,导入到项目中即可
然后是配置机器人,这些在往上教程很多就不多赘诉了,直接上图
一开始我是在页面上面写的,看到官方文档上面说到了header,考虑到可能要使用到request 获取,就直接在页面写了,
后来在页面上通过以后改到了WebService中,毕竟感觉上webservice 会好一些,
把消息接收地址改成了这样,其实两者代码类似,只是我可能更喜欢在接口里写
1 protected string secret = 改成你自己的机器人的appSecret;
2 #region 机器人操作类
3 [WebMethod]
4 public void Reboot()
5 {
6 string result = "";
7 using (StreamReader reader = new StreamReader(HttpContext.Current.Request.InputStream, Encoding.UTF8))
8 {
9 result = reader.ReadToEnd();
10 }
11 try
12 {
13 string sign = HttpContext.Current.Request.Headers.GetValues("sign")[0].ToString();
14 string timestamp = HttpContext.Current.Request.Headers.GetValues("timestamp")[0].ToString();
15 string json = result;
16 CommonJsonModel model = SymmetricMethod.DeSerialize(json);
17 string text = model.GetModel("text").GetValue("content");
18 string sessionWebhook = model.GetValue("sessionWebhook");
19 string senderStaffId = model.GetValue("senderStaffId");
20 DBHelper.InsertRebootLog(HttpContext.Current.Request, HttpContext.Current.Request.Url.ToString(), "调用机器人", text + "--" + sessionWebhook + "--" + senderStaffId, sign + "----------" + timestamp, result, HttpContext.Current.Request.Headers, "调用机器人");
21 }
22 catch (Exception ex)
23 {
24 DBHelper.InsertRebootLog(HttpContext.Current.Request, HttpContext.Current.Request.Url.ToString(), "调用机器人", result, ex.Message, "接口调用来源不正确","", "调用机器人");
25 }
26 }
27 #endregion
这是webservice 接口的
1 string result = "";
2 using (StreamReader reader = new StreamReader(Request.InputStream, Encoding.UTF8))
3 {
4 result = reader.ReadToEnd();
5 }
6 try
7 {
8 string sign = Request.Headers.GetValues("sign")[0].ToString();
9 string timestamp = Request.Headers.GetValues("timestamp")[0].ToString();
10 string json = result;
11 CommonJsonModel model = SymmetricMethod.DeSerialize(json);
12 string text = model.GetModel("text").GetValue("content");
13 string sessionWebhook = model.GetValue("sessionWebhook");
14 string senderStaffId = model.GetValue("senderStaffId");
15 DBHelper.InsertRebootLog(Request, Request.Url.ToString(), "调用机器人", text + "--" + sessionWebhook + "--" + senderStaffId, sign + "----------" + timestamp, result, Request.Headers, "调用机器人");
16 }
17 catch (Exception ex)
18 {
19 DBHelper.InsertRebootLog(Request, Request.Url.ToString(), "调用机器人", result, ex.Message, "", "", "调用机器人");
20 }
这是写在页面Page_Load方法里面的,因为只要执行到这个页面,就是直接执行,没有任何其他操作,所以一定要写在Page_Load方法里
那么json 解析的源码我也放后面,也就是 CommonJsonModel 这个方法的代码
直接建两个类,名字分别是CommonJsonModelAnalyzer 和 CommonJsonModel
1 using System;
2 using System.Collections.Generic;
3 using System.Web;
4 using System.Text;
5
6 /// <summary>
7 ///CommonJsonModelAnalyzer 的摘要说明
8 /// </summary>
9 public class CommonJsonModelAnalyzer
10 {
11 public CommonJsonModelAnalyzer()
12 {
13 //
14 //TODO: 在此处添加构造函数逻辑
15 //
16
17 }
18 protected string _GetKey(string rawjson)
19 {
20 if (string.IsNullOrEmpty(rawjson))
21 return rawjson;
22
23 rawjson = rawjson.Trim();
24
25 string[] jsons = rawjson.Split(new char[] { ':' });
26
27 if (jsons.Length < 2)
28 return rawjson;
29
30 return jsons[0].Replace("\"", "").Trim();
31 }
32
33 protected string _GetValue(string rawjson)
34 {
35 if (string.IsNullOrEmpty(rawjson))
36 return rawjson;
37
38 rawjson = rawjson.Trim();
39
40 string[] jsons = rawjson.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
41
42 if (jsons.Length < 2)
43 return rawjson;
44
45 StringBuilder builder = new StringBuilder();
46
47 for (int i = 1; i < jsons.Length; i++)
48 {
49 builder.Append(jsons[i]);
50
51 builder.Append(":");
52 }
53
54 if (builder.Length > 0)
55 builder.Remove(builder.Length - 1, 1);
56
57 string value = builder.ToString();
58
59 if (value.StartsWith("\""))
60 value = value.Substring(1);
61
62 if (value.EndsWith("\""))
63 value = value.Substring(0, value.Length - 1);
64
65 return value;
66 }
67
68 protected List<string> _GetCollection(string rawjson)
69 {
70 //[{},{}]
71
72 List<string> list = new List<string>();
73
74 if (string.IsNullOrEmpty(rawjson))
75 return list;
76
77 rawjson = rawjson.Trim();
78
79 StringBuilder builder = new StringBuilder();
80
81 int nestlevel = -1;
82
83 int mnestlevel = -1;
84
85 for (int i = 0; i < rawjson.Length; i++)
86 {
87 if (i == 0)
88 continue;
89 else if (i == rawjson.Length - 1)
90 continue;
91
92 char jsonchar = rawjson[i];
93
94 if (jsonchar == '{')
95 {
96 nestlevel++;
97 }
98
99 if (jsonchar == '}')
100 {
101 nestlevel--;
102 }
103
104 if (jsonchar == '[')
105 {
106 mnestlevel++;
107 }
108
109 if (jsonchar == ']')
110 {
111 mnestlevel--;
112 }
113
114 if (jsonchar == ',' && nestlevel == -1 && mnestlevel == -1)
115 {
116 list.Add(builder.ToString());
117
118 builder = new StringBuilder();
119 }
120 else
121 {
122 builder.Append(jsonchar);
123 }
124 }
125
126 if (builder.Length > 0)
127 list.Add(builder.ToString());
128
129 return list;
130 }
131 }
1 using System;
2 using System.Collections.Generic;
3 using System.Web;
4
5 /// <summary>
6 ///CommonJsonModel 的摘要说明
7 /// </summary>
8 public class CommonJsonModel : CommonJsonModelAnalyzer
9 {
10 private string rawjson;
11
12 private bool isValue = false;
13
14 private bool isModel = false;
15
16 private bool isCollection = false;
17 private string json;
18
19 internal CommonJsonModel(string rawjson)
20 {
21 this.rawjson = rawjson;
22
23 if (string.IsNullOrEmpty(rawjson))
24 throw new Exception("missing rawjson");
25
26 rawjson = rawjson.Trim();
27
28 if (rawjson.StartsWith("{"))
29 {
30 isModel = true;
31 }
32 else if (rawjson.StartsWith("["))
33 {
34 isCollection = true;
35 }
36 else
37 {
38 isValue = true;
39 }
40 }
41
42 public string Rawjson
43 {
44 get { return rawjson; }
45 }
46
47 public bool IsValue()
48 {
49 return isValue;
50 }
51 public bool IsValue(string key)
52 {
53 if (!isModel)
54 return false;
55
56 if (string.IsNullOrEmpty(key))
57 return false;
58
59 foreach (string subjson in base._GetCollection(this.rawjson))
60 {
61 CommonJsonModel model = new CommonJsonModel(subjson);
62
63 if (!model.IsValue())
64 continue;
65
66 if (model.Key == key)
67 {
68 CommonJsonModel submodel = new CommonJsonModel(model.Value);
69
70 return submodel.IsValue();
71 }
72 }
73
74 return false;
75 }
76 public bool IsModel()
77 {
78 return isModel;
79 }
80 public bool IsModel(string key)
81 {
82 if (!isModel)
83 return false;
84
85 if (string.IsNullOrEmpty(key))
86 return false;
87
88 foreach (string subjson in base._GetCollection(this.rawjson))
89 {
90 CommonJsonModel model = new CommonJsonModel(subjson);
91
92 if (!model.IsValue())
93 continue;
94
95 if (model.Key == key)
96 {
97 CommonJsonModel submodel = new CommonJsonModel(model.Value);
98
99 return submodel.IsModel();
100 }
101 }
102
103 return false;
104 }
105 public bool IsCollection()
106 {
107 return isCollection;
108 }
109 public bool IsCollection(string key)
110 {
111 if (!isModel)
112 return false;
113
114 if (string.IsNullOrEmpty(key))
115 return false;
116
117 foreach (string subjson in base._GetCollection(this.rawjson))
118 {
119 CommonJsonModel model = new CommonJsonModel(subjson);
120
121 if (!model.IsValue())
122 continue;
123
124 if (model.Key == key)
125 {
126 CommonJsonModel submodel = new CommonJsonModel(model.Value);
127
128 return submodel.IsCollection();
129 }
130 }
131
132 return false;
133 }
134
135
136 /// <summary>
137 /// 当模型是对象,返回拥有的key
138 /// </summary>
139 /// <returns></returns>
140 public List<string> GetKeys()
141 {
142 if (!isModel)
143 return null;
144
145 List<string> list = new List<string>();
146
147 foreach (string subjson in base._GetCollection(this.rawjson))
148 {
149 string key = new CommonJsonModel(subjson).Key;
150
151 if (!string.IsNullOrEmpty(key))
152 list.Add(key);
153 }
154
155 return list;
156 }
157
158 /// <summary>
159 /// 当模型是对象,key对应是值,则返回key对应的值
160 /// </summary>
161 /// <param name="key"></param>
162 /// <returns></returns>
163 public string GetValue(string key)
164 {
165 if (!isModel)
166 return null;
167
168 if (string.IsNullOrEmpty(key))
169 return null;
170
171 foreach (string subjson in base._GetCollection(this.rawjson))
172 {
173 CommonJsonModel model = new CommonJsonModel(subjson);
174
175 if (!model.IsValue())
176 continue;
177
178 if (model.Key == key)
179 return model.Value;
180 }
181
182 return null;
183 }
184
185 /// <summary>
186 /// 模型是对象,key对应是对象,返回key对应的对象
187 /// </summary>
188 /// <param name="key"></param>
189 /// <returns></returns>
190 public CommonJsonModel GetModel(string key)
191 {
192 if (!isModel)
193 return null;
194
195 if (string.IsNullOrEmpty(key))
196 return null;
197
198 foreach (string subjson in base._GetCollection(this.rawjson))
199 {
200 CommonJsonModel model = new CommonJsonModel(subjson);
201
202 if (!model.IsValue())
203 continue;
204
205 if (model.Key == key)
206 {
207 CommonJsonModel submodel = new CommonJsonModel(model.Value);
208
209 if (!submodel.IsModel())
210 return null;
211 else
212 return submodel;
213 }
214 }
215
216 return null;
217 }
218
219 /// <summary>
220 /// 模型是对象,key对应是集合,返回集合
221 /// </summary>
222 /// <param name="key"></param>
223 /// <returns></returns>
224 public CommonJsonModel GetCollection(string key)
225 {
226 if (!isModel)
227 return null;
228
229 if (string.IsNullOrEmpty(key))
230 return null;
231
232 foreach (string subjson in base._GetCollection(this.rawjson))
233 {
234 CommonJsonModel model = new CommonJsonModel(subjson);
235
236 if (!model.IsValue())
237 continue;
238
239 if (model.Key == key)
240 {
241 CommonJsonModel submodel = new CommonJsonModel(model.Value);
242
243 if (!submodel.IsCollection())
244 return null;
245 else
246 return submodel;
247 }
248 }
249
250 return null;
251 }
252
253 /// <summary>
254 /// 模型是集合,返回自身
255 /// </summary>
256 /// <returns></returns>
257 public List<CommonJsonModel> GetCollection()
258 {
259 List<CommonJsonModel> list = new List<CommonJsonModel>();
260
261 if (IsValue())
262 return list;
263
264 foreach (string subjson in base._GetCollection(rawjson))
265 {
266 list.Add(new CommonJsonModel(subjson));
267 }
268
269 return list;
270 }
271
272
273
274
275 /// <summary>
276 /// 当模型是值对象,返回key
277 /// </summary>
278 private string Key
279 {
280 get
281 {
282 if (IsValue())
283 return base._GetKey(rawjson);
284
285 return null;
286 }
287 }
288 /// <summary>
289 /// 当模型是值对象,返回value
290 /// </summary>
291 private string Value
292 {
293 get
294 {
295 if (!IsValue())
296 return null;
297
298 return base._GetValue(rawjson);
299 }
300 }
301 }
另外还要再建一个调用json解析方法的类 我的名称叫做SymmetricMethod,你们就随意起
在这个类里面写一个方法
1 public static CommonJsonModel DeSerialize(string json)
2 {
3 return new CommonJsonModel(json);
4 }
一定要静态类,方便调用
其实到这一步一些关键内容的核心已经全部写完了,接下来就是如何使用
按照官方文档的说法,是需要对信息进行验证的
开发者需对header中的timestamp和sign进行验证,以判断是否是来自钉钉的合法请求,避免其他仿冒钉钉调用开发者的HTTPS服务传送数据,具体验证逻辑如下:
timestamp 与系统当前时间戳如果相差1小时以上,则认为是非法的请求。
sign 与开发者自己计算的结果不一致,则认为是非法的请求。
必须当timestamp和sign同时验证通过,才能认为是来自钉钉的合法请求。
其中会有sign 计算方法,那么我们就按照文档说的做,
sign的计算方法
header中的timestamp + "\n" + 机器人的appSecret当做签名字符串,使用HmacSHA256算法计算签名,然后进行Base64 encode,得到最终的签名值。
1 //获得时间戳
2 public static long ToUTC(DateTime time)
3 {
4 var zts = TimeZoneInfo.Local.BaseUtcOffset;
5 var yc = new DateTime(1970, 1, 1).Add(zts);
6 return (long)(DateTime.Now - yc).TotalMilliseconds;
7 }
8 //计算签名值
9 public static string GetHmac(string message, string secret)
10 {
11 byte[] keyByte = Encoding.UTF8.GetBytes(secret);
12 byte[] messageBytes = Encoding.UTF8.GetBytes(message);
13 using (var hmacsha256 = new HMACSHA256(keyByte))
14 {
15 byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
16 string hash = Convert.ToBase64String(hashmessage).Replace("+"," ");
17 return hash;
18 }
19 }
以上两段代码网上就能搜到,其中计算签名值网上写的并不完全,因为我们计算出来的签名值与钉钉的实际签名值就差一个“+”和“ ”,所以在最后直接替换就可以了
1 private bool GetSign(string timestamp, string secret, string sign)
2 {
3 try
4 {
5 //获取当前时间的时间戳
6 long currentTime = SymmetricMethod.ToUTC(DateTime.Now);
7 long dingTimestamp = long.Parse(timestamp);
8 long time = currentTime - dingTimestamp;
9 string stringToSign = SymmetricMethod.GetHmac(dingTimestamp + "\n" + secret, secret).ToString();
10 if (time < 3600000 && sign.Equals(stringToSign))
11 {
12 return true;
13 }
14 return false;
15 }
16 catch (Exception ex)
17 {
18 return false;
19 }
20 }
这样我们就获得了钉钉返回的sign 和timestamp 和我们自己计算出来的sign ,然后根据规则进行判断即可
那么最终合在一起形成这样一段代码
1 #region 机器人操作类
2 [WebMethod]
3 public void Reboot()
4 {
5 string result = "";
6 using (StreamReader reader = new StreamReader(HttpContext.Current.Request.InputStream, Encoding.UTF8))
7 {
8 result = reader.ReadToEnd();
9 }
10 try
11 {
12 string sign = HttpContext.Current.Request.Headers.GetValues("sign")[0].ToString();
13 string timestamp = HttpContext.Current.Request.Headers.GetValues("timestamp")[0].ToString();
14 string json = result;
15 CommonJsonModel model = SymmetricMethod.DeSerialize(json);
16 string text = model.GetModel("text").GetValue("content");
17 string sessionWebhook = model.GetValue("sessionWebhook");
18 string senderStaffId = model.GetValue("senderStaffId");
19 DBHelper.InsertRebootLog(HttpContext.Current.Request, HttpContext.Current.Request.Url.ToString(), "调用机器人", text + "--" + sessionWebhook + "--" + senderStaffId, sign + "----------" + timestamp, result, HttpContext.Current.Request.Headers, "调用机器人");
20
21 if (GetSign(timestamp, secret, sign))//验证,如果不通过另行操作或者不返回都可以
22 {
23 DefaultDingTalkClient client = new DefaultDingTalkClient(sessionWebhook);
24 text(client, userid, "返回文本测试效果");
25 markdown(client, userid, "测试markdown", "返回markdown测试效果");
26 actionCard(client, userid, "测试actionCard", "返回actionCard测试效果", "点击详情", "https://www.taiwei6.com");
27 }
28 }
29 catch (Exception ex)
30 {
31 DBHelper.InsertRebootLog(HttpContext.Current.Request, HttpContext.Current.Request.Url.ToString(), "调用机器人", result, ex.Message, "接口调用来源不正确","", "调用机器人");
32 }
33 }
钉钉机器人总共是能够范围三种类型的分别是text ,markdown,actioncard ,
上源码
1 /**
2 * 实现@人员
3 * @param client
4 * @param userId
5 * 返回文本
6 */
7 private void text(DefaultDingTalkClient client, String userId, string textcontent)
8 {
9 try
10 {
11 OapiRobotSendRequest request = new OapiRobotSendRequest();
12 request.Msgtype = "text";
13 OapiRobotSendRequest.TextDomain text = new OapiRobotSendRequest.TextDomain();
14 text.Content = " @" + userId + " \n " + textcontent;
15 request.Text_ = text;
16 OapiRobotSendRequest.AtDomain at = new OapiRobotSendRequest.AtDomain();
17
18 List<string> userids = new List<string>();
19 userids.Add(userId);
20 at.AtUserIds = userids;
21 // isAtAll类型如果不为Boolean,请升级至最新SDK
22 at.IsAtAll = false;
23 request.At_ = at;
24 OapiRobotSendResponse response = client.Execute(request);
25 int code = Convert.ToInt32(response.Errcode);
26 string msg = response.Errmsg;
27 }
28 catch (Exception e)
29 {
30
31 }
32 }
33
34 /**
35 * markdown@人员效果
36 *
37 * @param client
38 * @param userId
39 *
40 * 返回markdown
41 *
42 */
43 private void markdown(DefaultDingTalkClient client, String userId, string title, string textcontent)
44 {
45 try
46 {
47 OapiRobotSendRequest request = new OapiRobotSendRequest();
48 request.Msgtype = "markdown";
49 OapiRobotSendRequest.MarkdownDomain markdown = new OapiRobotSendRequest.MarkdownDomain();
50 markdown.Title = title;
51 markdown.Text = " @" + userId + " \n " + textcontent;
52 request.Markdown_ = markdown;
53 OapiRobotSendRequest.AtDomain at = new OapiRobotSendRequest.AtDomain();
54 List<string> userids = new List<string>();
55 userids.Add(userId);
56 at.AtUserIds = userids;
57 // isAtAll类型如果不为Boolean,请升级至最新SDK
58 at.IsAtAll = false;
59 request.At_ = at;
60 OapiRobotSendResponse response = client.Execute(request);
61 int code = Convert.ToInt32(response.Errcode);
62 string msg = response.Errmsg;
63 }
64 catch (Exception e)
65 {
66
67 }
68 }
69 /**
70 * actionCard@人员效果
71 * @param client
72 * @param userId
73 */
74 private void actionCard(DefaultDingTalkClient client, String userId, string title, string textcontent, string SingleTitle, string url)
75 {
76 try
77 {
78 OapiRobotSendRequest request = new OapiRobotSendRequest();
79 request.Msgtype = "actionCard";
80 OapiRobotSendRequest.ActioncardDomain actionCard = new OapiRobotSendRequest.ActioncardDomain();
81 actionCard.Title = title;
82 actionCard.Text = " @" + userId + " \n " + textcontent;
83 ;
84 actionCard.SingleTitle = SingleTitle;
85 actionCard.SingleURL = url;
86 request.ActionCard_ = actionCard;
87 OapiRobotSendRequest.AtDomain at = new OapiRobotSendRequest.AtDomain();
88 List<string> userids = new List<string>();
89 userids.Add(userId);
90 at.AtUserIds = userids;
91 // isAtAll类型如果不为Boolean,请升级至最新SDK
92 at.IsAtAll = false;
93 request.At_ = at;
94 OapiRobotSendResponse response = client.Execute(request);
95 int code = Convert.ToInt32(response.Errcode);
96 string msg = response.Errmsg;
97 }
98 catch (Exception e)
99 {
100
101 }
102 }
文档中还提到有几种markdown 的用法,分别是标题,引用,字体,链接,图片,有序列表,无序列表的使用,从他的案例中可以看出,只是传入的text加上特殊符号即可
标题
# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题
引用
> A man who stands for nothing will fall for anything.
文字加粗、斜体
**bold**
*italic*
链接
[this is a link](https://www.dingtalk.com/)
图片
![](http://name.com/pic.jpg)
无序列表
- item1
- item2
有序列表
1. item1
2. item2
换行(建议\n前后各添加两个空格)
\n
至此,开发钉钉群机器人的所有开发过程写完了。