WeChat 企業號會話服務開發(callback)

使用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進行驗證
    1. 	@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
    1. @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 企業會話服務的開發