使用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 stringsign = 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
至此,開發釘釘群機器人的所有開發過程寫完了。