├── README.md ├── bots-industrial ├── net_maclife_wechat_http_Bot_HCICloudCSR.java └── net_maclife_wechat_http_Bot_iConTek.java ├── bots ├── net_maclife_wechat_http_Bot_ActiveDirectoryAddressBook.java ├── net_maclife_wechat_http_Bot_BaiduImageSearch.java ├── net_maclife_wechat_http_Bot_BaiduOCR.java ├── net_maclife_wechat_http_Bot_BaiduTranslate.java ├── net_maclife_wechat_http_Bot_BaiduVoice.java ├── net_maclife_wechat_http_Bot_Emoji.java ├── net_maclife_wechat_http_Bot_GoogleImageSearch.java ├── net_maclife_wechat_http_Bot_MakeFriend.java ├── net_maclife_wechat_http_Bot_Manager.java ├── net_maclife_wechat_http_Bot_MissileLaunched_JustForFun.java ├── net_maclife_wechat_http_Bot_Relay.java ├── net_maclife_wechat_http_Bot_Repeater.java ├── net_maclife_wechat_http_Bot_SaveContactsToDatabase.java ├── net_maclife_wechat_http_Bot_SaveMessagePackageToDatabase.java ├── net_maclife_wechat_http_Bot_SayHi.java ├── net_maclife_wechat_http_Bot_ShellCommand.java ├── net_maclife_wechat_http_Bot_SimpleAddressBook.java ├── net_maclife_wechat_http_Bot_WebSoup.java ├── net_maclife_wechat_http_Bot_XunFeiYun.java └── net_maclife_wechat_http_Bot_糗事百科热门.java ├── doc ├── ReadMe.Chinglish.md ├── ReadMe.中文.md └── img │ ├── bot-active-directory-address-book-50%.png │ ├── bot-baidu-image-search-50%.png │ ├── bot-baidu-translate-50%.png │ ├── bot-baidu-translate.png │ ├── bot-baidu-voice-50%.png │ ├── bot-baidu-voice.png │ ├── bot-emoji-50%.png │ ├── bot-emoji.png │ ├── bot-iConTek-50%.png │ ├── bot-make-friend-addme-50%.png │ ├── bot-make-friend-addme.png │ ├── bot-manager-50%.png │ ├── bot-manager.png │ ├── bot-missile-launched-50%.png │ ├── bot-missile-launched.png │ ├── bot-relay.message-push-qiushibaike.png │ ├── bot-relay.notify-transmission-download-complete-50%.png │ ├── bot-relay.scheduled-sign-50%.png │ ├── bot-shell-command-50%.png │ ├── bot-shell-command.png │ ├── bot-simple-address-book-50%.png │ └── text-QR-code.png ├── logging.properties ├── run.sh ├── sql ├── emoji.mysql.sql ├── save-messages.mysql.sql ├── simple-address-book.mysql.sql └── wechat-contacts.mysql.sql └── src ├── config.dist.properties ├── net_maclife_util_ANSIEscapeTool.java ├── net_maclife_util_BaiduCloud.java ├── net_maclife_util_HTTPUtils.java ├── net_maclife_wechat_http_Bot.java ├── net_maclife_wechat_http_BotApp.java └── net_maclife_wechat_http_BotEngine.java /README.md: -------------------------------------------------------------------------------- 1 | doc/ReadMe.中文.md -------------------------------------------------------------------------------- /bots-industrial/net_maclife_wechat_http_Bot_HCICloudCSR.java: -------------------------------------------------------------------------------- 1 | import java.io.*; 2 | import java.util.*; 3 | 4 | import org.apache.commons.lang3.*; 5 | 6 | import com.fasterxml.jackson.core.*; 7 | import com.fasterxml.jackson.databind.*; 8 | 9 | /** 10 | * 基于 http 协议的捷通华声 HCICloud CSR (灵云智能客服) 对话机器人。 11 | * 12 | * @author liuyan 13 | * 14 | */ 15 | public class net_maclife_wechat_http_Bot_HCICloudCSR extends net_maclife_wechat_http_Bot 16 | { 17 | String HCICLOUD_SERVER_ADDRESS = net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.hcicloud.csr.server.address"); 18 | int HCICLOUD_SERVER_PORT = net_maclife_wechat_http_BotApp.GetConfig ().getInt ("bot.hcicloud.csr.server.port"); 19 | String HCICLOUD_CSR_URL__Query = "http://" + HCICLOUD_SERVER_ADDRESS + (HCICLOUD_SERVER_PORT==80 ? "" : ":" + HCICLOUD_SERVER_PORT) + "/CSRBroker/queryAction"; 20 | 21 | String HCICLOUD_APP_KEY = net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.hcicloud.csr.app.key"); 22 | 23 | String HCICLOUD_CSR_ROBOT_ID = net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.hcicloud.csr.robot.id"); 24 | String HCICLOUD_CSR_ROBOT_CHANNEL_NUMBER = net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.hcicloud.csr.robot.channel.number"); 25 | String HCICLOUD_CSR_TALKER_ID = net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.hcicloud.csr.robot.talker.id"); 26 | String HCICLOUD_CSR_RECEIVER_ID = net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.hcicloud.csr.robot.receiver.id"); 27 | String HCICLOUD_CHARSET_ENCODING = net_maclife_wechat_http_BotApp.utf8; 28 | 29 | @Override 30 | public int OnTextMessageReceived 31 | ( 32 | JsonNode jsonMessage, 33 | JsonNode jsonFrom, String sFromAccount, String sFromName, boolean isFromMe, 34 | JsonNode jsonTo, String sToAccount, String sToName, boolean isToMe, 35 | JsonNode jsonReplyTo, String sReplyToAccount, String sReplyToName, boolean isReplyToRoom, 36 | JsonNode jsonReplyTo_RoomMember, String sReplyToAccount_RoomMember, String sReplyToName_RoomMember, 37 | JsonNode jsonReplyTo_Person, String sReplyToAccount_Person, String sReplyToName_Person, 38 | String sContent, boolean isContentMentionedMe, boolean isContentMentionedMeFirst 39 | ) 40 | { 41 | try 42 | { 43 | JsonNode jsonCSRResponse = GetCSRReponse (sFromAccount, sContent); 44 | if (jsonCSRResponse == null) 45 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 46 | 47 | String sResponse = ParseCSRResponse (jsonCSRResponse); 48 | if (StringUtils.isNotEmpty (sResponse)) 49 | { 50 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, sResponse); 51 | } 52 | } 53 | catch (Exception e) 54 | { 55 | e.printStackTrace (); 56 | } 57 | 58 | return 59 | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__PROCESSED 60 | | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 61 | } 62 | 63 | public String ParseCSRResponse (JsonNode jsonCSRResponse) 64 | { 65 | net_maclife_wechat_http_BotApp.logger.finer ("\n" + jsonCSRResponse); 66 | if (jsonCSRResponse == null || jsonCSRResponse.isNull()) 67 | return ""; 68 | 69 | int nProtocolId = net_maclife_wechat_http_BotApp.GetJSONInt (jsonCSRResponse, "protocolId"); 70 | assert (nProtocolId == 6); 71 | 72 | int nResult = net_maclife_wechat_http_BotApp.GetJSONInt (jsonCSRResponse, "result"); 73 | 74 | //JsonNode nodeSendTime = jsonResponse.get ("sendTime"); 75 | //int nAnswerTypeID = net_maclife_wechat_http_BotApp.GetJSONInt (jsonCSRResponse, "answerTypeId"); 76 | JsonNode nodeSingleNode = jsonCSRResponse.get ("singleNode"); 77 | JsonNode nodeVagueNode = jsonCSRResponse.get ("vagueNode"); 78 | 79 | String sResult = ""; 80 | if (nResult == 0) 81 | { 82 | /* 83 | switch (nAnswerTypeID) 84 | { 85 | case 1: // 系统错误 86 | case 2 // 有敏感词 87 | case 3: // 无法回答 88 | 89 | case 4: // 需要补问 90 | case 6: // 能够回答 (正常情况) 91 | case 8: // 模式匹配 92 | 93 | case 10: // 聊天过频 94 | sResult = nodeSingleNode 95 | break; 96 | case 7: // 无法回答 97 | sResult = "无法回答"; 98 | break; 99 | } 100 | */ 101 | if (nodeSingleNode != null && !nodeSingleNode.isNull()) 102 | { 103 | sResult = nodeSingleNode.get ("answerMsg").asText (); 104 | } 105 | if (nodeVagueNode != null && !nodeVagueNode.isNull()) 106 | { 107 | JsonNode jsonItemList = nodeVagueNode.get ("itemList"); 108 | if (jsonItemList!=null && !jsonItemList.isNull () && jsonItemList.size ()>0) 109 | { 110 | StringBuilder sbPromptMessages = new StringBuilder (); 111 | sbPromptMessages.append (nodeVagueNode.get ("promptVagueMsg").asText ()); 112 | sbPromptMessages.append ("\n"); 113 | for (JsonNode item : nodeVagueNode.get ("itemList")) 114 | { 115 | sbPromptMessages.append (item.get ("num").asInt ()); 116 | sbPromptMessages.append (". "); 117 | sbPromptMessages.append (item.get ("question").asText ()); 118 | sbPromptMessages.append (" (匹配分值 "); 119 | sbPromptMessages.append (item.get ("score").asInt ()); 120 | sbPromptMessages.append (")\n"); 121 | } 122 | sbPromptMessages.append (nodeVagueNode.get ("endVagueMsg").asText ()); 123 | //sbResult.append ("\n"); 124 | sResult = sResult + (StringUtils.isEmpty(sResult) ? "" : "\n\n") + sbPromptMessages.toString (); 125 | } 126 | } 127 | else 128 | { 129 | //throw new RuntimeException ("什么鬼,怎么会出这种错误"); 130 | } 131 | } 132 | else 133 | { 134 | sResult = "调用机器人接口返回失败的结果"; 135 | } 136 | return sResult; 137 | } 138 | 139 | public JsonNode GetCSRReponse (String sFrom_EncryptedAccount, String sInput) 140 | { 141 | try 142 | { 143 | Map mapRequestHeaders = new HashMap (); 144 | mapRequestHeaders.put ("Content-Type", "application/json"); 145 | 146 | if (sFrom_EncryptedAccount.length () > 40) 147 | sFrom_EncryptedAccount = StringUtils.left (sFrom_EncryptedAccount, 40); 148 | 149 | String sRequestBody_JSONString = 150 | "{\n" + 151 | " \"protocolId\": 5,\n" + 152 | " \"robotHashCode\": \"" + HCICLOUD_CSR_ROBOT_ID + "\",\n" + 153 | " \"platformConnType\": \"" + HCICLOUD_CSR_ROBOT_CHANNEL_NUMBER + "\",\n" + 154 | " \"userId\": \"" + sFrom_EncryptedAccount + "\",\n" + 155 | " \"talkerId\": \"" + HCICLOUD_CSR_TALKER_ID + "\",\n" + 156 | " \"receiverId\": \"" + HCICLOUD_CSR_RECEIVER_ID + "\",\n" + 157 | " \"appKey\": \"" + HCICLOUD_APP_KEY + "\",\n" + 158 | " \"sendTime\": " + System.currentTimeMillis() + ",\n" + 159 | " \"type\": \"text\",\n" + 160 | " \"isNeedClearHistory\": 1,\n" + 161 | " \"isQuestionQuery\": 0,\n" + 162 | " \"query\": \"" + sInput + "\",\n" + 163 | " \"__LAST__\": 0\n" + 164 | "}"; 165 | net_maclife_wechat_http_BotApp.logger.finer ("\n" + sRequestBody_JSONString); 166 | // 如果不传递 RequestHeader,则返回:“访问来源变量未定义” 167 | // {"aiResult":null,"answerTypeId":1,"protocolId":6,"result":0,"sendTime":null,"serviceLogId":null,"singleNode":{"answerMsg":"访问来源变量未定义","cmd":null,"isRichText":0,"list":null,"question":null,"score":0.0,"standardQuestion":"","standardQuestionId":0},"vagueNode":null} 168 | 169 | // 如果用微信的加密帐号当 userId 传递,则返回:“访问来源名过长” 170 | // {"aiResult":null,"answerTypeId":1,"protocolId":6,"result":0,"sendTime":null,"serviceLogId":null,"singleNode":{"answerMsg":"访问来源名过长","cmd":null,"isRichText":0,"list":null,"question":null,"score":0.0,"standardQuestion":"","standardQuestionId":0},"vagueNode":null} 171 | 172 | InputStream is = net_maclife_util_HTTPUtils.CURL_Post_Stream (HCICLOUD_CSR_URL__Query, mapRequestHeaders, sRequestBody_JSONString.getBytes (HCICLOUD_CHARSET_ENCODING)); 173 | 174 | JsonNode jsonCSRResponse = net_maclife_wechat_http_BotApp.jacksonObjectMapper_Loose.readTree (is); 175 | return jsonCSRResponse; 176 | } 177 | catch (Exception e) 178 | { 179 | e.printStackTrace (); 180 | } 181 | return null; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /bots-industrial/net_maclife_wechat_http_Bot_iConTek.java: -------------------------------------------------------------------------------- 1 | import java.io.*; 2 | import java.net.*; 3 | import java.util.*; 4 | 5 | import org.apache.commons.lang3.*; 6 | 7 | import com.fasterxml.jackson.core.*; 8 | import com.fasterxml.jackson.databind.*; 9 | 10 | import org.jsoup.*; 11 | import org.jsoup.nodes.*; 12 | import org.jsoup.select.*; 13 | 14 | /** 15 | * 基于 http 协议的 iConTek 客服机器人。 16 | * 17 | *

18 | * 与捷通华声 HCICloudCSR (灵云智能客服)机器人类似,该机器人也是利用 iConTek 的 HTTP 接口,将文字(或者语音?)发给其引擎,返回特定的结果,完成机器人客服对话的功能。 19 | *

20 | *

21 | * iConTek 有两套引擎,一个是基于语音对话的 (Sandroid)、一个是基于纯文本的 (Tandroid),由于微信网页版目前无法发语音消息,所以,只能使用 iConTek-Tandroid 引擎来处理。 22 | *

23 | * 24 | * @author liuyan 25 | * 26 | */ 27 | public class net_maclife_wechat_http_Bot_iConTek extends net_maclife_wechat_http_Bot 28 | { 29 | String iConTek_Tandroid_SERVER_HOST = net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.iConTek.Tandroid.server.host"); 30 | int iConTek_Tandroid_SERVER_PORT = net_maclife_wechat_http_BotApp.GetConfig ().getInt ("bot.iConTek.Tandroid.server.port"); 31 | static String iConTek_Tandroid_SERVER_SCHEME = net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.iConTek.Tandroid.server.scheme"); 32 | String iConTek_Tandroid_BaseURL__Query = (StringUtils.equalsIgnoreCase (iConTek_Tandroid_SERVER_SCHEME, "https") ? "https" : "http") + "://" + iConTek_Tandroid_SERVER_HOST + ":" + iConTek_Tandroid_SERVER_PORT + "/queryj"; 33 | 34 | String iConTek_CHARSET_ENCODING = net_maclife_wechat_http_BotApp.utf8; 35 | 36 | /** 37 | * 会话列表(会话,对应于请求 Tandroid 时 URL 中的 queryid)。 38 | * 由于 iConTek Tandroid 是基于知识库的问答机器人引擎,对于顺序型(Stepping)、深挖型(细化型,DrillDown)、树状型(Branching)这些问题类型,一定是一个问题接上一个问题来问答的,这样会导致一个问题:什么时候问题结束,不知道(API 不提供这样的接口?)。 39 | * 所以,需要手工维护一个会话列表,用单独的命令开启新会话。系统还要做个定时任务,定时清理过期(比如:5 分钟内无问答?) 40 | *

41 | * Map 中的 Key 说明: 42 | *

43 | *
session-id
44 | *
会话 ID,一般是微信昵称加上一个数字(Timestamp?)
45 | 46 | *
a
47 | *
48 | 49 | *
b
50 | *
51 | 52 | *
last-active-time
53 | *
最后活动时间。long 类型。清理会话的任务将根据该
54 | 55 | *
56 | *

57 | */ 58 | Map> mapSessions = new HashMap> (); 59 | 60 | @Override 61 | public int OnTextMessageReceived 62 | ( 63 | JsonNode jsonMessage, 64 | JsonNode jsonFrom, String sFromAccount, String sFromName, boolean isFromMe, 65 | JsonNode jsonTo, String sToAccount, String sToName, boolean isToMe, 66 | JsonNode jsonReplyTo, String sReplyToAccount, String sReplyToName, boolean isReplyToRoom, 67 | JsonNode jsonReplyTo_RoomMember, String sReplyToAccount_RoomMember, String sReplyToName_RoomMember, 68 | JsonNode jsonReplyTo_Person, String sReplyToAccount_Person, String sReplyToName_Person, 69 | String sContent, boolean isContentMentionedMe, boolean isContentMentionedMeFirst 70 | ) 71 | { 72 | List listCommands = net_maclife_wechat_http_BotApp.GetConfig ().getList (String.class, "bot.iConTek.commands"); 73 | if (listCommands==null || listCommands.isEmpty ()) // 如果未配置命令,则不处理 74 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 75 | 76 | try 77 | { 78 | if (! net_maclife_wechat_http_BotApp.hasCommandPrefix (sContent)) 79 | { 80 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 81 | } 82 | sContent = net_maclife_wechat_http_BotApp.StripOutCommandPrefix (sContent); 83 | 84 | 85 | // 解析命令行 86 | String[] arrayMessages = sContent.split ("\\s+", 2); 87 | if (arrayMessages==null || arrayMessages.length<1) 88 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 89 | 90 | String sCommandInputed = arrayMessages[0]; 91 | String sCommandParametersInputed = null; 92 | if (arrayMessages.length >= 2) 93 | sCommandParametersInputed = arrayMessages[1]; 94 | 95 | String[] arrayCommandAndOptions = sCommandInputed.split ("\\" + net_maclife_wechat_http_BotApp.COMMAND_OPTION_SEPARATOR + "+", 2); 96 | sCommandInputed = arrayCommandAndOptions[0]; 97 | String sCommandOptionsInputed = null; 98 | //String[] arrayCommandOptions = null; // 该 Bot 只支持一个命令选项: new-session (/icontek.new-session),所以,不再尝试解析多个 options 99 | if (arrayCommandAndOptions.length >= 2) 100 | { 101 | sCommandOptionsInputed = arrayCommandAndOptions[1]; 102 | //arrayCommandOptions = sCommandOptionsInputed.split ("\\" + net_maclife_wechat_http_BotApp.COMMAND_OPTION_SEPARATOR + "+"); 103 | } 104 | 105 | // 检查命令有效性 106 | boolean bValidCommand = false; 107 | for (int i=0; i mapSession = GetSession (sReplyToAccount_Person); // 根据说话“人”的帐号,获取会话 125 | JsonNode jsonTandroidResponse = GetTandroidReponse (mapSession, sCommandParametersInputed); 126 | if (jsonTandroidResponse == null) 127 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 128 | 129 | String sResponse = ParseTandroidResponse (mapSession, jsonTandroidResponse); 130 | if (StringUtils.isNotEmpty (sResponse)) 131 | { 132 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, sResponse); 133 | } 134 | } 135 | catch (Exception e) 136 | { 137 | e.printStackTrace (); 138 | } 139 | 140 | return 141 | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__PROCESSED 142 | | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 143 | } 144 | 145 | Map GetSession (String sFromAccount) 146 | { 147 | Map mapSession = mapSessions.get (sFromAccount); 148 | if (mapSession != null) 149 | return mapSession; 150 | else 151 | return NewSession (sFromAccount); 152 | } 153 | 154 | Map NewSession (String sFromAccount) 155 | { 156 | Map mapSession = new HashMap (); 157 | mapSession.put ("last-active-time", System.currentTimeMillis ()); 158 | mapSession.put ("account", sFromAccount); 159 | mapSession.put ("session-id", sFromAccount + "-" + mapSession.get ("last-active-time")); 160 | mapSessions.put (sFromAccount, mapSession); 161 | return mapSession; 162 | } 163 | 164 | public JsonNode GetTandroidReponse (Map mapSession, String sInput) 165 | { 166 | try 167 | { 168 | String sURL = iConTek_Tandroid_BaseURL__Query + "/x/" + (mapSession.get ("follow-up-question-id")==null ? "" : URLEncoder.encode ((String)mapSession.get ("follow-up-question-id"), iConTek_CHARSET_ENCODING) + "/") + mapSession.get ("session-id") + "/" + URLEncoder.encode (sInput, iConTek_CHARSET_ENCODING); 169 | net_maclife_wechat_http_BotApp.logger.finer (GetName() + " 请求的网址: " + sURL); 170 | Document doc = Jsoup.connect (sURL) 171 | .ignoreContentType (true) 172 | .validateTLSCertificates (false) 173 | //.header ("Content-Type", "application/json") 174 | //.header (name, value) 175 | .get (); 176 | 177 | String sResult = doc.text (); 178 | //net_maclife_wechat_http_BotApp.logger.finer (GetName() + " 获取到的消息:\n" + sResult); 179 | JsonNode jsonCSRResponse = net_maclife_wechat_http_BotApp.jacksonObjectMapper_Loose.readTree (sResult); 180 | return jsonCSRResponse; 181 | } 182 | catch (Exception e) 183 | { 184 | e.printStackTrace (); 185 | } 186 | return null; 187 | } 188 | 189 | public String ParseTandroidResponse (Map mapSession, JsonNode jsonTandroidResponse) 190 | { 191 | net_maclife_wechat_http_BotApp.logger.finer ("\n" + jsonTandroidResponse); 192 | if (jsonTandroidResponse == null || jsonTandroidResponse.isNull()) 193 | return ""; 194 | 195 | String sResponseID = net_maclife_wechat_http_BotApp.GetJSONText (jsonTandroidResponse, "responseId"); 196 | 197 | // 如果是响应「标准型」、「枚举型」的「分类」, answer 会是目标知识点内的答案, 但如果找其他上下文功能的「分类」, 会有以下特别的输出: 198 | //  「顺序型问卷」,「树状型问卷」及「细化型」分类, answer 会输出 followUp 所指定的「分类」中特殊知识点内的 answer。 199 | String sAnswer = net_maclife_wechat_http_BotApp.GetJSONText (jsonTandroidResponse, "answer"); 200 | 201 | String sConfidence = net_maclife_wechat_http_BotApp.GetJSONText (jsonTandroidResponse, "confidence"); 202 | 203 | // y 是知识点 ID, 亦即是 FAQID 或意图 ID。 204 | String sFAQID = net_maclife_wechat_http_BotApp.GetJSONText (jsonTandroidResponse, "y"); 205 | 206 | // followUp: 是系统认为开发者应该再访问的下一个「分类」位置, 只会出现在指定查找「顺序型问卷」、「树状型问卷」、「细化型」的「分类」才会出现, 查找「枚举型」或「标准型」的「分类」是不会出现 followUp。 207 | String sFollowUp = net_maclife_wechat_http_BotApp.GetJSONText (jsonTandroidResponse, "followUp"); 208 | if (StringUtils.isEmpty (sFollowUp)) 209 | { 210 | mapSession.remove ("follow-up-question-id"); 211 | } 212 | else 213 | { 214 | mapSession.put ("follow-up-question-id", sFollowUp); 215 | } 216 | 217 | String sResult = sAnswer; 218 | return sResult; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /bots/net_maclife_wechat_http_Bot_BaiduImageSearch.java: -------------------------------------------------------------------------------- 1 | import java.io.*; 2 | import java.net.*; 3 | 4 | import org.apache.commons.io.*; 5 | import org.apache.commons.lang3.*; 6 | import org.jsoup.nodes.*; 7 | import org.jsoup.select.*; 8 | 9 | import com.fasterxml.jackson.databind.*; 10 | 11 | public class net_maclife_wechat_http_Bot_BaiduImageSearch extends net_maclife_wechat_http_Bot 12 | { 13 | @Override 14 | public int OnImageMessageReceived 15 | ( 16 | JsonNode jsonMessage, 17 | JsonNode jsonFrom, String sFromAccount, String sFromName, boolean isFromMe, 18 | JsonNode jsonTo, String sToAccount, String sToName, boolean isToMe, 19 | JsonNode jsonReplyTo, String sReplyToAccount, String sReplyToName, boolean isReplyToRoom, 20 | JsonNode jsonReplyTo_RoomMember, String sReplyToAccount_RoomMember, String sReplyToName_RoomMember, 21 | JsonNode jsonReplyTo_Person, String sReplyToAccount_Person, String sReplyToName_Person, 22 | String sContent, File fMedia, String sImageURL 23 | ) 24 | { 25 | if ((fMedia == null || ! fMedia.exists ()) && StringUtils.isEmpty (sImageURL)) 26 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 27 | 28 | Document doc = null; 29 | org.jsoup.Connection jsoup_conn = null; 30 | try 31 | { 32 | String sURL = null; 33 | 34 | if (StringUtils.isNotEmpty (sImageURL)) 35 | { 36 | sURL = "http://image.baidu.com/n/pc_search?queryImageUrl=" + URLEncoder.encode (sImageURL, net_maclife_wechat_http_BotApp.utf8) + "&uptype=urlsearch"; 37 | net_maclife_wechat_http_BotApp.logger.info (GetName() + " 按图片网址搜索,搜索网址为:\n" + sURL); 38 | 39 | } 40 | else 41 | { 42 | sURL = "https://image.baidu.com/n/image?fr=html5&target=pcSearchImage&needJson=true&id=WU_FILE_0&name=" + URLEncoder.encode (fMedia.getName (), net_maclife_wechat_http_BotApp.utf8) + "&type=" + "&lastModifiedDate=" + "&size=" + fMedia.length (); 43 | //jsoup_conn = org.jsoup.Jsoup.connect (sURL) 44 | // .data ("", "", new FileInputStream (fMedia)) 45 | // ; 46 | //doc = jsoup_conn.post (); 47 | byte[] arrayPostData = IOUtils.toByteArray (new FileInputStream (fMedia), fMedia.length ()); 48 | String sJSONString = net_maclife_util_HTTPUtils.CURL_Post (sURL, arrayPostData); 49 | /* 50 | { 51 | "errno":0, 52 | "errmsg":"", 53 | "data":{ 54 | "querySign":"1835526740,788942838", 55 | "imageUrl":"http:\/\/b.hiphotos.baidu.com\/image\/pic\/item\/2f738bd4b31c8701602dcef02e7f9e2f0708ff09.jpg", 56 | "pageUrl":"https:\/\/image.baidu.com\/n\/pc_search?rn=30&appid=0&tag=1&isMobile=0&queryImageUrl=http%3A%2F%2Fb.hiphotos.baidu.com%2Fimage%2Fpic%2Fitem%2F2f738bd4b31c8701602dcef02e7f9e2f0708ff09.jpg&querySign=1835526740%2C788942838&fromProduct=&productBackUrl=" 57 | }, 58 | "extra":[ 59 | 60 | ] 61 | } */ 62 | net_maclife_wechat_http_BotApp.logger.info (GetName() + " 上传图片后返回的 JSON\n" + sJSONString); 63 | JsonNode jsonUploadImageResult = net_maclife_wechat_http_BotApp.jacksonObjectMapper_Loose.readTree (sJSONString); 64 | int errno = net_maclife_wechat_http_BotApp.GetJSONInt (jsonUploadImageResult, "errno"); 65 | if (errno == 0) 66 | { 67 | JsonNode jsonData = jsonUploadImageResult.get ("data"); 68 | sURL = net_maclife_wechat_http_BotApp.GetJSONText (jsonData, "pageUrl"); 69 | net_maclife_wechat_http_BotApp.logger.info (GetName() + " 上传图片后返回的 JSON 中的图片搜索网页的网址\n" + sURL); 70 | } 71 | else 72 | { 73 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 74 | } 75 | } 76 | 77 | doc = org.jsoup.Jsoup.connect (sURL).timeout (net_maclife_util_HTTPUtils.DEFAULT_READ_TIMEOUT_SECOND * 1000).get (); 78 | Elements e图片猜测 = doc.select ("#guessInfo"); 79 | if (e图片猜测.isEmpty ()) 80 | { 81 | net_maclife_wechat_http_BotApp.logger.info (GetName() + " 找不到 #guessInfo,也许,搜索出错了? " + doc.select (".error-text").text ()); 82 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 83 | } 84 | Elements e图片猜测词语链接 = e图片猜测.select (".guess-info-text a.guess-info-word-link"); 85 | if (e图片猜测词语链接.isEmpty ()) 86 | { 87 | net_maclife_wechat_http_BotApp.logger.info (GetName() + " 找不到 .guess-info-text a.guess-info-word-link,也许,没有结果? " + e图片猜测.text ()); 88 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 89 | } 90 | 91 | StringBuilder sbInfo = new StringBuilder (); 92 | sbInfo.append (e图片猜测词语链接.text ()); 93 | sbInfo.append ("\n"); 94 | sbInfo.append (e图片猜测词语链接.first ().absUrl ("href")); 95 | Elements e图片来源 = doc.select ("#sourceCard"); 96 | if (! e图片来源.isEmpty ()) 97 | { 98 | sbInfo.append ("\n\n" + e图片来源.select (".source-card-header-count").first ().text ()); // "发现 N 条图片来源" 99 | Elements e图片来源标题链接 = e图片来源.select (".source-card-topic-title-link"); 100 | for (int i=0; i 0); 157 | for (int i=0; i注意:这个接口并不提供免费使用。调用时会报错: 百度 OCR 通用文字识别(含生僻字版) 返回失败:18 Open api qps request limit reached 208 | * @param fMedia 209 | * @return 210 | */ 211 | public static JsonNode ProcessOCR_GeneralEnhanced (File fMedia) throws IOException, InterruptedException, ExecutionException 212 | { 213 | return ProcessOCR_Common (fMedia, BAIDU_OCR_URL__GeneralEnhanced, "通用文字识别(含生僻字版)"); 214 | } 215 | 216 | /** 217 | * 网络图片文字识别 218 | * @param fMedia 219 | * @return 220 | */ 221 | public static JsonNode ProcessOCR_WebImage (File fMedia) throws IOException, InterruptedException, ExecutionException 222 | { 223 | return ProcessOCR_Common (fMedia, BAIDU_OCR_URL__WebImage, "网络图片文字识别"); 224 | } 225 | 226 | /** 227 | * 表格文字识别(异步接口)。 228 | * 注意,调用方直接读取返回值即可,不需要做异步处理,本函数自己做了异步处理 -- 调用者如同使用同步接口一样,无需关心异步返回结果问题的处理 229 | * @param fMedia 230 | * @return 231 | * @throws ExecutionException 232 | * @throws InterruptedException 233 | */ 234 | public static JsonNode ProcessOCR_Form_Async (File fMedia) throws IOException, InterruptedException, ExecutionException 235 | { 236 | String sAccessToken = net_maclife_util_BaiduCloud.GetBaiduAccessToken (sBaiduCloudAppKey, sBaiduCloudAppPassword, sBaiduOAuthAccessTokenFileInJSONFormat); 237 | if (StringUtils.isEmpty (sAccessToken)) 238 | return null; 239 | 240 | byte[] arrayImageData = IOUtils.toByteArray (new FileInputStream (fMedia)); 241 | String sImageBase64 = Base64.encodeBase64String (arrayImageData); //Base64.encodeBase64URLSafeString (arrayImageData); 242 | //System.out.println (sImageBase64); 243 | //String sImageBase64_URLEncoded = URLEncoder.encode (sImageBase64, net_maclife_wechat_http_BotApp.utf8); 244 | //System.out.println (sImageBase64_URLEncoded); 245 | String sURL = BAIDU_OCR_URL__Form_Async__Request + "?access_token=" + sAccessToken + ""; 246 | org.jsoup.Connection jsoup_conn = null; 247 | jsoup_conn = org.jsoup.Jsoup.connect (sURL) 248 | .header ("Content-Type", "application/x-www-form-urlencoded") 249 | .ignoreContentType (true) 250 | .data ("image", sImageBase64) 251 | ; 252 | Document doc = jsoup_conn.post (); 253 | JsonNode jsonResponse = net_maclife_wechat_http_BotApp.jacksonObjectMapper_Loose.readTree (doc.text ()); 254 | System.out.println (jsonResponse); 255 | if (jsonResponse.get ("error_code") != null) 256 | { 257 | net_maclife_wechat_http_BotApp.logger.warning ("百度 OCR 表格文字识别(异步接口) 发出请求后返回失败:" + net_maclife_wechat_http_BotApp.GetJSONText (jsonResponse, "error_code") + " " + net_maclife_wechat_http_BotApp.GetJSONText (jsonResponse, "error_msg")); 258 | return null; 259 | } 260 | assert (jsonResponse.get ("request") != null && jsonResponse.get ("request").isArray ()); 261 | JsonNode jsonResult = jsonResponse.get ("result"); 262 | assert (jsonResult.size () > 0); 263 | String sRequestID = net_maclife_wechat_http_BotApp.GetJSONText (jsonResult.get (0), "request_id"); 264 | 265 | Callable taskAsyncGetResult = new FormOCR_AsyncGetResultTask (sRequestID); 266 | JsonNode jsonOCRResult = net_maclife_wechat_http_BotApp.executor.submit (taskAsyncGetResult).get (); 267 | return jsonOCRResult; 268 | } 269 | 270 | static class FormOCR_AsyncGetResultTask implements Callable 271 | { 272 | String sRequestID; 273 | public FormOCR_AsyncGetResultTask (String sRequestID) 274 | { 275 | this.sRequestID = sRequestID; 276 | } 277 | 278 | @Override 279 | public JsonNode call () throws Exception 280 | { 281 | try 282 | { 283 | int nProgressPercent = 0; 284 | do 285 | { 286 | String sAccessToken = net_maclife_util_BaiduCloud.GetBaiduAccessToken (sBaiduCloudAppKey, sBaiduCloudAppPassword, sBaiduOAuthAccessTokenFileInJSONFormat); 287 | if (StringUtils.isEmpty (sAccessToken)) 288 | return null; 289 | String sURL = BAIDU_OCR_URL__Form_Async__GetResult + "?access_token=" + sAccessToken + ""; 290 | org.jsoup.Connection jsoup_conn = null; 291 | jsoup_conn = org.jsoup.Jsoup.connect (sURL) 292 | .header ("Content-Type", "application/x-www-form-urlencoded") 293 | .ignoreContentType (true) 294 | .data ("request_id", sRequestID) 295 | .data ("result_type", "json") // request_type 若不设置,则默认为 "excel",但这里我们需要的是 "json" 296 | ; 297 | Document doc = jsoup_conn.post (); 298 | JsonNode jsonResponse = net_maclife_wechat_http_BotApp.jacksonObjectMapper_Loose.readTree (doc.text ()); 299 | System.out.println (jsonResponse); 300 | if (jsonResponse.get ("error_code") != null) 301 | { 302 | net_maclife_wechat_http_BotApp.logger.warning ("百度 OCR 表格文字识别(异步接口) 获取请求结果返回失败:" + net_maclife_wechat_http_BotApp.GetJSONText (jsonResponse, "error_code") + " " + net_maclife_wechat_http_BotApp.GetJSONText (jsonResponse, "error_msg")); 303 | return null; 304 | } 305 | assert (jsonResponse.get ("result") != null); 306 | JsonNode jsonRequest = jsonResponse.get ("result"); 307 | nProgressPercent = net_maclife_wechat_http_BotApp.GetJSONInt (jsonRequest, "percent"); 308 | if (nProgressPercent == 100) 309 | { 310 | return jsonRequest.get ("result_data"); 311 | } 312 | 313 | TimeUnit.SECONDS.sleep (2); 314 | } while (nProgressPercent != 100); 315 | } 316 | catch (Exception e) 317 | { 318 | e.printStackTrace (); 319 | } 320 | return null; 321 | } 322 | } 323 | 324 | public static void main (String[] args) throws Exception 325 | { 326 | if (args.length < 1) 327 | { 328 | System.err.println ("需要指定一个图片文件 (.jpg 或 .png 或 .bmp 格式的图片文件)"); 329 | return; 330 | } 331 | String sFileName = args[0]; 332 | File fMedia = new File (sFileName); 333 | 334 | //ProcessOCR_GeneralBasic (fMedia); 335 | //ProcessOCR_AccurateBasic (fMedia); 336 | //ProcessOCR_General (fMedia); 337 | ProcessOCR_Accurate (fMedia); 338 | //ProcessOCR_GeneralEnhanced (fMedia); 339 | //ProcessOCR_WebImage (fMedia); 340 | 341 | //ProcessOCR_Form_Async (fMedia); 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /bots/net_maclife_wechat_http_Bot_BaiduTranslate.java: -------------------------------------------------------------------------------- 1 | import java.net.*; 2 | import java.util.*; 3 | 4 | import org.apache.commons.codec.digest.*; 5 | import org.apache.commons.lang3.*; 6 | 7 | import com.fasterxml.jackson.databind.*; 8 | 9 | /** 10 | * 百度翻译机器人小程序。 11 | * @author liuyan 12 | * 13 | */ 14 | public class net_maclife_wechat_http_Bot_BaiduTranslate extends net_maclife_wechat_http_Bot 15 | { 16 | public static final String BAIDU_TRANSLATE_HTTP_URL = "http://api.fanyi.baidu.com/api/trans/vip/translate"; 17 | public static final String BAIDU_TRANSLATE_HTTPS_URL = "https://fanyi-api.baidu.com/api/trans/vip/translate"; 18 | 19 | //public static final String REGEXP_LanguageOptions = "(\\w*)(2*)(\\w*)"; 20 | //public static final Pattern PATTERN_LanguageOptions = Pattern.compile (REGEXP_LanguageOptions); 21 | 22 | @Override 23 | public int OnTextMessageReceived 24 | ( 25 | JsonNode jsonMessage, 26 | JsonNode jsonFrom, String sFromAccount, String sFromName, boolean isFromMe, 27 | JsonNode jsonTo, String sToAccount, String sToName, boolean isToMe, 28 | JsonNode jsonReplyTo, String sReplyToAccount, String sReplyToName, boolean isReplyToRoom, 29 | JsonNode jsonReplyTo_RoomMember, String sReplyToAccount_RoomMember, String sReplyToName_RoomMember, 30 | JsonNode jsonReplyTo_Person, String sReplyToAccount_Person, String sReplyToName_Person, 31 | String sContent, boolean isContentMentionedMe, boolean isContentMentionedMeFirst 32 | ) 33 | { 34 | List listCommands = net_maclife_wechat_http_BotApp.GetConfig ().getList (String.class, "bot.baidu-translate.commands"); 35 | if (listCommands==null || listCommands.isEmpty ()) // 如果未配置命令,则不处理 36 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 37 | 38 | if (! net_maclife_wechat_http_BotApp.hasCommandPrefix (sContent)) 39 | { 40 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 41 | } 42 | sContent = net_maclife_wechat_http_BotApp.StripOutCommandPrefix (sContent); 43 | 44 | String sFromLanguage = net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.baidu-translate.from-language"); 45 | String sToLanguage = net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.baidu-translate.to-language"); 46 | 47 | try 48 | { 49 | String[] arrayMessages = sContent.split ("\\s+", 2); 50 | if (arrayMessages==null || arrayMessages.length<1) 51 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 52 | 53 | String sCommandInputed = arrayMessages[0]; 54 | String sCommandParametersInputed = null; 55 | if (arrayMessages.length >= 2) 56 | sCommandParametersInputed = arrayMessages[1]; 57 | 58 | String[] arrayCommandOptions = sCommandInputed.split ("\\.+", 2); 59 | sCommandInputed = arrayCommandOptions[0]; 60 | String sCommandOptionsInputed = null; 61 | if (arrayCommandOptions.length >= 2) 62 | sCommandOptionsInputed = arrayCommandOptions[1]; 63 | 64 | for (int i=0; i\n\n可选的语言代码选项的格式:\n - .原文语言代码\n - .2译文语言代码\n - .原文语言代码2译文语言代码\n\n具体能用哪些语言代码,请参照: http://api.fanyi.baidu.com/api/trans/product/apidoc#languageList 给出的语言代码列表"); 73 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 74 | } 75 | 76 | // 解析命令“翻译语言选项”:.src2dst、.src、.2dst 77 | if (StringUtils.isNotEmpty (sCommandOptionsInputed)) 78 | { 79 | //Matcher matcher = PATTERN_LanguageOptions.matcher (sCommandOptionsInputed); 80 | //if (sCommandOptionsInputed.matches () 81 | //不用规则表达式来解析了,还不如用简单的字符串格式判断 82 | if (StringUtils.contains (sCommandOptionsInputed, "2")) 83 | { 84 | if (sCommandOptionsInputed.startsWith ("2")) 85 | { // 只指定了译文语言代码 86 | sToLanguage = sCommandOptionsInputed.substring (1); 87 | } 88 | else 89 | { 90 | String[] arrayFromTo = sCommandOptionsInputed.split ("2"); 91 | sFromLanguage = arrayFromTo[0]; 92 | sToLanguage = arrayFromTo[1]; 93 | } 94 | } 95 | else 96 | { // 只指定了原文的语言代码 97 | sFromLanguage = sCommandOptionsInputed; 98 | } 99 | } 100 | if (StringUtils.isEmpty (sFromLanguage) || StringUtils.isEmpty (sToLanguage)) 101 | { 102 | String sErrorInfo = GetName() + " 的原文、译文的语言代码不能为空"; 103 | //net_maclife_wechat_http_BotApp.logger.warning (sErrorInfo); 104 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, sErrorInfo); 105 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 106 | } 107 | if (StringUtils.equalsIgnoreCase (sToLanguage, "auto")) 108 | { 109 | String sErrorInfo = GetName() + "机器人设置的目标语言不能为 auto"; 110 | //net_maclife_wechat_http_BotApp.logger.warning (sErrorInfo); 111 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, sErrorInfo); 112 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 113 | } 114 | 115 | 116 | // 命令行命令格式没问题,现在开始查询数据库 117 | String sTranslation = null; 118 | JsonNode jsonResult = GetTranslation (sCommandParametersInputed, sFromLanguage, sToLanguage); 119 | if (jsonResult == null) 120 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 121 | JsonNode jsonErrorCode = jsonResult.get ("error_code"); 122 | if (jsonErrorCode != null && !jsonErrorCode.isNull ()) 123 | { 124 | String sErrorInfo = GetName() + " 返回错误结果: " + net_maclife_wechat_http_BotApp.GetJSONText (jsonResult, "error_code") + ": " + net_maclife_wechat_http_BotApp.GetJSONText (jsonResult, "error_msg"); 125 | //net_maclife_wechat_http_BotApp.logger.warning (sErrorInfo); 126 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, sErrorInfo); 127 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 128 | } 129 | JsonNode jsonTransResults = jsonResult.get ("trans_result"); 130 | if (jsonTransResults.size () == 1) 131 | { 132 | JsonNode jsonTransResult = jsonTransResults.get (0); 133 | sTranslation = net_maclife_wechat_http_BotApp.GetJSONText (jsonTransResult, "dst"); 134 | } 135 | else 136 | { 137 | StringBuilder sb = new StringBuilder (); 138 | for (int j=0; j= 2 && StringUtils.isNotEmpty (args[1])) 198 | sFrom = args[1]; 199 | if (args.length >= 3 && StringUtils.isNotEmpty (args[2])) 200 | sTo = args[2]; 201 | //net_maclife_wechat_http_Bot bot = new net_maclife_wechat_http_Bot_BaiduTranslate (); 202 | 203 | JsonNode jsonResult = GetTranslation (sQuery, sFrom, sTo); 204 | System.err.println (jsonResult); 205 | if (jsonResult.get ("error_code") != null && ! jsonResult.get ("error_code").isNull ()) 206 | { 207 | System.err.println (net_maclife_wechat_http_BotApp.GetJSONText (jsonResult, "error_code") + ": " + net_maclife_wechat_http_BotApp.GetJSONText (jsonResult, "error_msg")); 208 | return; 209 | } 210 | JsonNode jsonTransResults = jsonResult.get ("trans_result"); 211 | for (int i=0; i 17 | 语音识别结果 18 |
 19 | {
 20 |     "corpus_no":"******",
 21 |     "err_msg":"*****.",
 22 |     "err_no":0,	// 0 是成功,其他为失败
 23 |     "result":	// 最多 5 个结果,或者根本不存在(出错时)
 24 |     [
 25 |         "账号已暂停使用,"
 26 |     ],
 27 |     "sn":"*****"
 28 | }
 29 | 
30 |

31 | 32 | * @author liuyan 33 | * 34 | */ 35 | public class net_maclife_wechat_http_Bot_BaiduVoice extends net_maclife_wechat_http_Bot 36 | { 37 | public static final String BAIDU_ASR_API_URL = "http://vop.baidu.com/server_api"; 38 | public static final String BAIDU_TTS_API_URL = "http://tsn.baidu.com/text2audio"; 39 | 40 | static String sBaiduCloudAppID = net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.baidu.voice.app.id"); 41 | static String sBaiduCloudAppKey = net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.baidu.voice.app.key"); 42 | static String sBaiduCloudAppPassword = net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.baidu.voice.app.password"); 43 | static String sBaiduOAuthAccessTokenFileInJSONFormat = net_maclife_wechat_http_BotApp.cacheDirectory + "/" + net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.baidu.voice.accessTokenFile"); 44 | 45 | static String sMACAddress = "BaiduVoiceBotApplet"; 46 | static 47 | { 48 | try 49 | { 50 | // 确保百度 AccessToken 目录存在 51 | File fBaiduDir = new File (sBaiduOAuthAccessTokenFileInJSONFormat).getParentFile (); 52 | fBaiduDir.mkdirs (); 53 | 54 | // 获取本机 MAC 地址 55 | // 56 | InetAddress ip = InetAddress.getLocalHost(); 57 | net_maclife_wechat_http_BotApp.logger.info ("本机 IP 地址: " + ip.getHostAddress()); 58 | NetworkInterface network = NetworkInterface.getByInetAddress (ip); 59 | if (network != null) 60 | { 61 | byte[] arrayMAC = network.getHardwareAddress(); 62 | if (arrayMAC != null) 63 | { 64 | StringBuilder sb = new StringBuilder (); 65 | for (int i=0; i mapRequestHeaders = new HashMap (); 204 | mapRequestHeaders.put ("Content-Type", "audio/amr; rate=8000"); 205 | 206 | InputStream is = new FileInputStream (fMedia); 207 | byte[] arrayPostData = IOUtils.toByteArray (is, fMedia.length ()); 208 | is.close (); 209 | 210 | String sResponseBodyContent = net_maclife_util_HTTPUtils.CURL_Post (sURL, mapRequestHeaders, arrayPostData); 211 | JsonNode jsonNode = net_maclife_wechat_http_BotApp.jacksonObjectMapper_Loose.readTree (sResponseBodyContent); 212 | net_maclife_wechat_http_BotApp.logger.info (GetName() + " 机器人获取百度语音识别 (ASR) 的 http 响应消息体:"); 213 | net_maclife_wechat_http_BotApp.logger.info (" " + sResponseBodyContent); 214 | 215 | int err_no = net_maclife_wechat_http_BotApp.GetJSONInt (jsonNode, "err_no"); 216 | String err_msg = net_maclife_wechat_http_BotApp.GetJSONText (jsonNode, "err_msg"); 217 | switch (err_no) 218 | { 219 | case 0: 220 | JsonNode jsonResults = jsonNode.get ("result"); 221 | if (jsonResults.size () == 1) 222 | { 223 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, (StringUtils.isEmpty (sReplyToAccount_RoomMember) ? sReplyToName : sReplyToName_RoomMember) + " 说道:\n" + jsonResults.get (0).asText ()); 224 | return 225 | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__PROCESSED 226 | | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 227 | } 228 | 229 | StringBuilder sb = new StringBuilder (); 230 | for (int i=0; i 15 | * 注意:当在手机上发 emoji 表情时,比如:😞,web 端收到的是类似 <span class="emoji emoji1f612"></span> 这样的文字(难道微信担心 Web 版在不同浏览器下表现不一致?)。 16 | * 手机微信上的 emoji 字符的显示,应该是(猜测)手机操作系统自己显示的,比如 android ios 用系统内置的 emoji 字体来显示(再说一遍,是猜测)。 17 | *

18 | *

19 | * emoji 数据库是从 http://unicode.org/emoji/charts/full-emoji-list.html (这个 html 有 36M 大小!!!) 获取,然后通过本程序生成 SQL 脚本文件,然后执行该 SQL 脚本文件写入的。 20 | *

21 | * @author liuyan 22 | * 23 | */ 24 | public class net_maclife_wechat_http_Bot_Emoji extends net_maclife_wechat_http_Bot 25 | { 26 | @Override 27 | public int OnTextMessageReceived 28 | ( 29 | JsonNode jsonMessage, 30 | JsonNode jsonFrom, String sFromAccount, String sFromName, boolean isFromMe, 31 | JsonNode jsonTo, String sToAccount, String sToName, boolean isToMe, 32 | JsonNode jsonReplyTo, String sReplyToAccount, String sReplyToName, boolean isReplyToRoom, 33 | JsonNode jsonReplyTo_RoomMember, String sReplyToAccount_RoomMember, String sReplyToName_RoomMember, 34 | JsonNode jsonReplyTo_Person, String sReplyToAccount_Person, String sReplyToName_Person, 35 | String sContent, boolean isContentMentionedMe, boolean isContentMentionedMeFirst 36 | ) 37 | { 38 | List listCommands = net_maclife_wechat_http_BotApp.GetConfig ().getList (String.class, "bot.emoji-test.commands"); 39 | if (listCommands==null || listCommands.isEmpty ()) // 如果未配置命令,则不处理 40 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 41 | 42 | if (! net_maclife_wechat_http_BotApp.hasCommandPrefix (sContent)) 43 | { 44 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 45 | } 46 | sContent = net_maclife_wechat_http_BotApp.StripOutCommandPrefix (sContent); 47 | 48 | try 49 | { 50 | String[] arrayMessages = sContent.split ("\\s+", 2); 51 | if (arrayMessages==null || arrayMessages.length<1) 52 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 53 | 54 | String sCommandInputed = arrayMessages[0]; 55 | String sCommandParametersInputed = null; 56 | if (arrayMessages.length >= 2) 57 | sCommandParametersInputed = arrayMessages[1]; 58 | 59 | String[] arrayCommandOptions = sCommandInputed.split ("\\.+", 2); 60 | sCommandInputed = arrayCommandOptions[0]; 61 | String sCommandOptionsInputed = null; 62 | if (arrayCommandOptions.length >= 2) 63 | sCommandOptionsInputed = arrayCommandOptions[1]; 64 | 65 | // 命令行命令格式没问题,现在开始查询数据库 66 | for (int i=0; i...\n\n比如:\n" + sCommand + " cat face"); 74 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 75 | } 76 | 77 | // 解析命令“选项”: .detail .详细 78 | boolean bShowDetail = false; 79 | if (StringUtils.isNotEmpty (sCommandOptionsInputed)) 80 | { 81 | arrayCommandOptions = sCommandOptionsInputed.split ("\\.+"); 82 | for (String sCommandOption : arrayCommandOptions) 83 | { 84 | if (StringUtils.equalsIgnoreCase (sCommandOption, "detail") || StringUtils.equalsIgnoreCase (sCommandOption, "详细")) 85 | { 86 | bShowDetail = true; 87 | } 88 | } 89 | } 90 | 91 | try 92 | { 93 | String sResult = Query (sCommandParametersInputed, bShowDetail); 94 | if (StringUtils.isEmpty (sResult)) 95 | { 96 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, "找不到关键字为 " + sCommandParametersInputed + " 的 emoji 字符"); 97 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 98 | } 99 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, sResult); 100 | break; 101 | } 102 | catch (Exception e) 103 | { 104 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, "查询出错: " + e); 105 | } 106 | } 107 | } 108 | } 109 | catch (Exception e) 110 | { 111 | e.printStackTrace (); 112 | } 113 | 114 | return 115 | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__PROCESSED 116 | | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 117 | } 118 | 119 | String Query (String sQuery, boolean bShowDetail) throws SQLException 120 | { 121 | String sTablePrefix = StringUtils.trimToEmpty (net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.emoji-test.jdbc.database-table.prefix")); 122 | net_maclife_wechat_http_BotApp.SetupDataSource (); 123 | java.sql.Connection conn = null; 124 | PreparedStatement stmt = null; 125 | ResultSet rs = null; 126 | StringBuilder sb = null; 127 | try 128 | { 129 | conn = net_maclife_wechat_http_BotApp.botDS.getConnection (); 130 | StringBuilder sbSQL = new StringBuilder ("SELECT * FROM " + sTablePrefix + "emoji e WHERE 1=1"); 131 | if (! StringUtils.containsIgnoreCase (sQuery, "*")) 132 | { 133 | String[] arrayKeywords = sQuery.split (" +"); 134 | for (String sKeyword : arrayKeywords) 135 | { // 这里先用这种方式查询,以后考虑将 tag_name 挪到单独的表中,1对多的关系,查询时只按照 “=”的匹配方式进行匹配,不会造成现在这种模糊匹配的方式带来的不想要的结果:比如,查 eat 会匹配到 meat repeat 等 136 | sbSQL.append (" AND (tag_name=? OR tag_name LIKE ? OR tag_name LIKE ? OR 英文名称=? OR 英文名称 LIKE ? OR 英文名称 LIKE ?)"); 137 | } 138 | } 139 | stmt = conn.prepareStatement (sbSQL.toString ()); 140 | int nCol = 1; 141 | if (! StringUtils.containsIgnoreCase (sQuery, "*")) 142 | { 143 | String[] arrayKeywords = sQuery.split (" +"); 144 | for (String sKeyword : arrayKeywords) 145 | { 146 | stmt.setString (nCol++, sKeyword); 147 | stmt.setString (nCol++, "%" + sKeyword + " %"); 148 | stmt.setString (nCol++, "% " + sKeyword + "%"); 149 | stmt.setString (nCol++, sKeyword); 150 | stmt.setString (nCol++, "%" + sKeyword + " %"); 151 | stmt.setString (nCol++, "% " + sKeyword + "%"); 152 | } 153 | } 154 | rs = stmt.executeQuery (); 155 | sb = new StringBuilder (); 156 | while (rs.next ()) 157 | { 158 | if (bShowDetail) 159 | { 160 | sb.append ("--------------------\n"); 161 | sb.append ("字符: "); 162 | } 163 | sb.append (rs.getString ("emoji_char")); 164 | sb.append (' '); 165 | if (bShowDetail) 166 | { 167 | sb.append ("\n"); 168 | sb.append ("名称: "); 169 | sb.append (rs.getString ("英文名称")); 170 | sb.append ("\n"); 171 | sb.append ("关键字: "); 172 | sb.append (rs.getString ("tag_name")); 173 | sb.append ("\n"); 174 | //sb.append ("Unicode: "); 175 | //sb.append (rs.getString ("tag_name")); 176 | //sb.append ("\n"); 177 | //sb.append ("UTF-8: "); 178 | //sb.append (rs.getString ("tag_name")); 179 | //sb.append ("\n"); 180 | } 181 | //break; 182 | } 183 | } 184 | catch (SQLException e) 185 | { 186 | e.printStackTrace(); 187 | throw e; 188 | } 189 | finally 190 | { 191 | try 192 | { 193 | if (rs != null) 194 | rs.close (); 195 | if (stmt != null) 196 | stmt.close (); 197 | if (conn != null) 198 | conn.close (); 199 | } 200 | catch (SQLException e) 201 | { 202 | e.printStackTrace(); 203 | } 204 | } 205 | return sb==null ? null : sb.toString (); 206 | } 207 | 208 | 209 | public static void Usage () 210 | { 211 | System.out.println ("用法:"); 212 | System.out.println ("从 full-emoji-list.html 生成 .sql 入库 SQL 脚本:"); 213 | System.out.println (" java net_maclife_wechat_http_Bot_Emoji gensql <.html 文件名> [输出到 .sql 文件名]"); 214 | System.out.println (" 如果不指定 [输出到 .sql 文件名],则输出到当前目录下的 emoji-data.mysql.sql 文件中 (如果文件存在的话,则直接覆盖)"); 215 | System.out.println ("查询:"); 216 | System.out.println (" java net_maclife_wechat_http_Bot_Emoji "); 217 | } 218 | 219 | public static String ExtractImageData (String sImageSrc) 220 | { 221 | return StringUtils.substring (StringUtils.remove (sImageSrc, "—"), "data:image/png;base64,".length ()); 222 | } 223 | public static void main (String[] args) throws IOException 224 | { 225 | if (args.length < 1) 226 | { 227 | Usage (); 228 | return; 229 | } 230 | String sAction = args[0]; 231 | if (StringUtils.equalsIgnoreCase (sAction, "gensql")) 232 | { 233 | if (args.length < 2) 234 | { 235 | Usage (); 236 | return; 237 | } 238 | 239 | File fHTML = new File (args[1]); 240 | File fSQL = new File (args.length >=3 ? args[2] : "emoji-data.mysql.sql"); 241 | FileWriter fwSQL = new FileWriter (fSQL); 242 | //fwSQL.write ("INSERT INTO emoji (sn, code, emoji_char, 浏览器显示图, Apple显示图, Google显示图, Twitter显示图, One显示图, Facebook显示图, FBM显示图, 三星显示图, Windows显示图, GMail显示图, SB显示图, DCM显示图, KDDI显示图, 英文名称, 日期, tag_name) VALUES\n"); 243 | 244 | Document docHTML = Jsoup.parse (fHTML, net_maclife_wechat_http_BotApp.utf8); 245 | Elements eEmojiTable = docHTML.select ("table"); 246 | Elements eRows = eEmojiTable.select ("tr"); 247 | int nEmojiRow = 0; 248 | for (Element eRow : eRows) 249 | { 250 | Elements eCols = eRow.select ("td"); 251 | if (eCols.isEmpty ()) // 这行是 标题行? th? 252 | continue; 253 | 254 | if ((nEmojiRow % 100) == 0) // 每 100 行数据出一个 INSERT,免得 MySQL 报错: ERROR 2006 (HY000) at line 1: MySQL server has gone away 。原因: MySQL 数据包有大小限制,虽然可以在配置文件中用 SET max_allowed_packet=nnnM 来解决,但还是成多个 INSERT 吧 255 | { 256 | if (nEmojiRow != 0) 257 | fwSQL.write (";"); 258 | fwSQL.write ("\nINSERT INTO emoji (sn, code, emoji_char, 浏览器显示图, Apple显示图, Google显示图, Twitter显示图, EmojiOne显示图, Facebook显示图, FacebookMessenger显示图, Samsung显示图, Windows显示图, GMail显示图, SoftBank显示图, DoCoMo显示图, KDDI显示图, 英文名称, 日期, tag_name) VALUES\n"); 259 | } 260 | 261 | if ((nEmojiRow % 100) == 0) // 每个 INSERT 的第一条数据 262 | fwSQL.write ("\t ("); 263 | else 264 | fwSQL.write ("\n\t, ("); 265 | 266 | nEmojiRow ++; 267 | int i=0; 268 | String s序号 = eCols.get (i++).text (); 269 | String sCode = eCols.get (i++).text (); 270 | String sChars = eCols.get (i++).text (); 271 | String s浏览器显示图 = ExtractImageData (eCols.get (i++).select ("img").attr ("src")); 272 | String sApple显示图 = ExtractImageData (eCols.get (i++).select ("img").attr ("src")); 273 | String sGoogle显示图 = ExtractImageData (eCols.get (i++).select ("img").attr ("src")); 274 | String sTwitter显示图 = ExtractImageData (eCols.get (i++).select ("img").attr ("src")); 275 | String sEmojiOne显示图 = ExtractImageData (eCols.get (i++).select ("img").attr ("src")); 276 | String sFacebook显示图 = ExtractImageData (eCols.get (i++).select ("img").attr ("src")); 277 | String sFacebookMessenger显示图 = ExtractImageData (eCols.get (i++).select ("img").attr ("src")); 278 | String sSamsung显示图 = ExtractImageData (eCols.get (i++).select ("img").attr ("src")); 279 | String sWindows显示图 = ExtractImageData (eCols.get (i++).select ("img").attr ("src")); 280 | String sGMail显示图 = ExtractImageData (eCols.get (i++).select ("img").attr ("src")); 281 | String sSoftBank显示图 = ExtractImageData (eCols.get (i++).select ("img").attr ("src")); 282 | String sDoCoMo显示图 = ExtractImageData (eCols.get (i++).select ("img").attr ("src")); 283 | String sKDDI显示图 = ExtractImageData (eCols.get (i++).select ("img").attr ("src")); 284 | String s英文名称 = eCols.get (i++).text (); 285 | String s日期时间 = eCols.get (i++).text (); 286 | String s关键字 = eCols.get (i++).text (); 287 | 288 | fwSQL.write (s序号); 289 | fwSQL.write (", '" + sCode + "'"); 290 | fwSQL.write (", '" + sChars + "'"); 291 | fwSQL.write (", '" + s浏览器显示图 + "'"); 292 | fwSQL.write (", '" + sApple显示图 + "'"); 293 | fwSQL.write (", '" + sGoogle显示图 + "'"); 294 | fwSQL.write (", '" + sTwitter显示图 + "'"); 295 | fwSQL.write (", '" + sEmojiOne显示图 + "'"); 296 | fwSQL.write (", '" + sFacebook显示图 + "'"); 297 | fwSQL.write (", '" + sFacebookMessenger显示图 + "'"); 298 | fwSQL.write (", '" + sSamsung显示图 + "'"); 299 | fwSQL.write (", '" + sWindows显示图 + "'"); 300 | fwSQL.write (", '" + sGMail显示图 + "'"); 301 | fwSQL.write (", '" + sSoftBank显示图 + "'"); 302 | fwSQL.write (", '" + sDoCoMo显示图 + "'"); 303 | fwSQL.write (", '" + sKDDI显示图 + "'"); 304 | fwSQL.write (", '" + s英文名称 + "'"); 305 | fwSQL.write (", '" + s日期时间 + "'"); 306 | fwSQL.write (", '" + s关键字 + "'"); 307 | fwSQL.write (')'); 308 | 309 | System.out.println (sChars); 310 | } 311 | fwSQL.write (";"); 312 | 313 | fwSQL.close (); 314 | } 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /bots/net_maclife_wechat_http_Bot_GoogleImageSearch.java: -------------------------------------------------------------------------------- 1 | import java.io.*; 2 | import java.net.*; 3 | import java.nio.file.*; 4 | import java.util.*; 5 | 6 | import org.apache.commons.io.*; 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.jsoup.*; 9 | import org.jsoup.nodes.*; 10 | import org.jsoup.select.*; 11 | 12 | import com.fasterxml.jackson.databind.*; 13 | 14 | public class net_maclife_wechat_http_Bot_GoogleImageSearch extends net_maclife_wechat_http_Bot 15 | { 16 | public static String GOOGLE_BASE_URL = "https://www.google.com.hk"; 17 | public static String GOOGLE_IMAGE_SEARCH_URL = GOOGLE_BASE_URL + "/searchbyimage"; 18 | public static boolean USE_GFW_PROXY = net_maclife_wechat_http_BotApp.ParseBoolean (net_maclife_wechat_http_BotApp.GetConfig ().getString ("google.useGFWProxy"), true); 19 | public static String GFW_PROXY_TYPE = StringUtils.upperCase (net_maclife_wechat_http_BotApp.GetConfig ().getString ("app.gfw.proxy.type")); 20 | public static String GFW_PROXY_HOST = net_maclife_wechat_http_BotApp.GetConfig ().getString ("app.gfw.proxy.host"); 21 | public static int GFW_PROXY_PORT = net_maclife_wechat_http_BotApp.GetConfig ().getInt ("app.gfw.proxy.port"); 22 | static Proxy gfwProxy = null; 23 | //static HttpHost gfwProxy_forApacheHttpCore = null; 24 | static 25 | { 26 | if (USE_GFW_PROXY) 27 | { 28 | gfwProxy = new Proxy (Proxy.Type.valueOf (GFW_PROXY_TYPE), new InetSocketAddress(GFW_PROXY_HOST, GFW_PROXY_PORT)); 29 | //gfwProxy_forApacheHttpCore = new HttpHost (GFW_PROXY_HOST, GFW_PROXY_PORT, GFW_PROXY_TYPE); 30 | } 31 | } 32 | 33 | static final String sMultipartBoundary = "JsoupDoesNotSupportFormDataWell, and, ApacheHCDoesNotSupportSOCKSProxy"; 34 | 35 | @Override 36 | public int OnImageMessageReceived 37 | ( 38 | JsonNode jsonMessage, 39 | JsonNode jsonFrom, String sFromAccount, String sFromName, boolean isFromMe, 40 | JsonNode jsonTo, String sToAccount, String sToName, boolean isToMe, 41 | JsonNode jsonReplyTo, String sReplyToAccount, String sReplyToName, boolean isReplyToRoom, 42 | JsonNode jsonReplyTo_RoomMember, String sReplyToAccount_RoomMember, String sReplyToName_RoomMember, 43 | JsonNode jsonReplyTo_Person, String sReplyToAccount_Person, String sReplyToName_Person, 44 | String sContent, File fMedia, String sImageURL 45 | ) 46 | { 47 | if ((fMedia == null || ! fMedia.exists ()) && StringUtils.isEmpty (sImageURL)) 48 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 49 | 50 | String sURL = GOOGLE_IMAGE_SEARCH_URL; 51 | Document doc = null; 52 | org.jsoup.Connection jsoup_conn = null; 53 | InputStream fis = null; 54 | String sResponseBody = null; 55 | try 56 | { 57 | if (StringUtils.isNotEmpty (sImageURL)) 58 | { // GET 方法访问 59 | sURL = sURL + "?hl=zh-CN&image_url=" + URLEncoder.encode (sImageURL, net_maclife_wechat_http_BotApp.utf8); 60 | } 61 | else 62 | { // POST 方法访问 63 | sURL = sURL + "/upload"; 64 | } 65 | 66 | /* 67 | jsoup_conn = org.jsoup.Jsoup.connect (sURL); 68 | jsoup_conn.timeout (net_maclife_util_HTTPUtils.DEFAULT_READ_TIMEOUT_SECOND * 1000); 69 | if (USE_GFW_PROXY) 70 | { 71 | jsoup_conn.proxy (gfwProxy); 72 | } 73 | jsoup_conn 74 | .followRedirects (false) 75 | //.referrer ("https://www.google.com.hk/") 76 | .userAgent ("Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:50.0) Gecko/20100101 Firefox/1234 Firefox is versioning emperor #2, Chrome is versioning emperor #1!!!") 77 | .header ("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3") 78 | //.header ("Accept-Encoding", "gzip, deflate, br") 79 | //.header ("Cookie", "NID=91=eLR2Xt0oeN-XCDP3lQkbfBLqFU0fTxLq5ocj7lYBbkKKQBqnmvTJy-y9v3Y73nPQc_PIx59ir3T7hqmyPFAH02xSg6cCp9wqTiSTVGb0HuHqWd8U75jxpKeF47FK8DKl59mUX0WsfGjDzFvzsllfF6_HfPcBW54OATKvBBgseC-yBbJPE30YmK_z3KTWFiRRdWzWYAMTgeXERmDXBqFFpHZQLNebcQHkCTLLuBuAe5MsC2PIJs1TV8iYda_kbGFVvvvboNJTBw0eKwK9sPPt5NODU5s; SID=EwM1NpOSROI0ddQDNSMzCdQV7PF1NsutdbHv1QnVNhf2qSP3LtF-dkfUuJuBCZU5bXaMmQ.; HSID=AHHYjmOQYposTbxEx; APISID=S_Ga4t7dY6Xj_2IY/ATJD3hDdWt0OYN88-; SSID=AhuyWEzQ4qpQEbiW1; SAPISID=c91O3a08aWSgUrKP/AVd_0zQa4fzUH9adu; DV=grlNNAF72VBKxtBGytCv9RBrGlCGsQpb23j9sCB7RwAAAGqv7e54uemJFAAAAJa5wcBxIXwPCQAAAA") 80 | //.header ("Connection", "keep-alive") 81 | //.header ("Upgrade-Insecure-Requests", "1") 82 | ; 83 | if (! StringUtils.isNotEmpty (sImageURL)) 84 | { 85 | fis = new FileInputStream (fMedia); 86 | //jsoup_conn.data ("image_url", ""); 87 | jsoup_conn.data ("encoded_image", fMedia.getName (), fis); // 在浏览器开发工具里看,Google 图片搜索在 http 请求头里并未设置 Content-Type,而是在消息体里设置的。但是 jsoup 就设置在了请求头里,然后请求消息体里少了这部分,导致返回的数据不正确 88 | 89 | //byte[] arrayImg = IOUtils.toByteArray (fis); 90 | //jsoup_conn.data ("image_content", Base64.encodeBase64String (arrayImg)); 91 | //jsoup_conn.data ("filename", fMedia.getName ()); 92 | 93 | jsoup_conn.data ("hl", "zh-CN"); 94 | doc = jsoup_conn.post (); 95 | fis.close (); 96 | } 97 | else 98 | { 99 | doc = jsoup_conn.get (); 100 | } 101 | System.out.println (jsoup_conn.response ().header ("Location")); 102 | System.out.println (doc); 103 | //*/ 104 | 105 | // 106 | // HttpClient 不支持 SOCKS 代理 107 | // http://stackoverflow.com/questions/22937983/how-to-use-socks-5-proxy-with-apache-http-client-4 <-- 绝不这么做 I'm not doing this crap 108 | // 109 | /* 110 | MultipartEntityBuilder meb = MultipartEntityBuilder.create (); 111 | FormBodyPartBuilder fbpb = FormBodyPartBuilder.create (); 112 | //fbpb.addField (name, value) 113 | //FormBodyPart bodyPart = fbpb.build (); 114 | meb.addTextBody ("image_url", ""); 115 | meb.addBinaryBody ("encoded_image", fMedia); 116 | meb.addTextBody ("image_content", ""); 117 | meb.addTextBody ("filename", ""); 118 | meb.addTextBody ("hl", "zh-CN"); 119 | 120 | //MultipartEntity entity = new MultipartEntity(); 121 | //entity.addPart("user", new StringBody("user")); 122 | //entity.addPart("password", new StringBody("12345")); 123 | //entity.addPart("encoded_image", new FileBody(fMedia)); 124 | 125 | RequestConfig config = RequestConfig.custom() 126 | .setConnectTimeout (net_maclife_util_HTTPUtils.DEFAULT_CONNECT_TIMEOUT_SECOND * 1000) 127 | .setConnectionRequestTimeout (net_maclife_util_HTTPUtils.DEFAULT_READ_TIMEOUT_SECOND * 1000) 128 | //.setProxy (gfwProxy_forApacheHttpCore) 129 | .build(); 130 | HttpPost post = new HttpPost (sURL); 131 | post.setConfig (config); 132 | post.setEntity (meb.build ()); 133 | //System.out.println (EntityUtils.toString (entity)); 134 | 135 | HttpClient httpClient = HttpClients.createDefault (); //new DefaultHttpClient(); 136 | HttpResponse response = httpClient.execute (post); 137 | sResponseBody = EntityUtils.toString (response.getEntity()); 138 | //*/ 139 | 140 | // 自己构造 multipart/form-data 消息体 141 | //* 142 | OutputStream os = null; 143 | byte[] arrayPostData = null; 144 | Map mapRequestHeaders = new HashMap (); 145 | mapRequestHeaders.put ("Content-Type", ""); // Java HttpURLConnection 你妈的能不能彻底删除 Content-Type 消息头啊 146 | mapRequestHeaders.put ("Content-Length", ""); // Java HttpURLConnection 你妈的能不能彻底删除 Content-Length 消息头啊 147 | mapRequestHeaders.put ("User-Agent", "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:50.0) Gecko/20100101 Firefox/1234 Firefox is versioning emperor #2, Chrome is versioning emperor #1!!!"); // 经过多次测试,User-Agent 和/或 Accept-Language 头是必须要的,否则返回不了正确响应 148 | mapRequestHeaders.put ("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3"); 149 | if (StringUtils.isNotEmpty (sImageURL)) 150 | { // GET 方法访问 151 | if (USE_GFW_PROXY) 152 | sResponseBody = net_maclife_util_HTTPUtils.CURL_ViaProxy (sURL, mapRequestHeaders, GFW_PROXY_TYPE, GFW_PROXY_HOST, GFW_PROXY_PORT); 153 | else 154 | sResponseBody = net_maclife_util_HTTPUtils.CURL (sURL, mapRequestHeaders); 155 | } 156 | else 157 | { // POST 方法访问 158 | ByteArrayOutputStream baos = new ByteArrayOutputStream (); 159 | //net_maclife_util_HTTPUtils.FillMultipartSimplely (baos, sMultipartBoundary, "image_url", ""); 160 | 161 | String sImageContentType = Files.probeContentType (fMedia.toPath ()); 162 | net_maclife_util_HTTPUtils.FillMultipartSimplely (baos, sMultipartBoundary, "encoded_image", fMedia, sImageContentType); 163 | 164 | //InputStream is = new FileInputStream (fMedia); 165 | //byte[] arrayImg = IOUtils.toByteArray (is); 166 | //is.close (); 167 | //net_maclife_util_HTTPUtils.FillMultipartSimplely (baos, sMultipartBoundary, "image_content", Base64.encodeBase64String (arrayImg)); 168 | //net_maclife_util_HTTPUtils.FillMultipartSimplely (baos, sMultipartBoundary, "filename", fMedia.getName ()); 169 | 170 | net_maclife_util_HTTPUtils.FillMultipartSimplely (baos, sMultipartBoundary, "hl", "zh-CN"); 171 | net_maclife_util_HTTPUtils.FillMultipartSimplelyEnd (baos, sMultipartBoundary); 172 | baos.flush (); 173 | 174 | ByteArrayOutputStream baos_multipart = null; 175 | //baos_multipart = new ByteArrayOutputStream (); 176 | //baos_multipart.write (("Content-Type: multipart/form-data; boundary=" + sMultipartBoundary + "\r\nContent-Length: " + baos.size () + "\r\n\r\n").getBytes ()); 177 | //baos.writeTo (baos_multipart); 178 | //baos_multipart.flush (); 179 | mapRequestHeaders.put ("Content-Type", "multipart/form-data; boundary=" + sMultipartBoundary); 180 | mapRequestHeaders.put ("Content-Length", String.valueOf (baos.size ())); 181 | baos_multipart = baos; 182 | 183 | arrayPostData = baos_multipart.toByteArray (); 184 | //os = new FileOutputStream ("google-image-search-post.data"); 185 | //IOUtils.write (arrayPostData, os); 186 | //os.close (); 187 | 188 | URLConnection http = null; 189 | 190 | if (USE_GFW_PROXY) 191 | { 192 | //sResponseBody = net_maclife_util_HTTPUtils.CURL_Post_ViaProxy (sURL, mapRequestHeaders, arrayPostData, GFW_PROXY_TYPE, GFW_PROXY_HOST, GFW_PROXY_PORT); 193 | http = (URLConnection) net_maclife_util_HTTPUtils.CURL 194 | ("POST", sURL, mapRequestHeaders, arrayPostData, true, true, null, false /* 不跟随重定向 */, 0, 0, 195 | GFW_PROXY_TYPE, GFW_PROXY_HOST, GFW_PROXY_PORT, 196 | true, true, null, null, null, null, null, null 197 | ); 198 | } 199 | else 200 | { 201 | //sResponseBody = net_maclife_util_HTTPUtils.CURL_Post (sURL, mapRequestHeaders, arrayPostData); 202 | http = (URLConnection) net_maclife_util_HTTPUtils.CURL 203 | ("POST", sURL, mapRequestHeaders, arrayPostData, true, true, null, false /* 不跟随重定向 */, 0, 0, 204 | null, null, 0, 205 | true, true, null, null, null, null, null, null 206 | ); 207 | } 208 | if (http != null) 209 | { 210 | int iResponseCode = ((HttpURLConnection)http).getResponseCode(); 211 | String sStatusLine = http.getHeaderField(0); // HTTP/1.1 200 OK、HTTP/1.1 404 Not Found 212 | 213 | int iMainResponseCode = iResponseCode/100; 214 | if (iMainResponseCode == 3) 215 | { 216 | // 之前测试几天都返回不了正确的结果,是因为默认设置了“跟随重定向”, 217 | // 可是,openjdk 在自动重定向到该网址时,却丢掉了前面设置的 Accept-Language User-Agent 请求头信息,导致 Google 返回不了预期的结果。 218 | // 所以,在这里,要截获重定向的网址,加上请求头后再次访问 219 | String sRedirectedURL = http.getHeaderField ("Location"); 220 | 221 | String sSetCookie = http.getHeaderField ("Set-Cookie"); 222 | 223 | mapRequestHeaders.remove ("Content-Type"); 224 | mapRequestHeaders.remove ("Content-Length"); 225 | if (StringUtils.isNotEmpty (sSetCookie)) 226 | { 227 | mapRequestHeaders.put ("Cookie", sSetCookie); 228 | } 229 | 230 | if (USE_GFW_PROXY) 231 | sResponseBody = net_maclife_util_HTTPUtils.CURL_ViaProxy (sRedirectedURL, mapRequestHeaders, GFW_PROXY_TYPE, GFW_PROXY_HOST, GFW_PROXY_PORT); 232 | else 233 | sResponseBody = net_maclife_util_HTTPUtils.CURL (sRedirectedURL, mapRequestHeaders); 234 | } 235 | else if (iMainResponseCode == 2) 236 | { 237 | net_maclife_wechat_http_BotApp.logger.warning ("就目前的 Google 图片搜索来说,应该不会直接出现 2XX"); 238 | } 239 | else 240 | { 241 | net_maclife_wechat_http_BotApp.logger.severe ("http 响应代码是 " + iResponseCode + ", url = " + sURL); 242 | } 243 | } 244 | } 245 | 246 | //os = new FileOutputStream("google-image-search-result.html"); 247 | //IOUtils.write (sResponseBody, os, net_maclife_wechat_http_BotApp.utf8); 248 | //os.close (); 249 | 250 | doc = Jsoup.parse (sResponseBody, GOOGLE_BASE_URL); 251 | //*/ 252 | 253 | Elements eTopStuff = doc.select ("#topstuff"); 254 | if (eTopStuff.isEmpty ()) 255 | { 256 | net_maclife_wechat_http_BotApp.logger.info (GetName() + " 找不到 #topstuff,也许,搜索出错了? " + eTopStuff.text ()); 257 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 258 | } 259 | Element e图片猜测词语链接 = eTopStuff.select ("a._gUb").first (); 260 | if (e图片猜测词语链接 == null) 261 | { 262 | net_maclife_wechat_http_BotApp.logger.info (GetName() + " 找不到 ._gUb,也许,没有结果? " + eTopStuff.text ()); 263 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 264 | } 265 | 266 | StringBuilder sbInfo = new StringBuilder (); 267 | sbInfo.append (e图片猜测词语链接.text ()); 268 | sbInfo.append ("\n"); 269 | sbInfo.append (e图片猜测词语链接.absUrl ("href")); 270 | Elements e图片来源 = doc.select ("div.normal-header"); 271 | if (! e图片来源.isEmpty ()) 272 | { 273 | sbInfo.append ("\n\n" + e图片来源.select (".rg-header").first ().text ()); // "包含匹配图片的页面" 274 | Elements e图片来源标题链接 = e图片来源.select ("h3.r > a"); 275 | for (int i=0; i 18 | *
  • 在微信群中,其他人用 /addme 命令让本 Bot (自己的微信) 向命令发起者发起一个“请求加好友”的请求
  • 19 | *
  • 如果收到别人发来的“请求加好友”的请求,则,根据“接头暗号”来决定是否自动通过该请求
  • 20 | * 21 | * @author liuyan 22 | * 23 | */ 24 | public class net_maclife_wechat_http_Bot_MakeFriend extends net_maclife_wechat_http_Bot 25 | { 26 | @Override 27 | public int OnTextMessageReceived 28 | ( 29 | JsonNode jsonMessage, 30 | JsonNode jsonFrom, String sFromAccount, String sFromName, boolean isFromMe, 31 | JsonNode jsonTo, String sToAccount, String sToName, boolean isToMe, 32 | JsonNode jsonReplyTo, String sReplyToAccount, String sReplyToName, boolean isReplyToRoom, 33 | JsonNode jsonReplyTo_RoomMember, String sReplyToAccount_RoomMember, String sReplyToName_RoomMember, 34 | JsonNode jsonReplyTo_Person, String sReplyToAccount_Person, String sReplyToName_Person, 35 | String sContent, boolean isContentMentionedMe, boolean isContentMentionedMeFirst 36 | ) 37 | { 38 | List listCommands = net_maclife_wechat_http_BotApp.GetConfig ().getList (String.class, "bot.make-friend.commands"); 39 | if (listCommands==null || listCommands.isEmpty ()) // 如果未配置命令,则不处理 40 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 41 | 42 | if (! net_maclife_wechat_http_BotApp.hasCommandPrefix (sContent)) 43 | { 44 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 45 | } 46 | sContent = net_maclife_wechat_http_BotApp.StripOutCommandPrefix (sContent); 47 | 48 | try 49 | { 50 | String[] arrayMessages = sContent.split ("\\s+", 2); 51 | if (arrayMessages==null || arrayMessages.length<1) 52 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 53 | 54 | String sCommandInputed = arrayMessages[0]; 55 | String sCommandParametersInputed = null; 56 | if (arrayMessages.length >= 2) 57 | sCommandParametersInputed = arrayMessages[1]; 58 | 59 | if (StringUtils.isEmpty (sCommandParametersInputed)) 60 | { 61 | sCommandParametersInputed = "你于 " + new java.sql.Timestamp (System.currentTimeMillis ()) + " 在【" + sReplyToName + "】群内请求加好友"; 62 | } 63 | 64 | String[] arrayCommandOptions = sCommandInputed.split ("\\.+", 2); 65 | sCommandInputed = arrayCommandOptions[0]; 66 | String sCommandOptionsInputed = null; 67 | if (arrayCommandOptions.length >= 2) 68 | sCommandOptionsInputed = arrayCommandOptions[1]; 69 | 70 | for (int i=0; i\n\n可选的语言代码选项的格式:\n - .原文语言代码\n - .2译文语言代码\n - .原文语言代码2译文语言代码\n\n具体能用哪些语言代码,请参照: http://api.fanyi.baidu.com/api/trans/product/apidoc#languageList 给出的语言代码列表"); 93 | // return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 94 | //} 95 | 96 | // 解析“命令选项”: 97 | if (StringUtils.isNotEmpty (sCommandOptionsInputed)) 98 | { 99 | } 100 | 101 | // 命令行命令格式没问题,再检查是否已经是好友了 102 | boolean bAlreadyBeFriend = engine.SearchForSingleContact (sReplyToAccount_RoomMember) != null; 103 | if (bAlreadyBeFriend) 104 | { 105 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, "已经是好友,不需再次加好友"); 106 | return 107 | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__PROCESSED 108 | | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 109 | } 110 | 111 | // 现在向命令使用者发起“加好友请求”消息 112 | //int nGender = net_maclife_wechat_http_BotApp.GetJSONInt (jsonReplyTo_Person, "Sex"); 113 | JsonNode jsonResult = engine.SendRequestToMakeFriend (sReplyToAccount_RoomMember, sCommandParametersInputed); 114 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, "已向你【" + sReplyToName_RoomMember + "】发起“加好友请求”,携带的验证消息为:\n\n" + sCommandParametersInputed + "\n\n如果需要手工指定验证消息,请在命令后输入验证消息即可,如:\naddme " + sReplyToName_RoomMember + " 是坠棒的"); 115 | break; 116 | } 117 | } 118 | catch (Exception e) 119 | { 120 | e.printStackTrace (); 121 | } 122 | 123 | return 124 | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__PROCESSED 125 | | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 126 | } 127 | 128 | 129 | @Override 130 | public int OnRequestToMakeFriendMessageReceived 131 | ( 132 | JsonNode jsonMessage, 133 | JsonNode jsonFrom, String sFromAccount, String sFromName, boolean isFromMe, 134 | JsonNode jsonTo, String sToAccount, String sToName, boolean isToMe, 135 | JsonNode jsonReplyTo, String sReplyToAccount, String sReplyToName, boolean isReplyToRoom, 136 | JsonNode jsonReplyTo_RoomMember, String sReplyToAccount_RoomMember, String sReplyToName_RoomMember, 137 | JsonNode jsonReplyTo_Person, String sReplyToAccount_Person, String sReplyToName_Person, 138 | String sContent, JsonNode jsonRecommenedInfo, Element xmlMsg 139 | ) 140 | { 141 | List listAutoAccepts = net_maclife_wechat_http_BotApp.GetConfig ().getList (String.class, "bot.make-friend.auto-accepts"); 142 | if (listAutoAccepts==null || listAutoAccepts.isEmpty ()) // 如果未配置暗号,则不处理 143 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 144 | 145 | String s微信ID = net_maclife_wechat_http_BotApp.GetJSONText (jsonRecommenedInfo, "UserName"); 146 | //String s昵称 = net_maclife_wechat_http_BotApp.GetJSONText (jsonRecommenedInfo, "NickName"); 147 | //String s微信号 = net_maclife_wechat_http_BotApp.GetJSONText (jsonRecommenedInfo, "Alias"); 148 | //int n性别 = net_maclife_wechat_http_BotApp.GetJSONInt (jsonRecommenedInfo, "Sex"); 149 | String s个性签名 = net_maclife_wechat_http_BotApp.GetJSONText (jsonRecommenedInfo, "Signature"); 150 | //String s省 = net_maclife_wechat_http_BotApp.GetJSONText (jsonRecommenedInfo, "Province"); 151 | //String s市 = net_maclife_wechat_http_BotApp.GetJSONText (jsonRecommenedInfo, "City"); 152 | String s附加内容 = net_maclife_wechat_http_BotApp.GetJSONText (jsonRecommenedInfo, "Content"); 153 | int nScene = net_maclife_wechat_http_BotApp.GetJSONInt (jsonRecommenedInfo, "Scene"); // 根据什么来请求加好友的? 154 | String sMakeFriendTicket = net_maclife_wechat_http_BotApp.GetJSONText (jsonRecommenedInfo, "Ticket"); 155 | int nOpCode = net_maclife_wechat_http_BotApp.GetJSONInt (jsonRecommenedInfo, "OpCode"); // 固定为 2 ? 156 | 157 | try 158 | { 159 | for (int i=0; i 1) 167 | { 168 | sAutoReplyMessage = arrayAutoAccept [1]; 169 | sAutoReplyMessage = StringEscapeUtils.unescapeJava (sAutoReplyMessage); // 允许用 \n 的方式发送多行文本消息 170 | } 171 | 172 | if (StringUtils.startsWithIgnoreCase (sKeyword, "*") && StringUtils.endsWithIgnoreCase (sKeyword, "*")) 173 | { 174 | sKeyword = StringUtils.substring (sKeyword, 1, sKeyword.length () - 1); 175 | bMatched = StringUtils.containsIgnoreCase (s附加内容, sKeyword); 176 | } 177 | else if (StringUtils.startsWithIgnoreCase (sKeyword, "*")) 178 | { 179 | sKeyword = StringUtils.substring (sKeyword, 1); 180 | bMatched = StringUtils.endsWithIgnoreCase (s附加内容, sKeyword); 181 | } 182 | else if (StringUtils.endsWithIgnoreCase (sKeyword, "*")) 183 | { 184 | sKeyword = StringUtils.substring (sKeyword, 0, sKeyword.length () - 1); 185 | bMatched = StringUtils.startsWithIgnoreCase (s附加内容, sKeyword); 186 | } 187 | else 188 | { 189 | bMatched = StringUtils.equalsIgnoreCase (s附加内容, sKeyword); 190 | } 191 | 192 | if (bMatched) 193 | { 194 | JsonNode jsonResult = engine.AcceptRequestToMakeFriend (sMakeFriendTicket, nScene, /*sReplyToAccount_RoomMember*/s微信ID, "暗号已对上,自动通过"); 195 | if (StringUtils.isNotEmpty (sAutoReplyMessage)) 196 | { 197 | SendTextMessage (sReplyToAccount_Person, sReplyToName_Person, sAutoReplyMessage); 198 | } 199 | break; 200 | } 201 | } 202 | } 203 | catch (Exception e) 204 | { 205 | e.printStackTrace(); 206 | } 207 | 208 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /bots/net_maclife_wechat_http_Bot_Manager.java: -------------------------------------------------------------------------------- 1 | import java.io.*; 2 | import java.util.*; 3 | import java.util.logging.*; 4 | 5 | import org.apache.commons.lang3.*; 6 | 7 | import com.fasterxml.jackson.databind.*; 8 | 9 | /** 10 | * 远程管理 机器人。 11 | * 该机器人基本上就是:将控制台里的命令,用 Bot 再重新实现一遍,以达到:“不在电脑跟前时,用微信文字消息对 Bot 进行部分(嗯,只能是一部分)远程管理”的目的。 12 | * @author liuyan 13 | * 14 | */ 15 | public class net_maclife_wechat_http_Bot_Manager extends net_maclife_wechat_http_Bot 16 | { 17 | @Override 18 | public int OnTextMessageReceived 19 | ( 20 | JsonNode jsonMessage, 21 | JsonNode jsonFrom, String sFromAccount, String sFromName, boolean isFromMe, 22 | JsonNode jsonTo, String sToAccount, String sToName, boolean isToMe, 23 | JsonNode jsonReplyTo, String sReplyToAccount, String sReplyToName, boolean isReplyToRoom, 24 | JsonNode jsonReplyTo_RoomMember, String sReplyToAccount_RoomMember, String sReplyToName_RoomMember, 25 | JsonNode jsonReplyTo_Person, String sReplyToAccount_Person, String sReplyToName_Person, 26 | String sContent, boolean isContentMentionedMe, boolean isContentMentionedMeFirst 27 | ) 28 | { 29 | try 30 | { 31 | if (! net_maclife_wechat_http_BotApp.hasCommandPrefix (sContent)) 32 | { 33 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 34 | } 35 | sContent = net_maclife_wechat_http_BotApp.StripOutCommandPrefix (sContent); 36 | 37 | if (StringUtils.equalsIgnoreCase (sContent, net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.manager.command.list-bots"))) 38 | { 39 | StringBuilder sb = new StringBuilder (); 40 | for (int i=0; i= 2) 59 | sCommandParametersInputed = arrayMessages[1]; 60 | 61 | String[] arrayCommandOptions = sCommandInputed.split ("\\" + net_maclife_wechat_http_BotApp.COMMAND_OPTION_SEPARATOR + "+", 2); 62 | sCommandInputed = arrayCommandOptions[0]; 63 | String sCommandOptionsInputed = null; 64 | if (arrayCommandOptions.length >= 2) 65 | sCommandOptionsInputed = arrayCommandOptions[1]; 66 | 67 | if (StringUtils.equalsIgnoreCase (sCommandInputed, net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.manager.command.load-bot"))) 68 | { 69 | LoadBot (sCommandParametersInputed, sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember); 70 | } 71 | else if (StringUtils.equalsIgnoreCase (sCommandInputed, net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.manager.command.unload-bot"))) 72 | { 73 | boolean bForced = StringUtils.equalsIgnoreCase (sCommandOptionsInputed, "force"); 74 | if (StringUtils.equals (sCommandParametersInputed, getClass ().getCanonicalName ()) && !bForced) 75 | { 76 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, "要卸载的机器人是本机器人,默认不允许这么操作,除非用 .force 选项强制执行"); 77 | return 0; 78 | } 79 | UnloadBot (sCommandParametersInputed, sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember); 80 | } 81 | else if (StringUtils.equalsIgnoreCase (sCommandInputed, net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.manager.command.log-level"))) 82 | { 83 | if (StringUtils.isEmpty (sCommandParametersInputed)) 84 | { 85 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, "当前日志级别: " + net_maclife_wechat_http_BotApp.logger.getLevel ()); 86 | } 87 | else 88 | { 89 | try 90 | { 91 | String sNewLogLevel = StringUtils.upperCase (sCommandParametersInputed); 92 | net_maclife_wechat_http_BotApp.logger.setLevel (Level.parse (sNewLogLevel)); 93 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, "日志级别已改为: " + net_maclife_wechat_http_BotApp.logger.getLevel ()); 94 | } 95 | catch (IllegalArgumentException e) 96 | { 97 | e.printStackTrace (); 98 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, "非法日志级别: " + sCommandParametersInputed + ", 请换有效的日志级别名称,比如 all finest finer fine info warning severe 1000 0 1 ..."); 99 | } 100 | } 101 | } 102 | else if (StringUtils.equalsAnyIgnoreCase (sCommandInputed, 103 | net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.manager.command.invite"), 104 | net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.manager.command.kick"), 105 | net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.manager.command.topic") 106 | ) 107 | ) 108 | { 109 | if (! isReplyToRoom) 110 | { 111 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, "邀请或者踢人或改群名操作需要在群内执行"); 112 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 113 | } 114 | UpdateChatRoom (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, sCommandInputed, sCommandOptionsInputed, sCommandParametersInputed); 115 | } 116 | else if (StringUtils.equalsAnyIgnoreCase (sCommandInputed, net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.manager.command.new-room")) 117 | ) 118 | { 119 | CreateNewRoom (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, sCommandInputed, sCommandOptionsInputed, sCommandParametersInputed); 120 | } 121 | } 122 | } 123 | catch (Exception e) 124 | { 125 | e.printStackTrace (); 126 | } 127 | 128 | return 129 | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__PROCESSED 130 | | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 131 | } 132 | 133 | public void LoadBot (String sBotClassName, String sReplyToAccount, String sReplyToName, String sReplyToAccount_RoomMember, String sReplyToName_RoomMember) 134 | { 135 | try 136 | { 137 | if (StringUtils.isEmpty (sBotClassName)) 138 | { 139 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, "加载机器人需要指定机器人 java 类的完整类名"); 140 | return; 141 | } 142 | Class botClass = Class.forName (sBotClassName); 143 | Object obj = botClass.newInstance (); 144 | if (obj instanceof net_maclife_wechat_http_Bot) 145 | { 146 | net_maclife_wechat_http_Bot newBot = (net_maclife_wechat_http_Bot) obj; 147 | boolean bAlreadyLoaded = false; 148 | // 检查有没有该类的实例存在,有的话,则不再重复添加 149 | for (int i=0; i <朋友帐号/微信号/备注名/昵称>"); 269 | return; 270 | } 271 | 272 | List listFriends = net_maclife_wechat_http_BotApp.SplitCommandLine (sCommandParametersInputed); // 鉴于用户昵称可能包含空格或特殊字符的情况,这里必须用 SplitCommandLine 函数处理,不能简单的 .split (" ") 273 | StringBuilder sbFriendsAccounts = new StringBuilder (); 274 | for (int i=0; i listParams = net_maclife_wechat_http_BotApp.SplitCommandLine (sCommandParametersInputed); // 鉴于用户昵称可能包含空格或特殊字符的情况,这里必须用 SplitCommandLine 函数处理,不能简单的 .split (" ") 330 | //if (StringUtils.isEmpty (sCommandParametersInputed)) 331 | if (listParams.size () < 3) 332 | { 333 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, "必须输入新群名,以及,至少输入两个联系人帐号/微信号/备注名/昵称。如果群名、昵称有空格等特殊字符,请用引号引起来。\n\n" + sCommandInputed + "[" + net_maclife_wechat_http_BotApp.COMMAND_OPTION_SEPARATOR + "帐号|" + net_maclife_wechat_http_BotApp.COMMAND_OPTION_SEPARATOR + "微信号|" + net_maclife_wechat_http_BotApp.COMMAND_OPTION_SEPARATOR + "备注名|" + net_maclife_wechat_http_BotApp.COMMAND_OPTION_SEPARATOR + "昵称] <新群名> <朋友帐号/微信号/备注名/昵称> <朋友帐号/微信号/备注名/昵称>..."); 334 | return; 335 | } 336 | String sNewRoomName = listParams.remove (0); 337 | 338 | String sSearchBy = sCommandOptionsInputed; 339 | String sNameOfSearchBy = ""; 340 | 341 | if (StringUtils.equalsAnyIgnoreCase (sSearchBy, "Account", "帐号")) 342 | sNameOfSearchBy = "帐号"; 343 | else if (StringUtils.equalsAnyIgnoreCase (sSearchBy, "Alias", "微信号")) 344 | sNameOfSearchBy = "微信号"; 345 | else if (StringUtils.equalsAnyIgnoreCase (sSearchBy, "RemarkName", "备注名")) 346 | sNameOfSearchBy = "备注名"; 347 | else if (StringUtils.equalsAnyIgnoreCase (sSearchBy, "NickName", "昵称") || StringUtils.isEmpty (sSearchBy)) 348 | sNameOfSearchBy = "昵称"; 349 | else 350 | { 351 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, "不知道你要根据什么找人… sSearchByName = " + sNameOfSearchBy); 352 | return; 353 | } 354 | 355 | //listParams.add (0, engine.sMyEncryptedAccountInThisSession); 356 | for (int i=0; i listReplies = net_maclife_wechat_http_BotApp.GetConfig ().getList (String.class, "bot.missile-launched.replies"); 30 | if (listReplies!=null && listReplies.size ()>0) 31 | { 32 | int iRandom = net_maclife_wechat_http_BotApp.random.nextInt (listReplies.size ()); 33 | sReply = listReplies.get (iRandom); 34 | } 35 | 36 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, "经度: " + sLongtitude + "\n纬度: " + sLatitude + "\n位置: " + sLocation + (StringUtils.isEmpty (sReply) ? "" : "\n" + sReply)); 37 | return 38 | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__PROCESSED 39 | | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 40 | } 41 | catch (Exception e) 42 | { 43 | e.printStackTrace (); 44 | } 45 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /bots/net_maclife_wechat_http_Bot_Relay.java: -------------------------------------------------------------------------------- 1 | import java.io.*; 2 | import java.net.*; 3 | import java.util.*; 4 | 5 | import org.apache.commons.io.*; 6 | import org.apache.commons.lang3.*; 7 | 8 | import com.fasterxml.jackson.core.*; 9 | import com.fasterxml.jackson.databind.*; 10 | 11 | /** 12 | * 消息转发/消息中继机器人。 13 | * 该机器人将从 Socket 接收 JSON 格式的数据,根据数据中指定的接收人、消息类型、消息内容发送到微信好友。 14 | * 15 | *
    16 | * JSON 消息格式 17 | *
     18 | {
     19 | 	From: "",	// 来自,可以是任意内容,但通常指定应用程序名称及版本。该字段可省略(等同于值为空的 From)
     20 | 	To:	// 消息接收人,接收人可以是单个接收人、也可以多个(数组)。通过指定 Account、Alias、RemarkName、NickName 几个名称中的一个或多个的方式来定位该接收人。
     21 | 		//     为了避免重名的情况,建议是用 Account 或 Alias 来定位接收人。 对于 Account,建议使用在后台日志中找到联系人的【明文 ID / 明文 Account】 -- 唯一、不变、相比加密帐号也短些且容易分辨些。
     22 | 		// 如果是单个接收人,只需要采取下面数组内的格式即可,如:{Account: "weixin"}
     23 | 		// 如果是多个接收人,只需要采取下面数组格式即可,如:[{Account: "weixin"}, {Alias: "一个微信号"}, {RemarkName: "一个备注名"}, {NickName: "一个昵称"}]
     24 | 		[
     25 | 			{	// 这几个字段,至少需要指定其中的一个。
     26 | 				Account: "明文帐号,或加密帐号(前体是你得知道每次登录后该会话的加密帐号,通常是要手工在后台日志中获取),或通配符帐号"。
     27 | 					// 明文帐号: 是在手机端打开一个联系人聊天窗口,然后在网页端获取到的明文,诸如: wxid_XXXX (比较常见的个人帐号)、 gh_XXX (公众号帐号)、 NNNNNN@chatroom (群聊帐号)、还有一些用户自定义的帐号(早期注册的?)
     28 | 					// 通配符帐号: 通配符帐号的目的是为了群发用。目前支持的通配符帐号有:
     29 | 						// * - 通讯录里的所有联系人;含群聊天室、公众号(含类似“微信团队”之类的);
     30 | 						// *p - 通讯录除去群聊天室、公众号后的联系人 (人/Person,但含类似“文件传输助手”这类的非“人”的帐号 -- 暂时没法区分);
     31 | 						// *w*g - 所有联系人 (女人/Woman/Girl);
     32 | 						// *m*b - 所有联系人 (男人/Man/Boy);
     33 | 						// *r - 通讯录里的所有聊天室联系人 (Room);
     34 | 						// *gh - 通讯录里的所有公众号联系人;(gh 是公众号的明文 ID/明文 Account 的帐号前缀,g公 h号?)
     35 | 				Alias: "别名",
     36 | 				RemarkName: "备注名",
     37 | 				NickName: "昵称",	// 昵称可能会被好友自己改动,所以,不建议用这个
     38 | 				//DisplayName: "聊天室/群内 人在此聊天室/群的“我在本群的昵称”",
     39 | 			}
     40 | 			, ...
     41 | 		],
     42 | 	UseAppendBotNameConfig: true | false,	// true | false,是否使用“在消息中附加 Bot 名”配置项。若没有该 json 属性,则默认为 true -- 兼容以前的配置
     43 | 	AppendBotName: true | false,			// true | false,当 UseAppendBotNameConfig 为 false 时,则使用该参数明确指定是否要“在消息中附加 Bot 名”。若没有该 json 属性,则默认为 false -- 兼容以前的配置
     44 | 	MessageType: "消息类型",	// 取值 "text" | "voice" | "video"。 该字段也可以忽略,若为空或者忽略,则默认为 "text"
     45 | 	Message: "消息"	// 对于 text 文本消息,直接传文字。 对于 voice 和 video,需要用 base64 编码后传递
     46 | }
     47 |  * 
    48 | * @author liuyan 49 | * 50 | */ 51 | public class net_maclife_wechat_http_Bot_Relay extends net_maclife_wechat_http_Bot implements Runnable 52 | { 53 | ServerSocket ss = null; 54 | 55 | @Override 56 | public void Start () 57 | { 58 | try 59 | { 60 | String sListenAddress = net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.relay.listen.address"); 61 | int nListenPort = net_maclife_wechat_http_BotApp.GetConfig ().getInt ("bot.relay.listen.port"); 62 | 63 | ss = new ServerSocket (); 64 | ss.bind (new InetSocketAddress (InetAddress.getByName (sListenAddress), nListenPort)); 65 | } 66 | catch (IOException e) 67 | { 68 | net_maclife_wechat_http_BotApp.logger.severe (GetName () + " 启动失败: " + e); 69 | e.printStackTrace(); 70 | return; 71 | } 72 | 73 | if (botTask == null) 74 | { 75 | botTask = net_maclife_wechat_http_BotApp.executor.submit (this); 76 | } 77 | } 78 | 79 | @Override 80 | public void Stop () 81 | { 82 | try 83 | { 84 | super.Stop (); 85 | if (ss != null && !ss.isClosed ()) 86 | { 87 | ss.close (); 88 | } 89 | } 90 | catch (IOException e) 91 | { 92 | e.printStackTrace(); 93 | } 94 | } 95 | 96 | @Override 97 | public void run () 98 | { 99 | Socket s = null; 100 | while (!Thread.interrupted ()) 101 | { 102 | try 103 | { 104 | s = ss.accept (); 105 | InputStream is = s.getInputStream (); 106 | PrintWriter out = new PrintWriter (s.getOutputStream (), true); 107 | String sInput = IOUtils.toString (is, net_maclife_wechat_http_BotApp.utf8); 108 | net_maclife_wechat_http_BotApp.logger.fine (GetName() + " 收到数据:\n" + net_maclife_util_ANSIEscapeTool.Green (sInput)); 109 | JsonNode jsonNode = net_maclife_wechat_http_BotApp.jacksonObjectMapper_Loose.readTree (sInput); 110 | ProcessMessage (jsonNode, out); 111 | } 112 | catch (IOException e) 113 | { 114 | e.printStackTrace(); 115 | } 116 | finally 117 | { 118 | if (s != null) 119 | { 120 | try 121 | { 122 | s.close (); 123 | } 124 | catch (IOException e) 125 | { 126 | e.printStackTrace(); 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | void ProcessMessage (JsonNode jsonMessageToRelay, PrintWriter out) 134 | { 135 | try 136 | { 137 | String sFrom = net_maclife_wechat_http_BotApp.GetJSONText (jsonMessageToRelay, "From"); 138 | String sMessageType = net_maclife_wechat_http_BotApp.GetJSONText (jsonMessageToRelay, "MessageType"); 139 | String sMessageType_LowerCase = StringUtils.lowerCase (sMessageType); 140 | String sMessage = net_maclife_wechat_http_BotApp.GetJSONText (jsonMessageToRelay, "Message"); 141 | boolean bUseAppendBotNameConfig = net_maclife_wechat_http_BotApp.GetJSONBoolean (jsonMessageToRelay, "UseAppendBotNameConfig", true); 142 | boolean bAppendBotName = net_maclife_wechat_http_BotApp.GetJSONBoolean (jsonMessageToRelay, "AppendBotName", false); 143 | if (StringUtils.isEmpty (sMessage)) 144 | { 145 | out.println ("必须指定消息内容"); 146 | return; 147 | } 148 | if (StringUtils.isNotEmpty (sFrom)) 149 | { 150 | sMessage = sMessage + "\n-- 消息来源: " + sFrom; 151 | } 152 | 153 | JsonNode jsonTOs = jsonMessageToRelay.get ("To"); 154 | List listTOs = new ArrayList (); 155 | if (jsonTOs.isArray ()) 156 | { 157 | for (int i=0; i listTOs) 201 | { 202 | String sAccount = net_maclife_wechat_http_BotApp.GetJSONText (jsonTo, "Account"); 203 | String sAlias = net_maclife_wechat_http_BotApp.GetJSONText (jsonTo, "Alias"); 204 | String sRemarkName = net_maclife_wechat_http_BotApp.GetJSONText (jsonTo, "RemarkName"); 205 | String sNickName = net_maclife_wechat_http_BotApp.GetJSONText (jsonTo, "NickName"); 206 | //String sDisplayName = net_maclife_wechat_http_BotApp.GetJSONText (jsonTo, "DisplayName"); 207 | 208 | if (StringUtils.isEmpty (sAccount)) 209 | { 210 | JsonNode jsonContact = engine.SearchForSingleContact (null, sAlias, sRemarkName, sNickName); 211 | if (jsonContact != null) 212 | listTOs.add (net_maclife_wechat_http_BotApp.GetJSONText (jsonContact, "UserName")); 213 | } 214 | else 215 | { 216 | if (StringUtils.equalsAnyIgnoreCase (sAccount, "*")) // 所有联系人 217 | { 218 | if (engine.jsonContacts == null) 219 | return; 220 | 221 | JsonNode jsonContactList = engine.jsonContacts.get ("MemberList"); 222 | for (JsonNode jsonContact : jsonContactList) 223 | listTOs.add (net_maclife_wechat_http_BotApp.GetJSONText (jsonContact, "UserName")); 224 | } 225 | else if (StringUtils.equalsAnyIgnoreCase (sAccount, "*p")) // 所有“人”(排除群聊、公众号),但其实,也可能包含类似 filehelper 之类的 226 | { 227 | if (engine.jsonContacts == null) 228 | return; 229 | 230 | JsonNode jsonContactList = engine.jsonContacts.get ("MemberList"); 231 | for (JsonNode jsonContact : jsonContactList) 232 | { 233 | String sEncryptedAccount = net_maclife_wechat_http_BotApp.GetJSONText (jsonContact, "UserName"); 234 | int nVerifyFlag = net_maclife_wechat_http_BotApp.GetJSONInt (jsonContact, "VerifyFlag"); 235 | boolean isPublicAccount = net_maclife_wechat_http_BotApp.IsPublicAccount (nVerifyFlag); 236 | boolean isRoomAccount = net_maclife_wechat_http_BotApp.IsRoomAccount (sEncryptedAccount); 237 | if (isPublicAccount || isRoomAccount) 238 | continue; 239 | 240 | listTOs.add (sEncryptedAccount); 241 | } 242 | } 243 | else if (StringUtils.equalsAnyIgnoreCase (sAccount, "*m") || StringUtils.equalsAnyIgnoreCase (sAccount, "*b") // 所有“男人/Man/Boy”(排除群聊、公众号) 244 | || StringUtils.equalsAnyIgnoreCase (sAccount, "*w") || StringUtils.equalsAnyIgnoreCase (sAccount, "*g")) // 所有“女人/Woman/Girl”(排除群聊、公众号) 245 | { 246 | if (engine.jsonContacts == null) 247 | return; 248 | 249 | JsonNode jsonContactList = engine.jsonContacts.get ("MemberList"); 250 | for (JsonNode jsonContact : jsonContactList) 251 | { 252 | String sEncryptedAccount = net_maclife_wechat_http_BotApp.GetJSONText (jsonContact, "UserName"); 253 | int nVerifyFlag = net_maclife_wechat_http_BotApp.GetJSONInt (jsonContact, "VerifyFlag"); 254 | boolean isPublicAccount = net_maclife_wechat_http_BotApp.IsPublicAccount (nVerifyFlag); 255 | boolean isRoomAccount = net_maclife_wechat_http_BotApp.IsRoomAccount (sEncryptedAccount); 256 | if (isPublicAccount || isRoomAccount) 257 | continue; 258 | 259 | int nGender = net_maclife_wechat_http_BotApp.GetJSONInt (jsonContact, "Sex"); 260 | if (nGender == 1 && (StringUtils.equalsAnyIgnoreCase (sAccount, "*m") || StringUtils.equalsAnyIgnoreCase (sAccount, "*b")) 261 | || 262 | nGender == 2 && (StringUtils.equalsAnyIgnoreCase (sAccount, "*w") || StringUtils.equalsAnyIgnoreCase (sAccount, "*g")) 263 | ) 264 | listTOs.add (sEncryptedAccount); 265 | } 266 | } 267 | else if (StringUtils.equalsAnyIgnoreCase (sAccount, "*r")) // 所有群聊 268 | { 269 | if (engine.jsonContacts == null) 270 | return; 271 | 272 | JsonNode jsonContactList = engine.jsonContacts.get ("MemberList"); 273 | for (JsonNode jsonContact : jsonContactList) 274 | { 275 | String sEncryptedAccount = net_maclife_wechat_http_BotApp.GetJSONText (jsonContact, "UserName"); 276 | boolean isRoomAccount = net_maclife_wechat_http_BotApp.IsRoomAccount (sEncryptedAccount); 277 | if (isRoomAccount) 278 | listTOs.add (sEncryptedAccount); 279 | } 280 | } 281 | else if (StringUtils.equalsAnyIgnoreCase (sAccount, "*gh")) // 所有公众号,也不知道有没有人会专门发到所有公众号里… 282 | { 283 | if (engine.jsonContacts == null) 284 | return; 285 | 286 | JsonNode jsonContactList = engine.jsonContacts.get ("MemberList"); 287 | for (JsonNode jsonContact : jsonContactList) 288 | { 289 | String sEncryptedAccount = net_maclife_wechat_http_BotApp.GetJSONText (jsonContact, "UserName"); 290 | int nVerifyFlag = net_maclife_wechat_http_BotApp.GetJSONInt (jsonContact, "VerifyFlag"); 291 | boolean isPublicAccount = net_maclife_wechat_http_BotApp.IsPublicAccount (nVerifyFlag); 292 | if (isPublicAccount) 293 | listTOs.add (sEncryptedAccount); 294 | } 295 | } 296 | else 297 | { // 非特殊形式的帐号字符串,则直接当成帐号 298 | listTOs.add (sAccount); 299 | } 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /bots/net_maclife_wechat_http_Bot_Repeater.java: -------------------------------------------------------------------------------- 1 | import java.io.*; 2 | import java.security.*; 3 | import java.security.cert.*; 4 | 5 | import org.apache.commons.lang3.*; 6 | 7 | import com.fasterxml.jackson.core.*; 8 | import com.fasterxml.jackson.databind.*; 9 | 10 | /** 11 | * 复读机机器人 12 | * @author liuyan 13 | * 14 | */ 15 | public class net_maclife_wechat_http_Bot_Repeater extends net_maclife_wechat_http_Bot 16 | { 17 | @Override 18 | public int OnTextMessageReceived 19 | ( 20 | JsonNode jsonMessage, 21 | JsonNode jsonFrom, String sFromAccount, String sFromName, boolean isFromMe, 22 | JsonNode jsonTo, String sToAccount, String sToName, boolean isToMe, 23 | JsonNode jsonReplyTo, String sReplyToAccount, String sReplyToName, boolean isReplyToRoom, 24 | JsonNode jsonReplyTo_RoomMember, String sReplyToAccount_RoomMember, String sReplyToName_RoomMember, 25 | JsonNode jsonReplyTo_Person, String sReplyToAccount_Person, String sReplyToName_Person, 26 | String sContent, boolean isContentMentionedMe, boolean isContentMentionedMeFirst 27 | ) 28 | { 29 | boolean bRepeatMyOwnMessage = net_maclife_wechat_http_BotApp.ParseBoolean (net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.repeater.repeat-my-own-message", "no"), false); 30 | if (!bRepeatMyOwnMessage && isFromMe) 31 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 32 | 33 | try 34 | { 35 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, sContent); 36 | } 37 | catch (Exception e) 38 | { 39 | e.printStackTrace (); 40 | } 41 | 42 | return 43 | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__PROCESSED 44 | | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /bots/net_maclife_wechat_http_Bot_SaveMessagePackageToDatabase.java: -------------------------------------------------------------------------------- 1 | import java.sql.*; 2 | 3 | import org.apache.commons.lang3.*; 4 | 5 | import com.fasterxml.jackson.databind.*; 6 | 7 | /** 8 | *
      9 | *
    • 记录【韬乐园快餐店】润迅订餐群里的员工点餐记录(记录所点的每一份快餐的每一个具体餐品)
    • 10 | *
    11 | *

    12 | *

    13 | *

    14 | *

    15 | * @depends 16 | * @author liuyan 17 | * 18 | */ 19 | public class net_maclife_wechat_http_Bot_SaveMessagePackageToDatabase extends net_maclife_wechat_http_Bot 20 | { 21 | @Override 22 | public int OnMessagePackageReceived (JsonNode jsonMessagePackage) 23 | { 24 | 保存消息包 (jsonMessagePackage); 25 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 26 | } 27 | 28 | void 保存消息包 (JsonNode jsonMessagePackage) 29 | { 30 | Connection conn = null; 31 | PreparedStatement stmt_保存消息包 = null; 32 | try 33 | { 34 | int nAddMsgCount = net_maclife_wechat_http_BotApp.GetJSONInt (jsonMessagePackage, "AddMsgCount", 0); 35 | int nModContactCount = net_maclife_wechat_http_BotApp.GetJSONInt (jsonMessagePackage, "ModContactCount", 0); 36 | int nDelContactCount = net_maclife_wechat_http_BotApp.GetJSONInt (jsonMessagePackage, "DelContactCount", 0); 37 | int nModChatRoomMemerCount = net_maclife_wechat_http_BotApp.GetJSONInt (jsonMessagePackage, "ModChatRoomMemberCount", 0); 38 | 39 | net_maclife_wechat_http_BotApp.SetupDataSource (); 40 | conn = net_maclife_wechat_http_BotApp.botDS.getConnection (); 41 | stmt_保存消息包 = conn.prepareStatement ("INSERT INTO wechat_message_packages (package_json, AddMsgCount, ModContactCount, DelContactCount, ModChatRoomMemberCount) VALUES (?,?,?,?,?)", new String[] {"package_id"}); 42 | int nCol = 1; 43 | stmt_保存消息包.setString (nCol++, jsonMessagePackage.toString ()); 44 | stmt_保存消息包.setInt (nCol++, nAddMsgCount); 45 | stmt_保存消息包.setInt (nCol++, nModContactCount); 46 | stmt_保存消息包.setInt (nCol++, nDelContactCount); 47 | stmt_保存消息包.setInt (nCol++, nModChatRoomMemerCount); 48 | stmt_保存消息包.executeUpdate (); 49 | stmt_保存消息包.close (); 50 | 51 | conn.close (); 52 | } 53 | catch (Exception e) 54 | { 55 | e.printStackTrace (); 56 | } 57 | finally 58 | { 59 | try 60 | { 61 | if (stmt_保存消息包 != null) 62 | stmt_保存消息包.close (); 63 | } 64 | catch (Exception e) 65 | { 66 | e.printStackTrace (); 67 | } 68 | try 69 | { 70 | if (conn != null) 71 | conn.close (); 72 | } 73 | catch (Exception e) 74 | { 75 | e.printStackTrace (); 76 | } 77 | } 78 | } 79 | 80 | 81 | public static void main (String[] args) 82 | { 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /bots/net_maclife_wechat_http_Bot_SayHi.java: -------------------------------------------------------------------------------- 1 | import java.util.*; 2 | 3 | import org.apache.commons.lang3.*; 4 | 5 | import com.fasterxml.jackson.databind.*; 6 | 7 | public class net_maclife_wechat_http_Bot_SayHi extends net_maclife_wechat_http_Bot 8 | { 9 | String sHiMessage = net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.hi.message.started"); 10 | String sByeMessage = net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.hi.message.stopped"); 11 | 12 | List listTargetAliases = null; 13 | List listTargetRemarkNames = null; 14 | List listTargetNickNames = null; 15 | 16 | public net_maclife_wechat_http_Bot_SayHi () 17 | { 18 | listTargetAliases = net_maclife_wechat_http_BotApp.GetConfig ().getList (String.class, "bot.hi.message.target.aliases"); 19 | listTargetRemarkNames = net_maclife_wechat_http_BotApp.GetConfig ().getList (String.class, "bot.hi.message.target.remark-names"); 20 | listTargetNickNames = net_maclife_wechat_http_BotApp.GetConfig ().getList (String.class, "bot.hi.message.target.nick-names"); 21 | } 22 | 23 | int ProcessMessage (String sMessage) 24 | { 25 | boolean bProcessed = false; 26 | JsonNode jsonContact = null; 27 | try 28 | { 29 | String sTemp; 30 | List list = null; 31 | 32 | list = listTargetAliases; 33 | for (int i=0; i mapProcessesRunning = new HashMap (); 26 | 27 | @Override 28 | public void Start () 29 | { 30 | if (botTask == null) 31 | { 32 | botTask = net_maclife_wechat_http_BotApp.executor.submit (this); 33 | } 34 | } 35 | 36 | @Override 37 | public void Stop () 38 | { 39 | StopAllProcesses (); 40 | super.Stop (); 41 | } 42 | 43 | @Override 44 | public int OnTextMessageReceived 45 | ( 46 | JsonNode jsonMessage, 47 | JsonNode jsonFrom, String sFromAccount, String sFromName, boolean isFromMe, 48 | JsonNode jsonTo, String sToAccount, String sToName, boolean isToMe, 49 | JsonNode jsonReplyTo, String sReplyToAccount, String sReplyToName, boolean isReplyToRoom, 50 | JsonNode jsonReplyTo_RoomMember, String sReplyToAccount_RoomMember, String sReplyToName_RoomMember, 51 | JsonNode jsonReplyTo_Person, String sReplyToAccount_Person, String sReplyToName_Person, 52 | String sContent, boolean isContentMentionedMe, boolean isContentMentionedMeFirst 53 | ) 54 | { 55 | List listCommands = net_maclife_wechat_http_BotApp.GetConfig ().getList (String.class, "bot.command-line.commands"); 56 | if (listCommands==null || listCommands.isEmpty ()) // 如果未配置命令,则不处理 57 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 58 | 59 | if (! net_maclife_wechat_http_BotApp.hasCommandPrefix (sContent)) 60 | { 61 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 62 | } 63 | sContent = net_maclife_wechat_http_BotApp.StripOutCommandPrefix (sContent); 64 | 65 | try 66 | { 67 | String[] arrayMessages = sContent.split ("\\s+", 2); 68 | if (arrayMessages==null || arrayMessages.length<1) 69 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 70 | 71 | String sCommandInputed = arrayMessages[0]; 72 | String sCommandParametersInputed = null; 73 | if (arrayMessages.length >= 2) 74 | sCommandParametersInputed = arrayMessages[1]; 75 | 76 | String[] arrayCommandOptions = sCommandInputed.split ("\\" + net_maclife_wechat_http_BotApp.COMMAND_OPTION_SEPARATOR + "+", 2); 77 | sCommandInputed = arrayCommandOptions[0]; 78 | String sCommandOptionsInputed = null; 79 | if (arrayCommandOptions.length >= 2) 80 | sCommandOptionsInputed = arrayCommandOptions[1]; 81 | 82 | for (int i=0; i [及其参数]...\n\n.行数:用来指定最多返回该命令的输出内容的行数。如 .3 最多返回前 3 行。\n.stderr: stderr 与 stdout 合并。\n.t=超时秒数: 设置进程的超时时长\n.ll 或 .lbl、.rt、.rtr、.zss、.ss、.准实时、.实时 是打开准实时输出的选项,当命令执行时间过长时,可以利用该选项“准实时”获得输出 -- 一行一个输出,这可能会比较扰人。"); 98 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 99 | } 100 | 101 | // 解析命令“选项”: .stderr .行数 .t=超时时长(秒) .rt 102 | boolean bRedirectStdErr = false; 103 | boolean bResponseLineByLine = false; 104 | int nMaxLinesReturns = DEFAULT_MAX_LINES_RETURNS; 105 | int nTimeout = net_maclife_wechat_http_BotApp.GetConfig ().getInt ("bot.command-line.process.timeout.default", DEFAULT_WATCH_DOG_TIMEOUT); 106 | if (StringUtils.isNotEmpty (sCommandOptionsInputed)) 107 | { 108 | arrayCommandOptions = sCommandOptionsInputed.split ("\\.+"); 109 | for (String sCommandOption : arrayCommandOptions) 110 | { 111 | if (StringUtils.equalsIgnoreCase (sCommandOption, "stderr")) 112 | { 113 | bRedirectStdErr = true; 114 | } 115 | else if (StringUtils.startsWithIgnoreCase (sCommandOption, "t=")) 116 | { // 如果用 t= 指定超时时长,则用指定的数值代替默认值 117 | sCommandOption = StringUtils.substring (sCommandOption, 2); 118 | try 119 | { 120 | nTimeout = Integer.parseInt (sCommandOption); 121 | if (nTimeout < 1) 122 | { 123 | nTimeout = 1; // 至少 1 秒… 124 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, GetName() + " 机器人的命令超时时长最少为 1 秒(不允许设置不超时),已自动调整为 1 秒,命令将继续执行。"); 125 | } 126 | } 127 | catch (NumberFormatException e) 128 | { 129 | e.printStackTrace (); 130 | } 131 | } 132 | else if ( 133 | StringUtils.equalsIgnoreCase (sCommandOption, "ll") // Line by Line 134 | || StringUtils.equalsIgnoreCase (sCommandOption, "lbl") // Line By Line 135 | || StringUtils.equalsIgnoreCase (sCommandOption, "rt") // RealTime 136 | || StringUtils.equalsIgnoreCase (sCommandOption, "rtr") // RealTime Response 137 | || StringUtils.equalsIgnoreCase (sCommandOption, "zss") // z准s实s时 138 | || StringUtils.equalsIgnoreCase (sCommandOption, "ss") // s实s时 139 | || StringUtils.equalsIgnoreCase (sCommandOption, "准实时") // 140 | || StringUtils.equalsIgnoreCase (sCommandOption, "实时") // 141 | ) 142 | { 143 | bResponseLineByLine = true; 144 | } 145 | else 146 | { 147 | try 148 | { 149 | nMaxLinesReturns = Integer.parseInt (sCommandOption); 150 | if (nMaxLinesReturns < 0) 151 | nMaxLinesReturns = 0; // 0: 不限制输出行数 152 | } 153 | catch (NumberFormatException e) 154 | { 155 | e.printStackTrace (); 156 | } 157 | } 158 | } 159 | } 160 | 161 | // 162 | String sShell = net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.command-line.shell"); 163 | if (StringUtils.isEmpty (sShell)) 164 | { 165 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, GetName() + " 尚未配置 Shell,无法执行。"); 166 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 167 | } 168 | try 169 | { 170 | String sCommandOutput = ProcessShellCommand (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, sShell, sCommandParametersInputed, bRedirectStdErr, bResponseLineByLine, nMaxLinesReturns, nTimeout); 171 | if (StringUtils.isNotEmpty (sCommandOutput)) 172 | { 173 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, sCommandOutput); 174 | } 175 | } 176 | catch (Throwable e) 177 | { 178 | e.printStackTrace (); 179 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, e.toString ()); 180 | } 181 | } 182 | } 183 | } 184 | catch (Exception e) 185 | { 186 | e.printStackTrace (); 187 | } 188 | 189 | return 190 | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__PROCESSED 191 | | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 192 | } 193 | 194 | /** 195 | * 执行 Shell 命令。 196 | * 这里的实现方法与我的 IRC 机器人 TideBot 中的实现方式不同:这里不解析命令行,直接交给一个 Shell 处理 197 | * @param sShell 198 | * @param sCommandLineString 199 | * @param bRedirectStdErr 是否将 stderr 重定向到 stdout 200 | * @param bResponseLineByLine 是否每行都单独响应。这个选项,相当于“是否准实时输出”,对于执行类似 ping 命令这样不断定时输出行的命令,好处:可以使用这个选项让人直观的感到是否正在输出。 弊端:因为每行都产生一个微信消息,会影响到群里其他人手机 -- 不断提醒。 201 | * @param nMaxLinesReturns 最多返回多少行 202 | * @param nTimeout 进程运行超时时长。如果超过这个时长进程还未结束,则杀死该进程。 203 | */ 204 | String ProcessShellCommand (String sReplyToAccount, String sReplyToName, String sReplyToAccount_RoomMember, String sReplyToName_RoomMember, String sShell, String sCommandLineString, boolean bRedirectStdErr, boolean bResponseLineByLine, int nMaxLinesReturns, int nTimeout) throws KeyManagementException, UnrecoverableKeyException, JsonProcessingException, NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException, InterruptedException 205 | { 206 | //List listCommandLineParameters = new ArrayList<> 207 | ProcessBuilder pb = new ProcessBuilder (sShell, "-c", sCommandLineString); 208 | // pb.inheritIO (); // 这个不能要,因为不是打印在后台,而是要取出结果,并返回 209 | if (bRedirectStdErr) 210 | pb.redirectErrorStream (); 211 | 212 | Process p = null; 213 | int nLines = 0; 214 | int nTotalLines = 0; 215 | String sLine = null; 216 | StringBuilder sb = bResponseLineByLine ? null : new StringBuilder (); 217 | try 218 | { 219 | p = pb.start (); 220 | mapProcessesRunning.put (p, System.currentTimeMillis () + nTimeout * 1000); 221 | int rc = p.waitFor (); 222 | mapProcessesRunning.remove (p); 223 | assert (rc == 0); 224 | if (rc != 0) 225 | { 226 | net_maclife_wechat_http_BotApp.logger.severe ("'" + sCommandLineString + "' 执行失败(返回代码不是 0,而是 " + rc + ")。"); 227 | } 228 | 229 | InputStream in = p.getInputStream (); 230 | InputStream err = p.getErrorStream (); 231 | //IOUtils.toString (in); 232 | BufferedReader br = null; 233 | br = new BufferedReader (new InputStreamReader (in)); 234 | while ((sLine = br.readLine ()) != null) 235 | { 236 | //System.out.println (sLine); 237 | nTotalLines ++; 238 | if (nMaxLinesReturns != 0 && nLines >= nMaxLinesReturns) 239 | continue; 240 | //if (nMaxLinesReturns == 0 || (nMaxLinesReturns != 0 && nLines < nMaxLinesReturns)) 241 | if (bResponseLineByLine) 242 | { 243 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, sLine); 244 | } 245 | else 246 | { 247 | sb.append (sLine); 248 | sb.append ('\n'); 249 | } 250 | nLines ++; 251 | } 252 | //br.close (); 253 | if (! bRedirectStdErr) 254 | { 255 | br = new BufferedReader (new InputStreamReader (err)); 256 | while ((sLine = br.readLine ()) != null) 257 | { 258 | System.err.println (sLine); 259 | // nTotalLines ++; 260 | // if (nMaxLinesReturns != 0 && nLines >= nMaxLinesReturns) 261 | // continue; 262 | // //if (nMaxLinesReturns == 0 || (nMaxLinesReturns != 0 && nLines < nMaxLinesReturns)) 263 | // if (bResponseLineByLine) 264 | // { 265 | // SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, sLine); 266 | // } 267 | // else 268 | // { 269 | // sb.append (sLine); 270 | // sb.append ('\n'); 271 | // } 272 | // nLines ++; 273 | } 274 | //br.close (); 275 | } 276 | 277 | if (nMaxLinesReturns != 0 && nLines < nTotalLines) 278 | { 279 | sb.append ('\n'); 280 | sb.append ("(总共 "); 281 | sb.append (nTotalLines); 282 | sb.append (" 行,只显示前 "); 283 | sb.append (nLines); 284 | sb.append (" 行)"); 285 | } 286 | 287 | //err.close (); 288 | //in.close (); 289 | 290 | } 291 | catch (IOException e) 292 | { // 进程有可能超时被杀死,也应该返回被杀死前的内容 293 | e.printStackTrace(); 294 | } 295 | 296 | return sb==null ? null : sb.toString (); 297 | } 298 | 299 | public void StopAllProcesses () 300 | { 301 | for (Process p : mapProcessesRunning.keySet ()) 302 | { 303 | p.destroy (); 304 | } 305 | mapProcessesRunning.clear (); 306 | } 307 | 308 | /** 309 | * 监视线程:杀死超时的进程。类似 apache commaons exec 里的 WatchDog。 310 | * 目前,监视的频率为:每隔 1 秒检查一次。 311 | */ 312 | @Override 313 | public void run () 314 | { 315 | while (! Thread.currentThread ().interrupted ()) 316 | { 317 | try 318 | { 319 | TimeUnit.SECONDS.sleep (1); 320 | for (Process p : mapProcessesRunning.keySet ()) 321 | { 322 | long lExpectedStopTime = mapProcessesRunning.get (p); 323 | if (System.currentTimeMillis () > lExpectedStopTime) 324 | { 325 | p.destroy (); 326 | mapProcessesRunning.remove (p); 327 | } 328 | } 329 | } 330 | catch (InterruptedException e) 331 | { 332 | e.printStackTrace (); 333 | break; 334 | } 335 | catch (Throwable e) 336 | { 337 | e.printStackTrace (); 338 | } 339 | } 340 | } 341 | 342 | public static void main (String[] args) 343 | { 344 | net_maclife_wechat_http_Bot_ShellCommand cmd = new net_maclife_wechat_http_Bot_ShellCommand (); 345 | cmd.Start (); 346 | 347 | final int 运行秒数 = 7; 348 | final int 延时退出秒数 = 7; // 得比运行秒数数值大 349 | final String sShell = "bash"; 350 | String 命令行 = null; 351 | 命令行 = "ping 127.0.0.1"; 352 | //命令行 = "htop"; 353 | //命令行 = "nmap -T4 127.0.0.1"; 354 | boolean b准实时输出 = false; 355 | try 356 | { 357 | System.err.println ("运行 " + 运行秒数 + " 秒钟的 " + 命令行); 358 | String sResult = cmd.ProcessShellCommand (null, null, null, null, sShell, 命令行, true, b准实时输出, 0, 运行秒数); 359 | System.out.println (命令行 + " 输出:\n" + sResult); 360 | //System.err.println ("等待 " + 延时退出秒数 + " 秒钟退出"); 361 | // TimeUnit.SECONDS.sleep (延时退出秒数); 362 | } 363 | catch (Exception e) 364 | { 365 | e.printStackTrace(); 366 | } 367 | 368 | cmd.Stop (); 369 | net_maclife_wechat_http_BotApp.executor.shutdownNow (); 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /bots/net_maclife_wechat_http_Bot_SimpleAddressBook.java: -------------------------------------------------------------------------------- 1 | import java.sql.*; 2 | import java.util.*; 3 | 4 | import org.apache.commons.lang3.*; 5 | 6 | import com.fasterxml.jackson.databind.*; 7 | 8 | /** 9 | * 简易微信群通讯录。 10 | * 简易通讯录的数据存放在数据库的单张表中。简易通讯录提供简单的通讯录的 查、修改功能。 11 | * 12 | * 查: 13 | * 通讯录 姓名 -- 查指定姓名的通讯录 14 | * 通讯录 -- 查自己的通讯录、同时提供帮助信息? 15 | * 修改: 16 | * 17 | * 增加: 暂时不提供增加的功能,考虑的原因是: 18 | * @author liuyan 19 | * 20 | */ 21 | public class net_maclife_wechat_http_Bot_SimpleAddressBook extends net_maclife_wechat_http_Bot 22 | { 23 | 24 | @Override 25 | public int OnTextMessageReceived 26 | ( 27 | JsonNode jsonMessage, 28 | JsonNode jsonFrom, String sFromAccount, String sFromName, boolean isFromMe, 29 | JsonNode jsonTo, String sToAccount, String sToName, boolean isToMe, 30 | JsonNode jsonReplyTo, String sReplyToAccount, String sReplyToName, boolean isReplyToRoom, 31 | JsonNode jsonReplyTo_RoomMember, String sReplyToAccount_RoomMember, String sReplyToName_RoomMember, 32 | JsonNode jsonReplyTo_Person, String sReplyToAccount_Person, String sReplyToName_Person, 33 | String sContent, boolean isContentMentionedMe, boolean isContentMentionedMeFirst 34 | ) 35 | { 36 | List listCommands = net_maclife_wechat_http_BotApp.GetConfig ().getList (String.class, "bot.simple-address-book.commands"); 37 | if (listCommands==null || listCommands.isEmpty ()) // 如果未配置命令,则不处理 38 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 39 | 40 | if (! net_maclife_wechat_http_BotApp.hasCommandPrefix (sContent)) 41 | { 42 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 43 | } 44 | sContent = net_maclife_wechat_http_BotApp.StripOutCommandPrefix (sContent); 45 | 46 | try 47 | { 48 | String[] arrayMessages = sContent.split ("\\s+", 2); 49 | if (arrayMessages==null || arrayMessages.length<1) 50 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 51 | 52 | String sCommandInputed = arrayMessages[0]; 53 | String sCommandParametersInputed = null; 54 | if (arrayMessages.length >= 2) 55 | sCommandParametersInputed = arrayMessages[1]; 56 | 57 | // 命令行命令格式没问题,现在开始查询数据库 58 | for (int i=0; i"); 71 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 72 | } 73 | 74 | try 75 | { 76 | String sResult = Query (sReplyToName, sCommandParametersInputed); 77 | if (StringUtils.isEmpty (sResult)) 78 | { 79 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, "在通讯簿【" + sReplyToName + "】(与群名相同)中没找到姓名为【" + sCommandParametersInputed + "】的联系信息"); 80 | return net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 81 | } 82 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, sResult); 83 | break; 84 | } 85 | catch (Exception e) 86 | { 87 | SendTextMessage (sReplyToAccount, sReplyToName, sReplyToAccount_RoomMember, sReplyToName_RoomMember, "查询出错: " + e); 88 | } 89 | } 90 | } 91 | } 92 | catch (Exception e) 93 | { 94 | e.printStackTrace (); 95 | } 96 | 97 | return 98 | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__PROCESSED 99 | | net_maclife_wechat_http_BotEngine.BOT_CHAIN_PROCESS_MODE_MASK__CONTINUE; 100 | } 101 | 102 | String Query (String sFromName, String sQuery) throws SQLException 103 | { 104 | String sTablePrefix = StringUtils.trimToEmpty (net_maclife_wechat_http_BotApp.GetConfig ().getString ("bot.simple-address-book.jdbc.database-table.prefix")); 105 | net_maclife_wechat_http_BotApp.SetupDataSource (); 106 | Connection conn = null; 107 | PreparedStatement stmt = null; 108 | ResultSet rs = null; 109 | StringBuilder sb = null; 110 | try 111 | { 112 | conn = net_maclife_wechat_http_BotApp.botDS.getConnection (); 113 | stmt = conn.prepareStatement ("SELECT * FROM " + sTablePrefix + "wechat_simple_address_book WHERE 微信群昵称=? AND 群联系人姓名=?"); 114 | int nCol = 1; 115 | stmt.setString (nCol++, sFromName); 116 | stmt.setString (nCol++, sQuery); 117 | rs = stmt.executeQuery (); 118 | sb = new StringBuilder (); 119 | while (rs.next ()) 120 | { 121 | sb.append (rs.getString ("群联系人姓名")); 122 | sb.append ("\n"); 123 | sb.append ("--------------------\n"); 124 | sb.append ("电话: "); 125 | sb.append (rs.getString ("电话号码")); 126 | sb.append ("\n"); 127 | sb.append ("单位: "); 128 | sb.append (rs.getString ("工作单位")); 129 | sb.append ("\n"); 130 | sb.append ("住址: "); 131 | sb.append (rs.getString ("住址")); 132 | sb.append ("\n"); 133 | sb.append ("昵称: "); 134 | sb.append (rs.getString ("群联系人昵称")); 135 | sb.append ("\n"); 136 | break; 137 | } 138 | } 139 | catch (SQLException e) 140 | { 141 | e.printStackTrace(); 142 | throw e; 143 | } 144 | finally 145 | { 146 | try 147 | { 148 | if (rs != null) 149 | rs.close (); 150 | if (stmt != null) 151 | stmt.close (); 152 | if (conn != null) 153 | conn.close (); 154 | } 155 | catch (SQLException e) 156 | { 157 | e.printStackTrace(); 158 | } 159 | } 160 | return sb==null ? null : sb.toString (); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /bots/net_maclife_wechat_http_Bot_糗事百科热门.java: -------------------------------------------------------------------------------- 1 | import java.io.*; 2 | import java.util.*; 3 | 4 | import org.apache.commons.lang3.*; 5 | import org.jsoup.*; 6 | import org.jsoup.nodes.*; 7 | import org.jsoup.select.*; 8 | 9 | /** 10 | * 获取糗事百科热门信息。目前,不打算用 Bot 形式来处理糗事百科,而仅仅当做 Relay Bot 的一个外部消息源。 11 | * @author liuyan 12 | * @deprecated 除了个别网站(比如需要先获取一个网页、取得 Cookie 或者某数值,再用该数值访问第二个网页),其他所有抓取网页的 Bot,都建议改为 net_maclife_wechat_http_Bot_WebSoup 的模板来处理。 13 | * 14 | */ 15 | @Deprecated 16 | public class net_maclife_wechat_http_Bot_糗事百科热门 extends net_maclife_wechat_http_Bot 17 | { 18 | /** 19 | * Map 中的 key 名称: 20 | * - mode 模式,取值: 21 | * '=' 等于(不区分大小写); 22 | * 'regexp' 规则表达式 23 | * - expression : 表达式; 当 mode 为 '=' 时,这里就应该给出具体发帖人姓名; 当 mode 为 'regexp' 时,这里应该给出规则表达式 24 | * - reason : 加入黑名单的原因 25 | */ 26 | static List> listBlackList = new ArrayList>(); 27 | 28 | public static String 获取糗事百科热门 (int nMax) throws IOException 29 | { 30 | String sURL = "http://www.qiushibaike.com/8hr/page/"; // http://www.qiushibaike.com/8hr/page/${p=1} 31 | Document doc = null; 32 | //org.jsoup.Connection jsoup_conn = null; 33 | doc = Jsoup.connect (sURL) 34 | .userAgent ("IE") 35 | .timeout (net_maclife_util_HTTPUtils.DEFAULT_READ_TIMEOUT_SECOND * 1000) 36 | .get (); 37 | 38 | StringBuilder sb = new StringBuilder (); 39 | Elements 糗事列表 = doc.select ("div[id^=qiushi_tag_]"); 40 | if (糗事列表.size () == 0) 41 | System.exit (5); // 5 - 无内容 42 | 43 | int nNotBanned = 0; 44 | for (int i=0; nNotBanned span"); 49 | Elements 差评量列表 = e.select ("a[id^=dn-] > span"); 50 | Elements 发帖人列表 = e.select ("div.author h2"); 51 | Element 热评 = e.select ("div.cmtMain").first (); 52 | String s发帖人 = 发帖人列表.text (); 53 | String sBlackListReason = GetBanReason (s发帖人); 54 | if (sBlackListReason != null) 55 | { 56 | System.err.println ("【" + s发帖人 + "】 为黑名单用户,原因: " + sBlackListReason); 57 | continue; 58 | } 59 | 60 | nNotBanned ++; 61 | sb.append (i+1); 62 | sb.append (" "); 63 | 64 | sb.append (e.select ("li.comments a").first ().absUrl ("href")); 65 | sb.append ("\n"); 66 | sb.append (e.select ("div.content").text()); 67 | 68 | Elements 图片列表 = e.select ("div.thumb img"); 69 | if (图片列表.size () > 0) 70 | { 71 | sb.append ("\n"); 72 | String sImageURL = 图片列表.attr ("src"); 73 | if (StringUtils.startsWithIgnoreCase (sImageURL, "http:")) 74 | sb.append (sImageURL); 75 | else 76 | sb.append ("http:" + sImageURL); 77 | } 78 | 79 | // 视频目前已取不到,需要通过单独的 http 请求获取 json 数据获取 80 | //Elements 视频列表 = e.select ("div.thumb img"); 81 | //if (视频列表.size () > 0) 82 | //{ 83 | // sb.append ("\n"); 84 | // sb.append (视频列表.attr ("src")); 85 | //} 86 | 87 | int i好笑量 = 0; 88 | int i不好笑量 = 0; 89 | double 好笑率 = 0; 90 | try 91 | { 92 | i好笑量 = Integer.parseInt (StringUtils.trimToEmpty (好评量列表.text ())); 93 | i不好笑量 = Integer.parseInt (StringUtils.trimToEmpty (差评量列表.text ())); 94 | 好笑率 = ((double)i好笑量) / (i好笑量 + Math.abs (i不好笑量)); 95 | } 96 | catch (Exception ex) 97 | { 98 | ex.printStackTrace (); 99 | } 100 | sb.append ("\n"); 101 | //sb.append ("[呲牙]"); 102 | sb.append ("😁"); 103 | sb.append ("+"); 104 | sb.append (好评量列表.text ()); 105 | sb.append (" ("); 106 | sb.append ((int)(好笑率 * 100)); 107 | sb.append ("%)"); 108 | sb.append (" "); 109 | //sb.append (""); // 虽然收到的表情的文字是这样的,貌似相同的文字发出去就不行 110 | //sb.append ("[撇嘴]"); 111 | sb.append ("😞"); 112 | sb.append (差评量列表.text ()); 113 | sb.append (" 作者: "); 114 | sb.append (发帖人列表.text ()); 115 | if (热评 != null) 116 | { 117 | sb.append ("\n\n"); 118 | //sb.append ("热评:"); 119 | sb.append (" 💬 "); // 💬💭 120 | sb.append (热评.text ()); 121 | //sb.append (StringUtils.remove (热评.text (), '\u0001')); // 剔除掉 0x01 字符,否则 jackson 报错: com.fasterxml.jackson.core.JsonParseException: Illegal unquoted character ((CTRL-CHAR, code 1)): has to be escaped using backslash to be included in string value 。貌似表情代码 (如 '[doge]') 的前面就是 0x01 字符 122 | } 123 | sb.append ("\n\n"); 124 | } 125 | 126 | return sb.toString (); 127 | } 128 | 129 | public static void ReadBanListFromFile_ByName (String sBlackListFileName) 130 | { 131 | File f = new File (sBlackListFileName); 132 | if (! f.exists ()) 133 | return; 134 | 135 | try 136 | { 137 | BufferedReader br = new BufferedReader (new FileReader (f)); 138 | int n = 0; 139 | while (true) 140 | { 141 | String sLine = br.readLine (); 142 | n++; 143 | if (sLine == null) 144 | break; 145 | 146 | if (StringUtils.startsWithAny (sLine, "#", "//")) // 备注行,不做处理 147 | continue; 148 | 149 | String[] arrayColumns = sLine.split (" ", 3); // 分隔符 150 | if (arrayColumns==null || arrayColumns.length < 2) 151 | { 152 | System.err.println ("第 " + n + " 行不存在,或者列数小于 2"); 153 | continue; 154 | } 155 | 156 | String sMode = arrayColumns[0]; 157 | String sExpression = arrayColumns[1]; 158 | String sReason = ""; 159 | if (arrayColumns.length > 2) 160 | { 161 | sReason = arrayColumns[2]; 162 | } 163 | 164 | // 检查数据有效性 165 | if (StringUtils.equalsAnyIgnoreCase (sMode, "=", "%", " %", "%%", "*", "regexp")) 166 | { 167 | Map mapBlack = new HashMap (); 168 | mapBlack.put ("mode", sMode); 169 | mapBlack.put ("expression", sExpression); 170 | mapBlack.put ("reason", sReason); 171 | listBlackList.add (mapBlack); 172 | } 173 | else 174 | { 175 | System.err.println ("第 " + n + " 行第一列是非法的“模式”:" + sMode); 176 | continue; 177 | } 178 | } 179 | br.close (); 180 | } 181 | catch (Exception e) 182 | { 183 | e.printStackTrace(); 184 | } 185 | } 186 | 187 | public static void ReadBanListFromFile_GroupByReason (String sBlackListFileName) 188 | { 189 | File f = new File (sBlackListFileName); 190 | if (! f.exists ()) 191 | return; 192 | 193 | try 194 | { 195 | BufferedReader br = new BufferedReader (new FileReader (f)); 196 | int n = 0; 197 | while (true) 198 | { 199 | String sLine = br.readLine (); 200 | n++; 201 | if (sLine == null) 202 | break; 203 | 204 | sLine = StringUtils.trimToEmpty (sLine); 205 | if (StringUtils.isEmpty (sLine)) // 空行、空白字符行,不做处理 206 | continue; 207 | 208 | if (StringUtils.startsWithAny (sLine, "#", "//")) // 备注行,不做处理 209 | continue; 210 | 211 | String[] arrayColumns = sLine.split (" ", 3); // 分隔符 212 | if (arrayColumns==null || arrayColumns.length < 2) 213 | { 214 | System.err.println ("第 " + n + " 行不存在,或者列数小于 2"); 215 | continue; 216 | } 217 | 218 | String sMode = arrayColumns[0]; 219 | String sExpression = arrayColumns[1]; 220 | String sReason = ""; 221 | if (arrayColumns.length > 2) 222 | { 223 | sReason = arrayColumns[2]; 224 | } 225 | 226 | // 检查数据有效性 227 | if (StringUtils.equalsAnyIgnoreCase (sMode, "=", "%", " %", "%%", "*", "regexp")) 228 | { 229 | Map mapBlack = new HashMap (); 230 | mapBlack.put ("mode", sMode); 231 | mapBlack.put ("expression", sExpression); 232 | mapBlack.put ("reason", sReason); 233 | listBlackList.add (mapBlack); 234 | } 235 | else 236 | { 237 | System.err.println ("第 " + n + " 行第一列是非法的“模式”:" + sMode); 238 | continue; 239 | } 240 | } 241 | br.close (); 242 | } 243 | catch (Exception e) 244 | { 245 | e.printStackTrace(); 246 | } 247 | } 248 | 249 | /** 250 | * 251 | * @param sOP_OriginalPoster 252 | * @return 253 | * null - 没在黑名单内; 254 | * 非 null (含空字符串) - 加入黑名单的原因 255 | */ 256 | public static String GetBanReason (String sOP_OriginalPoster) 257 | { 258 | for (Map mapBan : listBlackList) 259 | { 260 | String sMode = mapBan.get ("mode"); 261 | String sExpression = mapBan.get ("expression"); 262 | String sReason = mapBan.get ("reason"); 263 | if (sReason==null) 264 | sReason = ""; 265 | 266 | if ( 267 | (StringUtils.equalsIgnoreCase (sMode, "=") && StringUtils.equalsIgnoreCase (sExpression, sOP_OriginalPoster)) 268 | || (StringUtils.equalsIgnoreCase (sMode, " %") && StringUtils.startsWithIgnoreCase (sOP_OriginalPoster, sExpression)) 269 | || ((StringUtils.equalsIgnoreCase (sMode, "%") || StringUtils.equalsIgnoreCase (sMode, "% ")) && StringUtils.endsWithIgnoreCase (sOP_OriginalPoster, sExpression)) 270 | || ((StringUtils.equalsIgnoreCase (sMode, "%%") || StringUtils.equalsIgnoreCase (sMode, "*")) && StringUtils.containsIgnoreCase (sOP_OriginalPoster, sExpression)) 271 | || (StringUtils.equalsIgnoreCase (sMode, "regexp") && sOP_OriginalPoster.matches (sExpression)) 272 | ) 273 | { 274 | return sReason; 275 | } 276 | } 277 | return null; 278 | } 279 | 280 | public static void main (String[] args) throws IOException 281 | { 282 | ReadBanListFromFile_ByName ("糗事百科黑名单.txt"); 283 | int n = 3; 284 | if (args.length >= 1) 285 | { 286 | try 287 | { 288 | n = Integer.parseInt (args[0]); 289 | } 290 | catch (Exception e) 291 | { 292 | e.printStackTrace (); 293 | } 294 | } 295 | System.out.println (获取糗事百科热门 (n)); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /doc/ReadMe.Chinglish.md: -------------------------------------------------------------------------------- 1 |
    中文 | Chinglish
    2 | 3 | ---- 4 | 5 | *Warning: It could caused your [wechat account forbidden to login web edition anymore if you use WeChatBotEngine](https://github.com/moontide/WeChatBotEngine/issues/12), hence you'll unable to run WeChatBotEngine properly anymore!* 6 | 7 | # About # 8 | 9 | WeChatBotEngine is a bot engine/framework based on HTTP protocol of WeChat Web Edition. 10 | WeChatBotEngine deals the communication with WeChat server itself, developers can develop bot based on that, so developers can build new bots to expand the power of WeChatBotEngine。 11 | 12 | ![text QR code](https://github.com/moontide/WeChatBotEngine/raw/master/doc/img/text-QR-code.png) 13 | 14 | ## Official bots shipped with the engine ## 15 | WeChatBotEngine project ships with several official bots, to demonstrate how to build a bot or you can just to run it. 16 | Those bots are: 17 | 18 | ### Bots shipped with WeChatBotEngine ### 19 | Legend: 20 | * 📌 - Recommeded 21 | * ✳ - Useful 22 | 23 | #### Common Bots #### 24 |
    25 |
    📌 WebSoup: Fetch HTML/XML/JSON from web, extract information from it.
    26 |
    HTML/XML: Use CSS Selector to extract information from fetched HTML/XML resource, send it to WeChat friend; JSON: Use JavaScript to extract data from JSON data, send it to WeChat friend. 27 |
    28 | 29 |
    30 |
    📌 Relay: A bot which relay messages from outside to a WeChat friend in your contacts (Incomig only, no outgoing)
    31 |
    32 | Receive message in JSON format from outside via Socket (TCP/IP), and send it to a friend (person or room) in your WeChat contacts according the target given in the message. Other applications can send message to WeChat via this relay bot, hence expanded the power of WeChatBotEngine, such as: 33 |
    34 |
      35 |
    • When download task of Transmission completed, a script will run (configurable in Transmission),the script send message to WeChat via this relay bot, hence Download-Completed-Notification function is implemented. 36 |
      37 | 38 |
    • 39 |
    • Using crontab to implement a Hourly-Time-Announcement function. (Of course you can write a bot to do it)
    • 40 |
    • Fetch information from a web page periodically, and send it to WeChat, to implement Scheduled-Information/Message-Push function. 41 |
      42 | 43 |
    • 44 |
    • Using crontab to 'sign in' to some public accounts to get membership points. 45 |
      46 | 47 |
    • 48 |
    • You must have good ideas, please write it here ...
    • 49 |
    • ...
    • 50 |
    51 |
    52 |
    📌 ShellCommand: A bot execute shell command
    53 |
    54 | Accept command line from text message, run it, return the result of command. Example: /cmd ls -l /bin/ 55 |
    56 | 57 |
    58 |
    📌 ActiveDirectoryAddressBook: Use Active Directory (or Samba 4.x in Linux) as address book for company
    59 |
    Address book using Active Directory as backend: According room name or nickname of friend, search the keyword (name or account or email address or telephone number) from the Active Directories bounded to this name for contact information. Example: Send /adab KEYWORD in My Company 1 room will search for the contact information which contains KEYWORD from the active directories bounded to My Company 1 name. 60 |
    61 | 62 |
    63 |
    SimpleAddressBook: A simple address book bot for WeChat chat room
    64 |
    65 | This is a simple address book bot which relied on MySQL database for WeChat chat room. It will query the database according the room nickname and return the contact information of the specified contact name. Example: send /sab Alice in chat room My Company 1 will return contact information of Alice of My Company 1 address book. 66 |
    67 | 68 |
    69 |
    MakeFriend: A MakeFriend agent bot
    70 |
    In chat room, use /addme command to send a request of MakeFriend to the command issuer. 71 |
    72 | 73 |
    74 | When a request of MakeFriend message is received, automatically accept the request according a keyword/password. 75 |
    76 |
    Manager: A remote manager bot
    77 |
    This is just a reimplementation of some console commands, to let you manage bot engine if you are not in front of your computer. Currently, the following commands are implemented: /LoadBot, /UnloadBot, /ListBots (everyone can use this command), /LogLevel (View or Set log level), /Topic (Change room name like change topic in IRC channel), /Invite (Invite a contact to a chat room), /Kick (Kick a member from chat room, Kick operation works only if you're the administrator of that room)。 78 |
    79 | 80 |
    81 |
    BaiduImageSearch: Baidu image search
    82 |
    83 | When an image message posted, send the image to baidu image search, return the guess information and possible image sources. 84 |
    85 | 86 |
    87 |
    BaiduVoice: Baidu Automatic Speech Recognition (ASR)
    88 |
    89 | When an audio or short video message is posted, send the audio to baidu ASR api, and return the text result. -- Because you can only use text keywords to search chat history, so it's very useful. 90 |
    91 | 92 |
    93 |
    BaiduTranslate: Baidu translate
    94 |
    95 | Provide translation service via Baidu translate API. 96 |
    97 | 98 |
    99 |
    GoogleImageSearch: Google Image Search
    100 |
    101 | When an image message posted, send the image to Google image search, return the guess information and possible image sources. 102 |
    103 |
    Emoji: Emoji bot
    104 |
    105 | Get emoji characters from database according keywords. Due to different OS implemented their own support of different Unicode standard version, some emoji characters may not be displayed well. 106 |
    107 | 108 |
    109 |
    MissileLaunched: Send a 'missile launched' message when someone shared a geographic position (just for fun)
    110 |
    111 | When someone send a geographic position message, read the longtitude and latitude value, and append a random customable joke. 112 |
    113 | 114 |
    115 |
    Repeater: A bot which repeat sender's message
    116 |
    117 | Repeat the message sent by sender. This bot is only for demonstration or test purpose, running for long term is not recommended. 118 |
    119 |
    SayHi: A bot which say Hi and Goodbye
    120 |
    121 | Send a Hi and Goodbye message when logged in or logged out. The Hi message and Goodbye message is configurable. 122 |
    123 |
    124 | 125 | #### Industrial Bots #### 126 |
    127 |
    HCICloudCSR: HCICloud customer service representative chat bot
    128 |
    129 | Using the HTTP protocol from HCICloud CSR, fetch the answer from CSR bot, then reply to user. 130 |
    131 | 132 |
    133 |
    iConTek: iConTek NLP (Natual Language Processing) customer service chat bot
    134 |
    135 | Using the HTTP protocol of iConTek, fetch the answer from iConTek NLP engine, then reply to user. 136 |
    137 | 138 |
    139 |
    140 | 141 | 142 | ### load/unload bots dynamically ### 143 | /listbots 144 | 2016-12-16 17:43:27.714 [信息] net_maclife_wechat_http_BotEngine ListBots: 简易通讯录 (net_maclife_wechat_http_Bot_SimpleAddressBook) 145 | 2016-12-16 17:43:27.715 [信息] net_maclife_wechat_http_BotEngine ListBots: 百度翻译 (net_maclife_wechat_http_Bot_BaiduTranslate) 146 | 2016-12-16 17:43:27.716 [信息] net_maclife_wechat_http_BotEngine ListBots: Google 图片搜索 (net_maclife_wechat_http_Bot_GoogleImageSearch) 147 | 2016-12-16 17:43:27.717 [信息] net_maclife_wechat_http_BotEngine ListBots: 消息中继 (net_maclife_wechat_http_Bot_Relay) 148 | 149 | /unloadbot net_maclife_wechat_http_Bot_GoogleImageSearch 150 | 2016-12-16 17:43:41.517 [信息] net_maclife_wechat_http_BotEngine UnloadBot: Google 图片搜索 机器人已被卸载 151 | 152 | /loadbot net_maclife_wechat_http_Bot_BaiduImageSearch 153 | 2016-12-16 17:44:03.795 [信息] net_maclife_wechat_http_BotEngine LoadBot: 百度识图 机器人已创建并加载 154 | 155 | # How to create your own bot # 156 | Very easy, just extends/inherit `net_maclife_wechat_http_Bot` class, implements any (or zero) interface you wantted. 157 | Currently, the following interfaces/events are planned: 158 | 159 | - `OnLoggedIn` 160 | - `OnLoggedOut` 161 | - `OnShutdown` 162 | - `OnMessagePackageReceived` Triggered when a message package received. This is the entrance of all the following `On***MessageReceived` event. If you want to analyze or modify the message by yourself, you can do it here. 163 | - `OnTextMessageReceived` Triggered when a Text message received. 164 | - `OnImageMessageReceived` Triggered when an Image message received. 165 | - `OnVoiceMessageReceived` Triggered when a Voice message received. 166 | - `OnVideoMessageReceived` Triggered when a Video message received. 167 | - `OnEmotionMessageReceived` Triggered when an Emotion message received. 168 | - `OnSystemMessageReceived` Triggered when a System message received. 169 | - `OnMessageIsRevokedMessageReceived` Triggered when a MessageIsRevoked message received. 170 | - `OnRequestToMakeFriendMessageReceived` Triggered when a RequestToMakeFriend message received. 171 | - `OnContactChanged` Triggered when a contact was changed. 172 | - `OnContactDeleted` Triggered when a contact was deleted. 173 | - `OnRoomMemberChanged` Triggered when a room member changed. 174 | 175 | # Why your class name named like this: `net_maclife_wechat_http_Bot` # 176 | I don't like the layout of how Java organize the sources files and classes files -- directories in directories, so I replace all `.` (dot) in canonical/full class names to `_` (underline). This will avoid the Java style layout, it's simple & effective, you don't need to go deep in directories to get a file. 177 | -------------------------------------------------------------------------------- /doc/ReadMe.中文.md: -------------------------------------------------------------------------------- 1 |
    中文 | Chinglish
    2 | 3 | ---- 4 | 5 | *注意: 使用本机器人引擎,可能会导致你的[微信帐号无法再登录 Web 版(Web 版被封)](https://github.com/moontide/WeChatBotEngine/issues/12),也就无法再使用本机器人引擎登录!* 6 | 7 | # 关于 # 8 | 9 | WeChatBotEngine 是一个基于微信 Web 版通信协议的机器人引擎/机器人框架。 10 | WeChatBotEngine 自身处理了与微信后台的通信,开发者只需要在此基础上开发自己的 Bot,即可打造、扩展 WeChatBotEngine 的机器人功能。 11 | 12 | ![文字二维码截图](https://github.com/moontide/WeChatBotEngine/raw/master/doc/img/text-QR-code.png) 13 | 14 | ## WeChatBotEngine 自带的几个机器人 ## 15 | WeChatBotEngine 自带了几个机器人,一些出于演示的目的,一些出于给开发者以参考的目的。这些机器人有: 16 | 17 | ### 自带的机器人列表 ### 18 | 图例: 19 | * 📌 - 推荐 20 | * ✳ - 有点用处 21 | 22 | #### 公用机器人 #### 23 |
    24 |
    📌 WebSoup: 【抓网页(HTML/XML/JSON)】机器人
    25 |
    抓取 HTML/XML:根据 CSS 选择器从下载下来的 HTML/XML 中提取数据并发送到微信;
    26 | 抓取 JSON:执行 JavaScript 将 JSON 中的数据取出,并发送到微信。
    27 | WebSoup 机器人的自定义模板功能,更可以让 WebSoup 更方便用户使用。 28 |
    29 | 30 |
    31 |
    📌 Relay: 【消息中继】机器人 (仅转入,不转出)
    32 |
    从 Socket 接收 JSON 格式的消息,根据消息中指定的接收人转发到微信群/微信号中。其他程序通过这种方式来把消息转发到微信。 33 | 利用这一点,可以把强大的外部应用程序的部分功能通过消息中继机器人将功能扩展,比如 34 |
    35 |
      36 |
    • Transmission 下载任务完成后,执行脚本(需在 Transmission 中配置),脚本通过消息中继发送到微信,以达到通知的目的。 37 |
      38 | 39 |
    • 40 |
    • 利用 crontab,实现一个简单的整点报时的功能。(当然你也可以单独写一个 Bot 来实现)
    • 41 |
    • 利用 crontab,定时获取一个网页的信息,将内容发到微信,达到定期推送消息的目的。 42 |
      43 | 44 |
    • 45 |
    • 利用 crontab,向一些“签到获得积分”的公众号定时“签到” 46 |
      47 | 48 |
    • 49 |
    • 你一定还有其他好的点子,可在此处补充…
    • 50 |
    • 51 |
    52 |
    53 |
    📌 ShellCommand: 【系统命令 / 命令行】机器人
    54 |
    从文本消息中接收命令输入,执行命令,返回命令输出结果。用法举例: /cmd ls -l /bin/ 55 |
    56 | 57 |
    58 |
    📌 ActiveDirectoryAddressBook: 【公司通讯录/域通讯录】机器人
    59 |
    基于活动目录 (Active Directory) 的微信通讯录: 根据微信群名、好友昵称,查询与该名称关联的通讯录下的某个关键字(姓名、域帐号、邮箱、电话号码)的联系信息。用法举例: 在 我的公司1 群中发送 /gstxl 关键字 将会查询与 我的公司1 所关联的几个通讯簿中 姓名或电话号码或邮箱地址为 关键字 的联系信息。 60 |
    61 | 62 |
    63 |
    SimpleAddressBook: 微信群【简易通讯录】机器人
    64 |
    基于 MySQL 数据库的微信群简易通讯录:根据微信群名称查询该群名下的指定的某个名称的联系信息。用法举例: 在 我的公司1 群中发送 /txl 张三 将会查询 我的公司1 通讯录中 张三 的联系信息。 65 |
    66 | 67 |
    68 |
    MakeFriend: 【加好友】机器人
    69 |
    在群聊中,用 /addme 命令向命令执行者发送加好友的请求。 70 |
    71 | 72 |
    73 | 当收到别人发来的请求加好友消息时,根据设定的“暗号”,自动通过好友请求。 74 |
    75 |
    Manager: 【远程管理】机器人
    76 |
    将控制台里的一些管理命令用远程管理机器人再次实现,以达到不在电脑跟前时,对引擎进行管理的目的。目前只实现了 加载机器人(/LoadBot)、卸载机器人(/UnloadBot)、列出机器人(/ListBots, 所有人都可执行该命令)、查看设置日志级别(/LogLevel)、改群名(/Topic)、邀请人加入群聊(/Invite)、将群成员踢出(/Kick,注意: Kick 操作仅当你是群主时才会执行成功)。 77 |
    78 | 79 |
    80 |
    BaiduImageSearch: 【百度图片搜索(百度识图)】机器人
    81 |
    当有人发了图片时,提交给百度图片搜索,给出百度图片搜索的结果、可能的图片来源。 82 |
    83 | 84 |
    85 |
    BaiduVoice: 【百度语音识别】机器人
    86 |
    当有人发了音频、视频时,将音频、视频中的音频提交给百度语音识别(语音转文字),给出识别后的文字结果。 -- 因为在搜索聊天记录只能用文字来搜索,所以,非常实用。 87 |
    88 | 89 |
    90 |
    BaiduTranslate: 【百度翻译】机器人
    91 |
    利用百度翻译接口,提供翻译功能。 92 |
    93 | 94 |
    95 |
    GoogleImageSearch: 【Google 图片搜索】机器人
    96 |
    当有人发了图片时,提交给 Google 图片搜索,给出 Google 图片搜索的结果、可能的图片来源。 97 |
    98 | 99 |
    100 |
    Emoji: 【Emoji 表情字符】机器人
    101 |
    根据关键字,从数据库中查询对应的 emoji 字符。因为不同系统对 Unicode 支持的不同,个别 emoji 字符可能无法正常显示。 102 |
    103 | 104 |
    105 |
    MissileLaunched: 用来【恶搞地理位置信息】的机器人
    106 |
    当有人发了地理位置信息时,把经纬度报出来,然后加上一句随机可设置的“导弹准备就绪”的恶搞话语…… = = 107 |
    108 | 109 |
    110 |
    Repeater: 【复读机】机器人
    111 |
    重复消息发送者的消息。该机器人仅用于演示、测试用途,正式环境不建议使用。
    112 |
    SayHi: 问候/再见机器人
    113 |
    主要用于机器人上线、下线时发出通知。问候语、再见语可配置。
    114 |
    115 | 116 | #### 工业/商业机器人 #### 117 |
    118 |
    HCICloudCSR: 捷通华声【灵云智能客服 (CSR)】对话机器人
    119 |
    利用灵云智能客服提供的 http 接口,从智能客服机器人获取一条答案,回复给用户。 120 |
    121 | 122 |
    123 |
    iConTek: 【iConTek 智能客服】机器人
    124 |
    利用 iConTek 提供的 http 接口,从 iConTek 机器人引擎获取一条答案,回复给用户。 125 |
    126 | 127 |
    128 |
    129 | 130 | ### Bot 的动态加载、卸载 ### 131 | /listbots 132 | 2016-12-16 17:43:27.714 [信息] net_maclife_wechat_http_BotEngine ListBots: 简易通讯录 (net_maclife_wechat_http_Bot_SimpleAddressBook) 133 | 2016-12-16 17:43:27.715 [信息] net_maclife_wechat_http_BotEngine ListBots: 百度翻译 (net_maclife_wechat_http_Bot_BaiduTranslate) 134 | 2016-12-16 17:43:27.716 [信息] net_maclife_wechat_http_BotEngine ListBots: Google 图片搜索 (net_maclife_wechat_http_Bot_GoogleImageSearch) 135 | 2016-12-16 17:43:27.717 [信息] net_maclife_wechat_http_BotEngine ListBots: 消息中继 (net_maclife_wechat_http_Bot_Relay) 136 | 137 | /unloadbot net_maclife_wechat_http_Bot_GoogleImageSearch 138 | 2016-12-16 17:43:41.517 [信息] net_maclife_wechat_http_BotEngine UnloadBot: Google 图片搜索 机器人已被卸载 139 | 140 | /loadbot net_maclife_wechat_http_Bot_BaiduImageSearch 141 | 2016-12-16 17:44:03.795 [信息] net_maclife_wechat_http_BotEngine LoadBot: 百度识图 机器人已创建并加载 142 | 143 | 144 | # 怎样开发自己的 Bot # 145 | 灰常简单:继承 `net_maclife_wechat_http_Bot`,实现该类的任意一个你想要实现的 (0 个也可以 -- 啥都不做的机器人) 接口。 146 | 目前已规划的接口有: 147 | 148 | - `OnLoggedIn` 149 | - `OnLoggedOut` 150 | - `OnShutdown` 151 | - `OnMessagePackageReceived` 当收到【消息包】时触发。该接口是下面几个 `On***MessageReceived` 接口的总入口,如果你要自己解析、甚至修改收到的微信消息包,可以在此接口入手。 152 | - `OnTextMessageReceived` 当收到【文本消息】时触发。 153 | - `OnImageMessageReceived` 当收到【图片消息】时触发。 154 | - `OnVoiceMessageReceived` 当收到【语音消息】时触发。 155 | - `OnVideoMessageReceived` 当收到【视频消息】时触发。 156 | - `OnEmotionMessageReceived` 当收到【表情图消息】时触发。 157 | - `OnSystemMessageReceived` 当收到【系统消息】(比如“收到红包,请到手机上查看”)时触发。 158 | - `OnMessageIsRevokedMessageReceived` 当收到【消息被撤回】消息时触发。 159 | - `OnRequestToMakeFriendMessageReceived` 当收到【别人请求加好友】消息时触发 160 | - `OnContactChanged` 当【联系人变更】时触发。 161 | - `OnContactDeleted` 当【联系人被删除】时触发。 162 | - `OnRoomMemberChanged` 当【群成员变更】时触发。 163 | 164 | # 为什么你的类名是 `net_maclife_wechat_http_Bot` 这样子 # 165 | 不喜欢 java 默认的一层套一层文件夹的组织方式,所以,把类的全名中的小数点 `.` 替换成 `_`,这样可以避免多层文件夹的组织方式 -- 简单、高效,找个文件不用来回切换文件夹。 166 | -------------------------------------------------------------------------------- /doc/img/bot-active-directory-address-book-50%.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-active-directory-address-book-50%.png -------------------------------------------------------------------------------- /doc/img/bot-baidu-image-search-50%.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-baidu-image-search-50%.png -------------------------------------------------------------------------------- /doc/img/bot-baidu-translate-50%.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-baidu-translate-50%.png -------------------------------------------------------------------------------- /doc/img/bot-baidu-translate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-baidu-translate.png -------------------------------------------------------------------------------- /doc/img/bot-baidu-voice-50%.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-baidu-voice-50%.png -------------------------------------------------------------------------------- /doc/img/bot-baidu-voice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-baidu-voice.png -------------------------------------------------------------------------------- /doc/img/bot-emoji-50%.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-emoji-50%.png -------------------------------------------------------------------------------- /doc/img/bot-emoji.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-emoji.png -------------------------------------------------------------------------------- /doc/img/bot-iConTek-50%.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-iConTek-50%.png -------------------------------------------------------------------------------- /doc/img/bot-make-friend-addme-50%.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-make-friend-addme-50%.png -------------------------------------------------------------------------------- /doc/img/bot-make-friend-addme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-make-friend-addme.png -------------------------------------------------------------------------------- /doc/img/bot-manager-50%.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-manager-50%.png -------------------------------------------------------------------------------- /doc/img/bot-manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-manager.png -------------------------------------------------------------------------------- /doc/img/bot-missile-launched-50%.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-missile-launched-50%.png -------------------------------------------------------------------------------- /doc/img/bot-missile-launched.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-missile-launched.png -------------------------------------------------------------------------------- /doc/img/bot-relay.message-push-qiushibaike.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-relay.message-push-qiushibaike.png -------------------------------------------------------------------------------- /doc/img/bot-relay.notify-transmission-download-complete-50%.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-relay.notify-transmission-download-complete-50%.png -------------------------------------------------------------------------------- /doc/img/bot-relay.scheduled-sign-50%.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-relay.scheduled-sign-50%.png -------------------------------------------------------------------------------- /doc/img/bot-shell-command-50%.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-shell-command-50%.png -------------------------------------------------------------------------------- /doc/img/bot-shell-command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-shell-command.png -------------------------------------------------------------------------------- /doc/img/bot-simple-address-book-50%.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/bot-simple-address-book-50%.png -------------------------------------------------------------------------------- /doc/img/text-QR-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moontide/WeChatBotEngine/afe338c7d550750c2023bac5fb566baa453cbf4b/doc/img/text-QR-code.png -------------------------------------------------------------------------------- /logging.properties: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # LogManager 3 | # 参见 java.util.logging.LogManager 的 javadoc 4 | ################################################################################ 5 | # The global logging properties may include: 6 | # 7 | # A property "handlers". This defines a whitespace or comma separated list of class names for handler classes to load and register as handlers on the root Logger (the Logger named ""). Each class name must be for a Handler class which has a default constructor. Note that these Handlers may be created lazily, when they are first used. 8 | handlers = java.util.logging.ConsoleHandler java.util.logging.FileHandler 9 | 10 | ## A property ".handlers". This defines a whitespace or comma separated list of class names for handlers classes to load and register as handlers to the specified logger. Each class name must be for a Handler class which has a default constructor. Note that these Handlers may be created lazily, when they are first used. 11 | # 就不单独针对类日志器设置了,采用 root Logger 的设置 12 | # net_maclife_wechat_http_BotApp.handlers = java.util.logging.ConsoleHandler java.util.logging.FileHandler 13 | 14 | 15 | # All properties whose names end with ".level" are assumed to define log levels for Loggers. Thus "foo.level" defines a log level for the logger called "foo" and (recursively) for any of its children in the naming hierarchy. Log Levels are applied in the order they are defined in the properties file. Thus level settings for child nodes in the tree should come after settings for their parents. 16 | # The property name ".level" can be used to set the level for the root of the tree. 17 | .level = INFO 18 | 19 | # A property "config". This property is intended to allow arbitrary configuration code to be run. The property defines a whitespace or comma separated list of class names. A new instance will be created for each named class. The default constructor of each class may execute arbitrary code to update the logging configuration, such as setting logger levels, adding handlers, adding filters, etc. 20 | 21 | 22 | 23 | # yyyy-MM-dd HH:mm:ss.SSS [$LEVEL] $SOURCE: $message\n 24 | java.util.logging.SimpleFormatter.format = %1$tF %1$tT.%1$tL [%4$s] %2$s: %5$s%n 25 | 26 | 27 | 28 | 29 | 30 | ################################################################################ 31 | # 日志输出到控制台/终端 32 | # java.util.logging.ConsoleHandler 33 | ################################################################################ 34 | 35 | # .level specifies the default level for the Handler (defaults to Level.INFO). 36 | # 垃圾 java.util.logging 非要把 Handler 和 Logger 的 level 分开处理,tm 有病么 37 | java.util.logging.ConsoleHandler.level = ALL 38 | 39 | ## .filter specifies the name of a Filter class to use (defaults to no Filter). 40 | # java.util.logging.ConsoleHandler.filter = 41 | 42 | ## .formatter specifies the name of a Formatter class to use (defaults to java.util.logging.SimpleFormatter). 43 | #java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter 44 | 45 | ## .encoding the name of the character set encoding to use (defaults to the default platform encoding). 46 | #java.util.logging.ConsoleHandler.encoding = UTF-8 47 | 48 | 49 | 50 | 51 | 52 | ################################################################################ 53 | # 日志写文件 54 | # java.util.logging.FileHandler 55 | ################################################################################ 56 | 57 | ## .level specifies the default level for the Handler (defaults to Level.ALL). 58 | ## 垃圾 java.util.logging 非要把 Handler 和 Logger 的 level 分开处理,tm 有病么 59 | #java.util.logging.FileHandler.level = ALL 60 | 61 | ## .filter specifies the name of a Filter class to use (defaults to no Filter). 62 | # java.util.logging.FileHandler.filter = 63 | 64 | # .formatter specifies the name of a Formatter class to use (defaults to java.util.logging.XMLFormatter). 65 | # XML, fuckoff !!! 66 | java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter 67 | 68 | # .encoding the name of the character set encoding to use (defaults to the default platform encoding). 69 | # 日志写文件就不用平台默认编码了,统一为 UTF-8 70 | java.util.logging.FileHandler.encoding = UTF-8 71 | 72 | # .limit specifies an approximate maximum amount to write (in bytes) to any one file. If this is zero, then there is no limit. (Defaults to no limit). 73 | # 10485760 = 10M = 10 * 1024 * 1024 74 | # 20971520 = 20M = 20 * 1024 * 1024 75 | #java.util.logging.FileHandler.limit = 20971520 76 | 77 | ## .count specifies how many output files to cycle through (defaults to 1). 78 | #java.util.logging.FileHandler.count = 79 | 80 | # 无法配置为日志文件不循环的数值,只好配置为 10000000,但 10000000 会导致在启动时 java 进程 CPU 占用率 100% !!! Fuck java.util.logging.FileHandler 81 | #java.util.logging.FileHandler.count = 10000000 82 | 83 | # .pattern specifies a pattern for generating the output file name. See below for details. (Defaults to "%h/java%u.log"). 84 | java.util.logging.FileHandler.pattern = logs/wechat-bot-engine-%g.log 85 | 86 | # .append specifies whether the FileHandler should append onto any existing files (defaults to false). 87 | # 默认不附加日志 -- 覆盖日志,这样导致没法记录历史日志,改改改 88 | java.util.logging.FileHandler.append = true 89 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | 2 | dir=$(dirname "$0") 3 | #cd "$dir" 4 | 5 | for jar in "$dir/lib/"*.jar 6 | do 7 | cp="${cp}${jar}:" 8 | done 9 | 10 | cp="${cp}${dir}/bin" 11 | echo "classpath: ${cp}" 12 | 13 | java -Djsse.enableSNIExtension=false -Djava.util.logging.config.file=logging.properties -cp "${cp}" net_maclife_wechat_http_BotApp 14 | -------------------------------------------------------------------------------- /sql/emoji.mysql.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE emoji 2 | ( 3 | sn INT UNSIGNED NOT NULL DEFAULT 0, 4 | code VARCHAR(45) CHARACTER SET ascii NOT NULL DEFAULT '', 5 | emoji_char VARCHAR(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT 'emoji 字符。有的 emoji 是由两个甚至五个字符组成的,所以,长度不能设置为 1。而且很多 emoji 的 utf8 编码都是 4 字节的,所以此字段的字符集编码也要用 4 字节的 utf8: utf8mb4。还要注意: COLLATE 必须用 utf8mb4_bin,原因,默认的 COLLATION 不支持增补集字符,参见 http://mysqlserverteam.com/sushi-beer-an-introduction-of-utf8-support-in-mysql-8-0/ https://bugs.mysql.com/bug.php?id=76553', 6 | 浏览器显示图 TEXT CHARACTER SET ascii NOT NULL, 7 | Apple显示图 TEXT CHARACTER SET ascii NOT NULL, 8 | Google显示图 TEXT CHARACTER SET ascii NOT NULL, 9 | Twitter显示图 TEXT CHARACTER SET ascii NOT NULL, 10 | EmojiOne显示图 TEXT CHARACTER SET ascii NOT NULL, 11 | Facebook显示图 TEXT CHARACTER SET ascii NOT NULL, 12 | FacebookMessenger显示图 TEXT CHARACTER SET ascii NOT NULL, 13 | Samsung显示图 TEXT CHARACTER SET ascii NOT NULL, 14 | Windows显示图 TEXT CHARACTER SET ascii NOT NULL, 15 | GMail显示图 TEXT CHARACTER SET ascii NOT NULL, 16 | SoftBank显示图 TEXT CHARACTER SET ascii NOT NULL, 17 | DCM显示图 TEXT CHARACTER SET ascii NOT NULL, 18 | KDDI显示图 TEXT CHARACTER SET ascii NOT NULL, 19 | 英文名称 VARCHAR(100) NOT NULL DEFAULT '', 20 | 日期 VARCHAR(20) NOT NULL DEFAULT '', 21 | tag_name VARCHAR(80) NOT NULL DEFAULT '', 22 | is_unique_tag TINYINT(1) NOT NULL DEFAULT 0 COMMENT '此标签是不是能够唯一能取到单个 emoji 字符的。', 23 | 24 | PRIMARY KEY PK__emoji (emoji_char) 25 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='emoji 字符表。'; 26 | -------------------------------------------------------------------------------- /sql/save-messages.mysql.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE wechat_message_packages 2 | ( 3 | package_id INT UNSIGNED AUTO_INCREMENT, 4 | package_json MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE ut8mb4_bin NOT NULL DEFAULT '' COMMENT 'HTTP 接口收到的原始消息包', 5 | 6 | AddMsgCount INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '新消息数量 AddMsgCount', 7 | ModContactCount INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '联系人变更数量 ModContactCount', 8 | DelContactCount INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '联系人删除数量', 9 | ModChatRoomMemberCount INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '群成员变更数量', 10 | 11 | PRIMARY KEY PK__package_id (package_id) 12 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='接受到消息包。这是包含了文字'; 13 | 14 | /* 15 | CREATE TABLE wechat_messages 16 | ( 17 | package_id INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '此 ID 为本表自己产生的 ID,非消息包', 18 | message_id, 19 | id_wechat VARCHAR(50) NOT NULL DEFAULT '' COMMENT '这是消息中自带的 ID,非自增长类型,也不确定是否唯一', 20 | type INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '消息类型: 1=文本消息, 51=, 10000=系统消息', 21 | content TEXT NOT NULL DEFAULT '' COMMENT '注意:此消息内容已经过处理:', 22 | from_account, 23 | from_name NOT NULL DEFAULT '' COMMENT '注意,消息本身不包含此信息,这个名称只是根据 account 从内部缓存的通讯录中获取到的', 24 | to_account, 25 | to_name NOT NULL DEFAULT '' COMMENT '注意,消息本身不包含此信息,这个名称只是根据 account 从内部缓存的通讯录中获取到的', 26 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='接受到文本消息包。'; 27 | */ 28 | -------------------------------------------------------------------------------- /sql/simple-address-book.mysql.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE wechat_simple_address_book 2 | ( 3 | 微信群昵称 VARCHAR(100) NOT NULL DEFAULT '' COMMENT '通讯录根据群昵称,来决定群聊时,只能查哪个群的通讯录', 4 | 群联系人姓名 VARCHAR(50) NOT NULL DEFAULT '', 5 | 电话号码 VARCHAR(100) NOT NULL DEFAULT '', 6 | 工作单位 VARCHAR(100) NOT NULL DEFAULT '', 7 | 住址 VARCHAR(100) NOT NULL DEFAULT '', 8 | 9 | /* 下面这些仅仅对应微信群内的昵称 */ 10 | 群联系人昵称 VARCHAR(50) NOT NULL DEFAULT '', 11 | 群联系人群昵称 VARCHAR(50) DEFAULT '' COMMENT '就是“在微信群中的昵称”/“显示名”', 12 | 群联系人微信号 VARCHAR(50) DEFAULT '', 13 | 14 | PRIMARY KEY PK__wechat_simple_address_book (微信群昵称, 群联系人姓名) 15 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='非常简单的微信群通讯录,用于 WeChatBotEngine 的简易通讯录机器人小程序'; 16 | -------------------------------------------------------------------------------- /sql/wechat-contacts.mysql.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE wechat_sessions 2 | ( 3 | session_id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增长 ID', 4 | SessionID VARCHAR(32) CHARACTER SET ascii NOT NULL DEFAULT '' COMMENT '微信返回的会话 ID', 5 | MyAccountInThisSession VARCHAR(70) CHARACTER SET ascii NOT NULL DEFAULT '' COMMENT '“我”在此会话的加密帐号', 6 | SessionCreatedTime DATETIME COMMENT '会话创建时间 / 登录微信 Web 版的时间', 7 | 8 | PRIMARY KEY (session_id), 9 | UNIQUE KEY (SessionID), 10 | UNIQUE KEY (MyAccountInThisSession) 11 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='微信 Web 会话。每次会话(每次登录)都会产生一个不同的加密帐号,同时所有联系人的帐号、群成员的帐号也会改变,但在同一个会话内联系人的帐号和群成员的帐号不变。'; 12 | 13 | CREATE TABLE wechat_contacts 14 | ( 15 | contact_id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增长 ID', 16 | SessionID VARCHAR(32) CHARACTER SET ascii NOT NULL DEFAULT '' COMMENT '微信返回的会话 ID', 17 | MyAccountInThisSession VARCHAR(70) CHARACTER SET ascii NOT NULL DEFAULT '' COMMENT '“我”在此会话的帐号', 18 | ContactAccountInThisSession VARCHAR(70) CHARACTER SET ascii NOT NULL DEFAULT '' COMMENT '此联系人在此会话的帐号', 19 | 明文ID VARCHAR(100) NOT NULL DEFAULT '' COMMENT '微信明文帐号,不是那种每次会话都会变化的且经过加密后的帐号。这个明文帐号 是需要手工在手机端逐个打开联系人的聊天窗口后才会获取到(这种方法已失效,微信已不返回该数据,现在改为从“消息被撤回”消息中获取该数据),所有,如果从未打开过某人的聊天窗口,这个信息可能会为空', 20 | 微信号 VARCHAR(50) NOT NULL DEFAULT '' COMMENT '微信号。现在这个数据已经获取不到', 21 | 昵称 VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '昵称', 22 | 备注名 VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '备注名', 23 | 签名 VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '签名', 24 | 25 | /* 26 | 电话号码 VARCHAR(100) NOT NULL DEFAULT '' COMMENT '电话号码', 27 | 描述 VARCHAR(100) NOT NULL DEFAULT '' COMMENT '描述/更多备注', 28 | 标签 VARCHAR(100) NOT NULL DEFAULT '' COMMENT '标签', 29 | 照片或名片 VARCHAR(100) NOT NULL DEFAULT '' COMMENT '照片或名片', 30 | */ 31 | 32 | 性别 TINYINT(1) NOT NULL DEFAULT 0 COMMENT '0-未设置; 1-男; 2-女', 33 | 省 VARCHAR(100) NOT NULL DEFAULT '' COMMENT '省', 34 | 市 VARCHAR(100) NOT NULL DEFAULT '' COMMENT '市', 35 | 36 | 是否星标好友 TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否星标好友', 37 | 是否已删除 TINYINT(1) NOT NULL DEFAULT 0 COMMENT '微信联系人中是否已删除,删除后,这里也许只是做个标志位更改,而不真正删除', 38 | 删除时间 DATETIME, 39 | 40 | 41 | 是否群 TINYINT(1) NOT NULL DEFAULT 0 COMMENT '群的明文 ID 是以 @chatroom 结尾的', 42 | 群主UIN BIGINT NOT NULL DEFAULT 0 COMMENT '群主的 UIN ,类似 QQ 号的信息。这个信息,呃,简直比明文 ID 还透明,这个信息只是在微信初始化接口的返回结果里看到过(仅仅最近联系的群信息中才有数据,其他联系人数据都是 0) 没有了,最近联系人里的数据也是 0 了', 43 | 群主昵称 VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '群主昵称', 44 | 群成员数量 SMALLINT UNSIGNED NOT NULL DEFAULT 0, 45 | 是否公众号 TINYINT(1) NOT NULL DEFAULT 0 COMMENT '公众号的明文 ID 是以 gh_ 开头的', 46 | 是否企业号 TINYINT(1) NOT NULL DEFAULT 0, 47 | 是否微信团队号 TINYINT(1) NOT NULL DEFAULT 0, 48 | 49 | 数据来源 VARCHAR(10) NOT NULL DEFAULT '' COMMENT '数据从改哪里来的,可选取值:微信初始化(最近联系人)、微信通讯录。这个来源,基本对应引擎触发的某个事件', 50 | 最后更新时间 DATETIME, 51 | 52 | PRIMARY KEY (contact_id), 53 | UNIQUE KEY UQ__昵称_备注名 (昵称, 备注名), 54 | /*UNIQUE KEY UQ__昵称_备注名 (MyAccountInThisSession, 昵称, 备注名), 注意事项: */ 55 | INDEX IX__wechat_contacts_昵称 (昵称), 56 | INDEX IX__wechat_contacts_备注名 (备注名) 57 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='同步个人微信的通讯录。除了位于加入到微信通讯录的联系人,还包括未加入到通讯录的群'; 58 | 59 | CREATE TABLE wechat_contact_members 60 | ( 61 | contact_id INT UNSIGNED NOT NULL DEFAULT 0, 62 | 序号 SMALLINT UNSIGNED NOT NULL DEFAULT 0, 63 | 64 | 成员昵称 VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '成员昵称', 65 | 成员群昵称 VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '成员的群昵称 - DisplayName', 66 | 67 | PRIMARY KEY (contact_id, 序号) 68 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='群聊里的成员'; 69 | 70 | CREATE VIEW v_wechat_contact_members 71 | AS 72 | SELECT 73 | c.昵称 AS 群名, 74 | m.* 75 | FROM 76 | wechat_contact_members m 77 | LEFT JOIN wechat_contacts c ON m.contact_id = c.contact_id 78 | ; 79 | -------------------------------------------------------------------------------- /src/net_maclife_util_BaiduCloud.java: -------------------------------------------------------------------------------- 1 | import java.io.*; 2 | import java.security.*; 3 | import java.security.cert.*; 4 | 5 | import com.fasterxml.jackson.databind.*; 6 | 7 | /** 8 | 百度访问令牌 (AccessToken) 返回的数据格式 9 |
    10 | {
    11 |     "access_token":"***...***",
    12 |     "session_key":"***...***",
    13 |     "scope":"public audio_voice_assistant_get audio_tts_post wise_adapt lebo_resource_base lightservice_public hetu_basic lightcms_map_poi kaidian_kaidian",	// 类似这样
    14 |     "refresh_token":"***...***",
    15 |     "session_secret":"***...***",
    16 |     "expires_in":NNNNNNN
    17 | }
    18 | 
    19 | 20 | * @author liuyan 21 | * 22 | */ 23 | public class net_maclife_util_BaiduCloud 24 | { 25 | public static final String BAIDU_OAUTH_ACCESS_TOKEN_URL = "https://openapi.baidu.com/oauth/2.0/token"; 26 | 27 | public static String GetBaiduAccessToken (String sAppKey, String sAppPassword, String sAccessTokenCacheFile) 28 | { 29 | File f = new File (sAccessTokenCacheFile); 30 | if (f.exists ()) 31 | { 32 | long nFileModifiedTime_Millisecond = f.lastModified (); 33 | try 34 | { 35 | JsonNode jsonAccessToken = net_maclife_wechat_http_BotApp.jacksonObjectMapper_Loose.readTree (f); 36 | int nExpireDuration_Seconds = net_maclife_wechat_http_BotApp.GetJSONInt (jsonAccessToken, "expires_in"); 37 | long now = System.currentTimeMillis (); 38 | if (now <= (nFileModifiedTime_Millisecond + nExpireDuration_Seconds*1000)) 39 | return net_maclife_wechat_http_BotApp.GetJSONText (jsonAccessToken, "access_token"); 40 | } 41 | catch (IOException e) 42 | { 43 | e.printStackTrace(); 44 | } 45 | } 46 | 47 | // 已过期,或者从未获取过 48 | return GetNewBaiduAccessToken (sAppKey, sAppPassword, sAccessTokenCacheFile); 49 | } 50 | 51 | public static String GetNewBaiduAccessToken (String sAppKey, String sAppPassword, String sAccessTokenCacheFile) 52 | { 53 | // 使用 Client Credentials 方式获得百度访问令牌 54 | // 参见: http://developer.baidu.com/wiki/index.php?title=docs/oauth/client 55 | String sPostData = "grant_type=client_credentials&client_id=" + sAppKey + "&client_secret=" + sAppPassword; 56 | try 57 | { 58 | String sResponseBodyContent = net_maclife_util_HTTPUtils.CURL_Post (net_maclife_util_BaiduCloud.BAIDU_OAUTH_ACCESS_TOKEN_URL, sPostData.getBytes ()); 59 | FileWriter fw = new FileWriter (sAccessTokenCacheFile); 60 | fw.write (sResponseBodyContent); 61 | fw.close (); 62 | 63 | JsonNode jsonAccessToken = net_maclife_wechat_http_BotApp.jacksonObjectMapper_Loose.readTree (sResponseBodyContent); 64 | return net_maclife_wechat_http_BotApp.GetJSONText (jsonAccessToken, "access_token"); 65 | } 66 | catch (KeyManagementException | UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException | CertificateException | IOException e) 67 | { 68 | e.printStackTrace(); 69 | } 70 | return null; 71 | } 72 | } 73 | --------------------------------------------------------------------------------