使用WeChat 企業號 API提供的回調模式(Callback),側錄企業號中所有人員在群組內的交談內容。
此篇是在WeChat 版的Chat Bot Release後,被上面的大老闆要求打通的功能。
以下是開發時的筆記
開發語言與使用套件:
-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.foxlink</groupId> <artifactId>FoxlinkChatBot</artifactId> <version>1.0</version> <packaging>war</packaging> <dependencies> <!-- log --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>com.googlecode.json-simple</groupId> <artifactId>json-simple</artifactId> <version>1.1.1</version> </dependency> <!-- Spring Framework --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.2.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>4.2.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>4.2.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.2.5.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>4.2.5.RELEASE</version> </dependency> <!-- JSON --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.7</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- Jackson-databind --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.2</version> </dependency> <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jsr310 --> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.9.2</version> </dependency> <!-- https://mvnrepository.com/artifact/com.google.guava/guava --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>23.2-jre</version> </dependency> <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>0.10.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>commons-digester</groupId> <artifactId>commons-digester</artifactId> <version>2.1</version> </dependency> <!-- https://mvnrepository.com/artifact/ai.api/libai --> <dependency> <groupId>ai.api</groupId> <artifactId>libai</artifactId> <version>1.6.12</version> </dependency> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>6.0.6</version> </dependency> <!-- https://mvnrepository.com/artifact/javax.servlet/servlet-api --> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-util --> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-util</artifactId> <version>8.5.23</version> </dependency> <!-- https://mvnrepository.com/artifact/commons-io/commons-io --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.6</version> </dependency> <!-- Oracle JDBC --> <dependency> <groupId>com.oracle</groupId> <artifactId>ojdbc6</artifactId> <version>11.2.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.7</version> </dependency> <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-simple --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.7</version> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/javax.mail/mail --> <dependency> <groupId>javax.mail</groupId> <artifactId>mail</artifactId> <version>1.4.7</version> </dependency> <dependency> <groupId>com.qq.weixin</groupId> <artifactId>commons-codec</artifactId> <version>1.9</version> </dependency> <!-- XML TO JSON Convert --> <dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <version>20160212</version> </dependency> <!-- https://mvnrepository.com/artifact/org.javatuples/javatuples --> <dependency> <groupId>org.javatuples</groupId> <artifactId>javatuples</artifactId> <version>1.2</version> </dependency> </dependencies> <build> <resources> <resource> <directory>src</directory> <includes> <include>Beans.xml</include> <include>log4j.xml</include> </includes> </resource> </resources> <sourceDirectory>src</sourceDirectory> <plugins> <plugin> <artifactId>maven-war-plugin</artifactId> <version>3.0.0</version> <configuration> <warSourceDirectory>WebContent</warSourceDirectory> </configuration> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.7.0</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> </project>
開發步驟
-
建立Controller
- Controller 內實作handleGroupChatCallBack 的 Get 與 Post 方法
- handleGroupChatCallBack 的 Get 方法主要提供WeChat Server進行驗證
-
@RequestMapping(value="/wechatcallback",method=RequestMethod.GET) @ResponseBody public String handleWeChatCallBack(@RequestParam(value="msg_signature")String msgSignature, @RequestParam(value="timestamp")String timestamp,@RequestParam(value="nonce")String nonce, @RequestParam(value="echostr")String echostr) { String decryptContent=""; WeChatUtils weChatUtil=null; try { System.out.println("MsgSignature: "+msgSignature+", TimeStamp: "+timestamp+", Nonce: "+nonce+", Echostr: "+echostr); weChatUtil=new WeChatUtils(WeChatToken,WeChatAESKey,WeChatCorpID); decryptContent=weChatUtil.VerifyCallbackURL(msgSignature, timestamp, nonce, echostr); logger.info("Verify wechat callback URL , the decrypt content: "+decryptContent); System.out.println("Verify wechat callback URL , the decrypt content: "+decryptContent); } catch(Exception ex) { logger.error("Verify WeChat Callback failed , due to: ",ex); System.out.println("Verify WeChat Callback failed , due to: "+ex.toString()); } return decryptContent; }
- handleGroupChatCallBack 的 Get方法用於接收WeChat Server傳入的Package
-
@RequestMapping(value="/wechatcallback",method=RequestMethod.POST) public ResponseEntity handleGroupChatCallBack(HttpServletRequest request, HttpServletResponse response, @RequestParam(value="msg_signature")String msgSignature, @RequestParam(value="timestamp")String timestamp, @RequestParam(value="nonce")String nonce) { String httpRequestBody=null; WeChatUtils weChatUtil=null; String packageID=null; WeChatRecorderService recorderService=null; try { weChatUtil=new WeChatUtils(WeChatToken,WeChatAESKey,WeChatCorpID); request.setCharacterEncoding("UTF-8"); InputStream inputStream=request.getInputStream(); BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream,"utf-8")); httpRequestBody=IOUtils.toString(reader); System.out.println("Http Request Body: "+httpRequestBody); Pair<HashMap<String,List> ,String> tuples=weChatUtil.DecryptGroupChatMsg(msgSignature, timestamp, nonce, httpRequestBody); HashMap<String,List> DecryptMsgDict=(HashMap<String, List>) tuples.getValue(0); packageID=(String) tuples.getValue(1); System.out.print("Package ID:"+packageID); recorderService=new WeChatRecorderService(); recorderService.RecordWeChatEvent(DecryptMsgDict); } catch(Exception ex) { logger.error("Chat Recorder is failed ",ex); ex.printStackTrace(); } return ResponseEntity.ok().contentType(MediaType.TEXT_PLAIN).body(packageID); }
-
處理WeChat訊息加解密
- WeChat 企業會話訊息:text訊息、image訊息、voice訊息、file訊息、link訊息 與 location訊息
- WeChat企業會話事件:建立會話(群聊群組)、修改會話(成員增刪、會話名稱修改...等)及 服務事件(關注/取消關注 企業號)
- 訊息解密的Source Code如下
-
public Pair<HashMap<String,List> ,String> DecryptGroupChatMsg(String sReqMsgSig,String sReqTimeStamp,String sReqNonce,String sReqData) { List<CreateChat> newChatGroups=new ArrayList<CreateChat>(); List<UpdateChat> updateChatGroups=new ArrayList<UpdateChat>(); List<GroupChatMsg> groupChatMsgs=new ArrayList<GroupChatMsg>(); HashMap<String, List> dictMap = new HashMap<String, List>(); WXBizMsgCrypt wxcpt=null; String decryptMsg=null; String PackageID=null; try { System.out.println("----- Start To decrypt WeChat Messages -----"); wxcpt= new WXBizMsgCrypt(sToken, sEncodingAESKey, sCorpID); decryptMsg=wxcpt.DecryptMsg(sReqMsgSig, sReqTimeStamp, sReqNonce, sReqData); System.out.println("----- Decrypt Message Contents: "+decryptMsg+" -----"); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); StringReader sr = new StringReader(decryptMsg); InputSource is = new InputSource(sr); Document document = db.parse(is); Element root = document.getDocumentElement(); System.out.println("Root Element: "+root); NodeList nodes=root.getElementsByTagName("Item"); PackageID=root.getElementsByTagName("PackageId").item(0).getTextContent(); for(int i=0;i<nodes.getLength();i++) { Element element =(Element) nodes.item(i); String msgType=element.getElementsByTagName("MsgType").item(0).getTextContent(); System.out.println("Node Element: "+msgType); switch(msgType) { case "event": String eventType=element.getElementsByTagName("Event").item(0).getTextContent(); if(eventType.equals("create_chat")) { CreateChat newChatGroup=new CreateChat(); newChatGroup.setChatID(element.getElementsByTagName("ChatId").item(0).getTextContent()); newChatGroup.setChatName(element.getElementsByTagName("Name").item(0).getTextContent()); newChatGroup.setOwner(element.getElementsByTagName("Owner").item(0).getTextContent()); newChatGroup.setUserList(new Utils().SplitString2List(element.getElementsByTagName("UserList").item(0).getTextContent(),"|")); newChatGroup.setFromUserName(element.getElementsByTagName("Owner").item(0).getTextContent()); newChatGroup.setCreateTime(Long.parseLong(element.getElementsByTagName("CreateTime").item(0).getTextContent())); newChatGroup.setMsgType(msgType); newChatGroup.setEvent(element.getElementsByTagName("Event").item(0).getTextContent()); newChatGroups.add(newChatGroup); break; } else if(eventType.equals("update_chat")) { UpdateChat updateChatGroup=new UpdateChat(); updateChatGroup.setChatID(element.getElementsByTagName("ChatId").item(0).getTextContent()); updateChatGroup.setChatName(element.getElementsByTagName("Name").item(0).getTextContent()); updateChatGroup.setOwner(element.getElementsByTagName("Owner").item(0).getTextContent()); updateChatGroup.setAddUserList(new Utils().SplitString2List(element.getElementsByTagName("AddUserList").item(0).getTextContent(), "|")); updateChatGroup.setDelUserList(new Utils().SplitString2List(element.getElementsByTagName("DelUserList").item(0).getTextContent(), "|")); updateChatGroup.setFromUserName(element.getElementsByTagName("Owner").item(0).getTextContent()); updateChatGroup.setCreateTime(Long.parseLong(element.getElementsByTagName("CreateTime").item(0).getTextContent())); updateChatGroup.setMsgType(msgType); updateChatGroup.setEvent(element.getElementsByTagName("Event").item(0).getTextContent()); updateChatGroups.add(updateChatGroup); } else if(eventType.equals("quit_chat")) { } else { } break; case "text": case "image": case "voice": case "file": case "link": GroupChatMsg groupChatMsg=new GroupChatMsg(); groupChatMsg.setFromUserName(element.getElementsByTagName("FromUserName").item(0).getTextContent()); groupChatMsg.setCreateTime(Long.parseLong(element.getElementsByTagName("CreateTime").item(0).getTextContent())); groupChatMsg.setMsgType(msgType); groupChatMsg.setContent(element.getElementsByTagName("Content").item(0).getTextContent()); groupChatMsg.setMsgId(element.getElementsByTagName("MsgId").item(0).getTextContent()); /*Receiver ID 與 Receiver Type要再對下一層取值*/ Element ReceiverRoot=(Element) element.getElementsByTagName("Receiver").item(0); groupChatMsg.setReceiverId(ReceiverRoot.getElementsByTagName("Id").item(0).getTextContent()); groupChatMsg.setReceiverType(ReceiverRoot.getElementsByTagName("Type").item(0).getTextContent()); if(msgType.equals("file") || msgType.equals("image")||msgType.equals("voice")) { groupChatMsg.setMediaId(element.getElementsByTagName("MediaId").item(0).getTextContent()); } if(msgType.equals("image")||msgType.equals("link")) { groupChatMsg.setMediaUrl(element.getElementsByTagName("PicUrl").item(0).getTextContent()); } if(msgType.equals("link")) { groupChatMsg.setUrl(element.getElementsByTagName("Url").item(0).getTextContent()); } groupChatMsgs.add(groupChatMsg); break; } } dictMap.put("create_chat", newChatGroups); dictMap.put("update_chat", updateChatGroups); dictMap.put("message_event", groupChatMsgs); } catch(Exception ex) { logger.error("Decrypt WeChat Message Is failed ",ex); ex.printStackTrace(); } Pair<HashMap<String,List> ,String> tuples=new Pair(dictMap, PackageID); return tuples; }
-
測試
- 將上述的方法都建立後,我們實際在WeChat企業號發送訊息進行測試
- 在WeChat 企業號 Client 發送訊息,如下圖所示
- 到資料庫中尋找剛才發送的訊息,如下圖所示
- 在WeChat 企業號 Client 發送訊息,如下圖所示
以上,為簡易版本的WeChat 企業會話服務的開發