NuGet包地址: https://www.nuget.org/packages/OYMLCN.WeChat.Core 由於來的OYMLCN.WeChat存在深度封裝,並沒有做完整的測試,對於使用不友好,現已重新構建SDK的接收消息被動回覆模塊。 現已做到最大程度的簡易化及模塊化,每個模塊都已完成單 ...
NuGet包地址:
https://www.nuget.org/packages/OYMLCN.WeChat.Core
由於來的OYMLCN.WeChat存在深度封裝,並沒有做完整的測試,對於使用不友好,現已重新構建SDK的接收消息被動回覆模塊。
現已做到最大程度的簡易化及模塊化,每個模塊都已完成單元測試(根據微信說明文檔的示例內容)。
接下來將會重新構建微信介面的調用模塊,將會在基本完成後發佈。
類JQuery操作的方式將會在所有功能完善並完成測試以後再整合到OYMLCN.WeChat當中。
主要使用方式:
var postModel = this.Request.GetQuery().IsValidRequest(Config); if (postModel == null) return Content(""); return Content(new DemoHandler(WeChatRequest.Build(Config, postModel, this.Request.GetBody().ReadToEnd())).Result);
Config是基礎介面配置OYMLCN.WeChat.Config的實例,參數依次為【公眾平臺微信號】、【AppId】、【AppSecret】、【Token】以及可選的【AESKey】。
配置的參數順序相對於以往版本有變更,若報錯無法驗證簽名有效性則需要檢查參數的賦值順序。
Request.GetQuery()是對Request的擴展方法,封裝在依賴包OYMLCN.Web當中,主要獲取請求附帶的參數。若在ASP.Net Web Api當中使用,需要安裝相容擴展包OYMLCN.Web.Api。
Request.GetBody()主要是獲取請求的正文,相當於Request.Body,為了使代碼相容WebApi而設置的一個方法。
ReadToEnd()是Stream的擴展方法,主要是讀取Stream中的文本內容。
IsValidRequest(Config)是Dictionary<string, string>的擴展方法,用於驗證請求的有效性並獲取PostModel參數。驗證失敗則返回null。
當前示例是使用Handler處理的。
public class DemoHandler : OYMLCN.WeChat.MessageHandler { public DemoHandler(OYMLCN.WeChat.WeChatRequest request) : base(request) { } public override OYMLCN.WeChat.WeChatResponse DefaultResponseMessage(OYMLCN.WeChat.WeChatRequest request) { return OYMLCN.WeChat.WeChatResponse.ResponseText(request, "Success"); } }
若需求簡單,可不使用Handler來處理,使用方式如下:
OYMLCN.WeChat.Config config = new OYMLCN.WeChat.Config("微信賬號名", "AppId", "AppSecret", "Token", "AESKey"); OYMLCN.WeChat.PostModel postModel = this.Request.GetQuery().IsValidRequest(config); string body = this.Request.GetBody().ReadToEnd(); OYMLCN.WeChat.WeChatRequest request = OYMLCN.WeChat.WeChatRequest.Build(config, postModel, body); OYMLCN.WeChat.WeChatResponse response = null; if (request.MessageType == OYMLCN.WeChat.WeChatRequestMessageType.Text) response = OYMLCN.WeChat.WeChatResponse.ResponseText(request, "我是測試"); else if (request.MessageType == OYMLCN.WeChat.WeChatRequestMessageType.Event) if (request.EventType == OYMLCN.WeChat.WeChatRequestEventType.Event關註) response = OYMLCN.WeChat.WeChatResponse.ResponseText(request, "你來晚了"); if (response != null) return Content(response.Result); return Content("");
附上單元測試代碼,基本用法均已展現。
1 using System; 2 using System.Collections.Generic; 3 using Microsoft.VisualStudio.TestTools.UnitTesting; 4 using System.Linq; 5 6 namespace OYMLCN.WeChat.Core.Test 7 { 8 [TestClass] 9 public class UnitTest 10 { 11 Config Config = new Config("wxName", "appId", "appSecret", "token", "aes"); 12 PostModel PostModel = PostModel.Build(new Dictionary<string, string>() 13 { 14 {"nonce","1362870167" }, 15 {"openid","oOk2XjhrbcHP3tGgzDGAVHppo3Bs" }, 16 {"signature","7940891098b505c22f99b0e3708627ec715aa832" }, 17 {"timestamp","1496218735" } 18 }); 19 20 [TestMethod] 21 public void WeChatRequestTest() 22 { 23 string textMsg = @"<xml> 24 <ToUserName><![CDATA[toUser]]></ToUserName> 25 <FromUserName><![CDATA[fromUser]]></FromUserName> 26 <CreateTime>1348831860</CreateTime> 27 <MsgType><![CDATA[text]]></MsgType> 28 <Content><![CDATA[this is a test]]></Content> 29 <MsgId>1234567890123456</MsgId> 30 </xml>"; 31 32 var request = WeChatRequest.Build(Config, PostModel, textMsg); 33 Assert.AreEqual(request.MessageType, WeChatRequestMessageType.Text); 34 Assert.AreEqual(request.ToUserName, "toUser"); 35 Assert.AreEqual(request.FromUserName, "fromUser"); 36 Assert.AreEqual(request.CreateTime, 1348831860); 37 Assert.AreEqual(request.MsgId, 1234567890123456); 38 Assert.AreEqual(request.MessageText.Content, "this is a test"); 39 40 string imgMsg = @"<xml> 41 <ToUserName><![CDATA[toUser]]></ToUserName> 42 <FromUserName><![CDATA[fromUser]]></FromUserName> 43 <CreateTime>1348831860</CreateTime> 44 <MsgType><![CDATA[image]]></MsgType> 45 <PicUrl><![CDATA[this is a url]]></PicUrl> 46 <MediaId><![CDATA[media_id]]></MediaId> 47 <MsgId>1234567890123456</MsgId> 48 </xml>"; 49 request = WeChatRequest.Build(Config, PostModel, imgMsg); 50 Assert.AreEqual(request.MessageType, WeChatRequestMessageType.Image); 51 Assert.AreEqual(request.ToUserName, "toUser"); 52 Assert.AreEqual(request.FromUserName, "fromUser"); 53 Assert.AreEqual(request.CreateTime, 1348831860); 54 Assert.AreEqual(request.MessageImage.PicUrl, "this is a url"); 55 Assert.AreEqual(request.MessageImage.MediaId, "media_id"); 56 Assert.AreEqual(request.MsgId, 1234567890123456); 57 58 string voiceMsg = @"<xml> 59 <ToUserName><![CDATA[toUser]]></ToUserName> 60 <FromUserName><![CDATA[fromUser]]></FromUserName> 61 <CreateTime>1357290913</CreateTime> 62 <MsgType><![CDATA[voice]]></MsgType> 63 <MediaId><![CDATA[media_id]]></MediaId> 64 <Format><![CDATA[Format]]></Format> 65 <MsgId>1234567890123456</MsgId> 66 </xml>"; 67 request = WeChatRequest.Build(Config, PostModel, voiceMsg); 68 Assert.AreEqual(request.MessageType, WeChatRequestMessageType.Voice); 69 Assert.AreEqual(request.ToUserName, "toUser"); 70 Assert.AreEqual(request.FromUserName, "fromUser"); 71 Assert.AreEqual(request.CreateTime, 1357290913); 72 Assert.AreEqual(request.MessageVoice.MediaId, "media_id"); 73 Assert.AreEqual(request.MessageVoice.Format, "Format"); 74 Assert.AreEqual(request.MsgId, 1234567890123456); 75 76 voiceMsg = @"<xml> 77 <ToUserName><![CDATA[toUser]]></ToUserName> 78 <FromUserName><![CDATA[fromUser]]></FromUserName> 79 <CreateTime>1357290913</CreateTime> 80 <MsgType><![CDATA[voice]]></MsgType> 81 <MediaId><![CDATA[media_id]]></MediaId> 82 <Format><![CDATA[Format]]></Format> 83 <Recognition><![CDATA[騰訊微信團隊]]></Recognition> 84 <MsgId>1234567890123456</MsgId> 85 </xml>"; 86 request = WeChatRequest.Build(Config, PostModel, voiceMsg); 87 Assert.AreEqual(request.MessageType, WeChatRequestMessageType.Voice); 88 Assert.AreEqual(request.ToUserName, "toUser"); 89 Assert.AreEqual(request.FromUserName, "fromUser"); 90 Assert.AreEqual(request.CreateTime, 1357290913); 91 Assert.AreEqual(request.MessageVoice.MediaId, "media_id"); 92 Assert.AreEqual(request.MessageVoice.Format, "Format"); 93 Assert.AreEqual(request.MessageVoice.Recognition, "騰訊微信團隊"); 94 Assert.AreEqual(request.MsgId, 1234567890123456); 95 96 var videoMsg = @"<xml> 97 <ToUserName><![CDATA[toUser]]></ToUserName> 98 <FromUserName><![CDATA[fromUser]]></FromUserName> 99 <CreateTime>1357290913</CreateTime> 100 <MsgType><![CDATA[video]]></MsgType> 101 <MediaId><![CDATA[media_id]]></MediaId> 102 <ThumbMediaId><![CDATA[thumb_media_id]]></ThumbMediaId> 103 <MsgId>1234567890123456</MsgId> 104 </xml>"; 105 request = WeChatRequest.Build(Config, PostModel, videoMsg); 106 Assert.AreEqual(request.MessageType, WeChatRequestMessageType.Video); 107 Assert.AreEqual(request.ToUserName, "toUser"); 108 Assert.AreEqual(request.FromUserName, "fromUser"); 109 Assert.AreEqual(request.CreateTime, 1357290913); 110 Assert.AreEqual(request.MessageVideo.MediaId, "media_id"); 111 Assert.AreEqual(request.MessageVideo.ThumbMediaId, "thumb_media_id"); 112 Assert.AreEqual(request.MsgId, 1234567890123456); 113 114 videoMsg = @"<xml> 115 <ToUserName><![CDATA[toUser]]></ToUserName> 116 <FromUserName><![CDATA[fromUser]]></FromUserName> 117 <CreateTime>1357290913</CreateTime> 118 <MsgType><![CDATA[shortvideo]]></MsgType> 119 <MediaId><![CDATA[media_id]]></MediaId> 120 <ThumbMediaId><![CDATA[thumb_media_id]]></ThumbMediaId> 121 <MsgId>1234567890123456</MsgId> 122 </xml>"; 123 request = WeChatRequest.Build(Config, PostModel, videoMsg); 124 Assert.AreEqual(request.MessageType, WeChatRequestMessageType.ShortVideo); 125 Assert.AreEqual(request.ToUserName, "toUser"); 126 Assert.AreEqual(request.FromUserName, "fromUser"); 127 Assert.AreEqual(request.CreateTime, 1357290913); 128 Assert.AreEqual(request.MessageVideo.MediaId, "media_id"); 129 Assert.AreEqual(request.MessageVideo.ThumbMediaId, "thumb_media_id"); 130 Assert.AreEqual(request.MsgId, 1234567890123456); 131 132 var locationMsg = @"<xml> 133 <ToUserName><![CDATA[toUser]]></ToUserName> 134 <FromUserName><![CDATA[fromUser]]></FromUserName> 135 <CreateTime>1351776360</CreateTime> 136 <MsgType><![CDATA[location]]></MsgType> 137 <Location_X>23.134521</Location_X> 138 <Location_Y>113.358803</Location_Y> 139 <Scale>20</Scale> 140 <Label><![CDATA[位置信息]]></Label> 141 <MsgId>1234567890123456</MsgId> 142 </xml>"; 143 request = WeChatRequest.Build(Config, PostModel, locationMsg); 144 Assert.AreEqual(request.MessageType, WeChatRequestMessageType.Location); 145 Assert.AreEqual(request.ToUserName, "toUser"); 146 Assert.AreEqual(request.FromUserName, "fromUser"); 147 Assert.AreEqual(request.CreateTime, 1351776360); 148 Assert.AreEqual(request.MessageLocation.Location_X, 23.134521); 149 Assert.AreEqual(request.MessageLocation.Location_Y, 113.358803); 150 Assert.AreEqual(request.MessageLocation.Scale, 20); 151 Assert.AreEqual(request.MessageLocation.Label, "位置信息"); 152 Assert.AreEqual(request.MsgId, 1234567890123456); 153 154 var linkMsg = @"<xml> 155 <ToUserName><![CDATA[toUser]]></ToUserName> 156 <FromUserName><![CDATA[fromUser]]></FromUserName> 157 <CreateTime>1351776360</CreateTime> 158 <MsgType><![CDATA[link]]></MsgType> 159 <Title><![CDATA[公眾平臺官網鏈接]]></Title> 160 <Description><![CDATA[公眾平臺官網鏈接]]></Description> 161 <Url><![CDATA[url]]></Url> 162 <MsgId>1234567890123456</MsgId> 163 </xml>"; 164 request = WeChatRequest.Build(Config, PostModel, linkMsg); 165 Assert.AreEqual(request.MessageType, WeChatRequestMessageType.Link); 166 Assert.AreEqual(request.ToUserName, "toUser"); 167 Assert.AreEqual(request.FromUserName, "fromUser"); 168 Assert.AreEqual(request.CreateTime, 1351776360); 169 Assert.AreEqual(request.MessageLink.Title, "公眾平臺官網鏈接"); 170 Assert.AreEqual(request.MessageLink.Description, "公眾平臺官網鏈接"); 171 Assert.AreEqual(request.MessageLink.Url, "url"); 172 Assert.AreEqual(request.MsgId, 1234567890123456); 173 174 } 175 176 [TestMethod] 177 public void WeChatRequestEventTest() 178 { 179 var subscribeEvent = @"<xml> 180 <ToUserName><![CDATA[toUser]]></ToUserName> 181 <FromUserName><![CDATA[FromUser]]></FromUserName> 182 <CreateTime>123456789</CreateTime> 183 <MsgType><![CDATA[event]]></MsgType> 184 <Event><![CDATA[subscribe]]></Event> 185 </xml>"; 186 var request = WeChatRequest.Build(Config, PostModel, subscribeEvent); 187 Assert.AreEqual(request.MessageType, WeChatRequestMessageType.Event); 188 Assert.AreEqual(request.EventType, WeChatRequestEventType.Event關註); 189 Assert.AreEqual(request.ToUserName, "toUser"); 190 Assert.AreEqual(request.FromUserName, "FromUser"); 191 Assert.AreEqual(request.CreateTime, 123456789); 192 Assert.IsTrue(request.Event關註); 193 194 subscribeEvent = @"<xml> 195 <ToUserName><![CDATA[toUser]]></ToUserName> 196 <FromUserName><![CDATA[FromUser]]></FromUserName> 197 <CreateTime>123456789</CreateTime> 198 <MsgType><![CDATA[event]]></MsgType> 199 <Event><![CDATA[unsubscribe]]></Event> 200 </xml>"; 201 request = WeChatRequest.Build(Config, PostModel, subscribeEvent); 202 Assert.IsTrue(request.Event取消關註); 203 204 subscribeEvent = @"<xml> 205 <ToUserName><![CDATA[toUser]]></ToUserName> 206 <FromUserName><![CDATA[FromUser]]></FromUserName> 207 <CreateTime>123456789</CreateTime> 208 <MsgType><![CDATA[event]]></MsgType> 209 <Event><![CDATA[subscribe]]></Event> 210 <EventKey><![CDATA[qrscene_123123]]></EventKey> 211 <Ticket><![CDATA[TICKET]]></Ticket> 212 </xml>"; 213 request = WeChatRequest.Build(Config, PostModel, subscribeEvent); 214 Assert.AreEqual(request.MessageType, WeChatRequestMessageType.Event); 215 Assert.AreEqual(request.EventType, WeChatRequestEventType.Event關註); 216 Assert.AreEqual(request.ToUserName, "toUser"); 217 Assert.AreEqual(request.FromUserName, "FromUser"); 218 Assert.AreEqual(request.CreateTime, 123456789); 219 Assert.AreEqual(request.Event掃描帶參數二維碼.EventKey, "qrscene_123123"); 220 Assert.AreEqual(request.Event掃描帶參數二維碼.SceneId, "123123"); 221 Assert.AreEqual(request.Event掃描帶參數二維碼.Ticket, "TICKET"); 222 223 subscribeEvent = @"<xml> 224 <ToUserName><![CDATA[toUser]]></ToUserName> 225 <FromUserName><![CDATA[FromUser]]></FromUserName> 226 <CreateTime>123456789</CreateTime> 227 <MsgType><![CDATA[event]]></MsgType> 228 <Event><![CDATA[SCAN]]></Event> 229 <EventKey><![CDATA[SCENE_VALUE]]></EventKey> 230 <Ticket><![CDATA[TICKET]]></Ticket> 231 </xml>"; 232 request = WeChatRequest.Build(Config, PostModel, subscribeEvent); 233 Assert.AreEqual(request.MessageType, WeChatRequestMessageType.Event); 234 Assert.AreEqual(request.EventType, WeChatRequestEventType.Event掃描帶參數二維碼); 235 Assert.AreEqual(request.ToUserName, "toUser"); 236 Assert.AreEqual(request.FromUserName, "FromUser"); 237 Assert.AreEqual(request.CreateTime, 123456789); 238 Assert.AreEqual(request.Event掃描帶參數二維碼.EventKey, "SCENE_VALUE"); 239 Assert.AreEqual(request.Event掃描帶參數二維碼.SceneId, "SCENE_VALUE"); 240 Assert.AreEqual(request.Event掃描帶參數二維碼.Ticket, "TICKET"); 241 242 243 var locationEvent = @"<xml> 244 <ToUserName><![CDATA[toUser]]></ToUserName> 245 <FromUserName><![CDATA[fromUser]]></FromUserName> 246 <CreateTime>123456789</CreateTime> 247 <MsgType><![CDATA[event]]></MsgType> 248 <Event><![CDATA[LOCATION]]></Event> 249 <Latitude>23.137466</Latitude> 250 <Longitude>113.352425</Longitude> 251 <Precision>119.385040</Precision> 252 </xml>"; 253 request = WeChatRequest.Build(Config, PostModel, locationEvent); 254 Assert.AreEqual(request.MessageType, WeChatRequestMessageType.Event); 255 Assert.AreEqual(request.EventType, WeChatRequestEventType.Event上報地理位置); 256 Assert.AreEqual(request.ToUserName, "toUser"); 257 Assert.AreEqual(request.FromUserName, "fromUser"); 258 Assert.AreEqual(request.CreateTime, 123456789); 259 Assert.AreEqual(request.Event上報地理位置.Latitude, 23.137466); 260 Assert.AreEqual(request.Event上報地理位置.Longitude, 113.352425); 261 Assert.AreEqual(request.Event上報地理位置.Precision, 119.385040); 262 263 var clickEvent = @"<xml> 264 <ToUserName><![CDATA[toUser]]></ToUserName> 265 <FromUserName><![CDATA[FromUser]]></FromUserName> 266 <CreateTime>123456789</CreateTime> 267 <MsgType><![CDATA[event]]></MsgType> 268 <Event><![CDATA[CLICK]]></Event> 269 <EventKey><![CDATA[EVENTKEY]]></EventKey> 270 </xml>"; 271 request = WeChatRequest.Build(Config, PostModel, clickEvent); 272 Assert.AreEqual(request.MessageType, WeChatRequestMessageType.Event); 273 Assert.AreEqual(request.EventType, WeChatRequestEventType.Event點擊自定義菜單); 274 Assert.AreEqual(request.ToUserName, "toUser"); 275 Assert.AreEqual(request.FromUserName, "FromUser"); 276 Assert.AreEqual(request.CreateTime, 123456789); 277 Assert.AreEqual(request.Event點擊自定義菜單.EventKey, "EVENTKEY"); 278 279 var linkEvent = @"<xml> 280 <ToUserName><![CDATA[toUser]]></ToUserName> 281 <FromUserName><![CDATA[FromUser]]></FromUserName> 282 <CreateTime>123456789</CreateTime> 283 <MsgType><![CDATA[event]]></MsgType> 284 <Event><![CDATA[VIEW]]></Event> 285 <EventKey><![CDATA[www.qq.com]]></EventKey> 286 <MenuId>123</MenuId> 287 </xml>"; 288 request = WeChatRequest.Build(Config, PostModel, linkEvent); 289 Assert.AreEqual(request.MessageType, WeChatRequestMessageType.Event); 290 Assert.AreEqual(request.EventType, WeChatRequestEventType.Event點擊菜單跳轉鏈接); 291 Assert.AreEqual(request.ToUserName, "toUser"); 292 Assert.AreEqual(request.FromUserName, "FromUser"); 293 Assert.AreEqual(request.CreateTime, 123456789); 294 Assert.AreEqual(request.Event點擊菜單跳轉鏈接.Url, "www.qq.com"); 295 Assert.AreEqual(request.Event點擊菜單跳轉鏈接.MenuId, 123); 296 297 } 298 299 [TestMethod] 300 public void WeChatRequestPushEventTest() 301 { 302 #region 模板消息發送結果 303 var tempplatePush = @"<xml> 304 <ToUserName><![CDATA[gh_7f083739789a]]></ToUserName> 305 <FromUserName><![CDATA[oia2TjuEGTNoeX76QEjQNrcURxG8]]></FromUserName> 306 <CreateTime>1395658920</CreateTime> 307 <MsgType><![CDATA[event]]></MsgType> 308 <Event><![CDATA[TEMPLATESENDJOBFINISH]]></Event> 309 <MsgID>200163836</MsgID> 310 <Status><![CDATA[success]]></Status> 311 </xml>"; 312 var request = WeChatRequest.Build(Config, PostModel, tempplatePush); 313 Assert.AreEqual(request.MessageType, WeChatRequestMessageType.Event); 314 Assert.AreEqual(request.EventType, WeChatRequestEventType.Push模板消息發送結果); 315 Assert.AreEqual(request.ToUserName, "gh_7f083739789a"); 316 Assert.AreEqual(request.FromUserName, "oia2TjuEGTNoeX76QEjQNrcURxG8"); 317 Assert.AreEqual(request.CreateTime, 1395658920); 318 Assert.AreEqual(request.Push模板消息發送結果.Status, "success"); 319 Assert.IsTrue(request.Push模板消息發送結果.Success); 320 321 tempplatePush = @"<xml> 322 <ToUserName><![CDATA[gh_7f083739789a]]></ToUserName> 323 <FromUserName><![CDATA[oia2TjuEGTNoeX76QEjQNrcURxG8]]></FromUserName> 324 <CreateTime>1395658984</CreateTime> 325 <MsgType><![CDATA[event]]></MsgType> 326 <Event><![CDATA[TEMPLATESENDJOBFINISH]]></Event> 327 <MsgID>200163840</MsgID> 328 <Status><![CDATA[failed:userblock]]></Status> 329 </xml>"; 330 request = WeChatRequest.Build(Config, PostModel, tempplatePush); 331 Assert.AreEqual(request.MessageType, WeChatRequestMessageType.Event); 332 Assert.AreEqual(request.EventType, WeChatRequestEventType.Push模板消息發送結果); 333 Assert.AreEqual(request.ToUserName, "gh_7f083739789a"); 334 Assert.AreEqual(request.FromUserName, "oia2TjuEGTNoeX76QEjQNrcURxG8"); 335 Assert.AreEqual(request.CreateTime, 1395658984); 336 Assert.AreEqual(request.Push模板消息發送結果.Status, "failed:userblock"); 337 Assert.IsFalse(request.Push模板消息發送結果.Success); 338 tempplatePush = @"<xml> 339 <ToUserName><![CDATA[gh_7f083739789a]]></ToUserName> 340 <FromUserName><![CDATA[oia2TjuEGTNoeX76QEjQNrcURxG8]]></FromUserName> 341 <CreateTime>1395658984</CreateTime> 342 <MsgType><![CDATA[event]]></MsgType> 343 <Event><![CDATA[TEMPLATESENDJOBFINISH]]></Event> 344 <MsgID>200163840</MsgID> 345 <Status><![CDATA[failed:system failed]]></Status> 346 </xml>"; 347 request = WeChatRequest.Build(Config, PostModel, tempplatePush); 348 Assert.AreEqual(request.MessageType, WeChatRequestMessageType.Event); 349 Assert.AreEqual(request.EventType, WeChatRequestEventType.Push模板消息發送結果); 350 Assert.AreEqual(request.ToUserName, "gh_7f083739789a"); 351 Assert.AreEqual(request.FromUserName, "oia2TjuEGTNoeX76QEjQNrcURxG8"); 352 Assert.AreEqual(request.CreateTime, 1395658984); 353 Assert.AreEqual(request.Push模板消息發送結果.Status, "failed:system failed"); 354 Assert.IsFalse(request.Push模板消息發送結果.Success); 355 #endregion 356 357 #region 群髮結果 358 var massResultPush = @"<xml> 359 <ToUserName><![CDATA[gh_4d00ed8d6399]]></ToUserName> 360 <FromUserName><![