使用asp.net开发钉钉群机器人全过程

[删除(380066935@qq.com或微信通知)]

更好的阅读体验请查看原文:https://www.cnblogs.com/taiwei6/p/16085321.html

集团是使用钉钉进行工作交流的, 发现群里有很多问题其实是重复的,就在想是不是可以使用钉钉的群机器人,虽然说的确是可以部分实现,但是感觉还是差点什么,而且公司内部很多东西也不方便放上去,所以就想开发一个群机器人,然后就看钉钉开发文档,发现是有这个功能的,就开始研究,官方文档使用的语言主要是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  

至此,开发钉钉群机器人的所有开发过程写完了。