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