├── .gitignore ├── README.MD ├── commandLineInferface.js ├── httpsWrapper.js ├── package.json └── wechat.js /.gitignore: -------------------------------------------------------------------------------- 1 | test.js 2 | npm-debug.log 3 | /node_modules 4 | .*.swp 5 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # WeChat Node Client 2 | 3 | ## Description: 4 | 5 | This is a [wechat](https://wx.qq.com/) client for node. It authenticates and acts 6 | as though the user is using the official wechat [web interface](http://web.wechat.com). 7 | 8 | Note: Development is continuing. 9 | 10 | ## Installation: 11 | 12 | ``` 13 | npm install --save wechat-webclient 14 | ``` 15 | 16 | ## API Flow: 17 | 18 | 1. Get uuid from WeChat service. 19 | 2. Get QR code from WeChat service using uuid. 20 | 3. Display QR code to user, so that they can scan it using their WeChat phone app 21 | 4. User scans QR code, and confirms they'd like to login. 22 | 5. Get the login data (cookies, postdata) from the WeChat service so it knows who we are. 23 | 6. Get contact list, contact's icons 24 | 7. Recieve messages as they come in, send messages and logout at will. 25 | 26 | ## Usage: 27 | 28 | ```javascript 29 | var wechat = require('wechat-webclient'); 30 | var client = new wechat.weChatClient(); 31 | 32 | client.getUUID() // Gets uuid, passes to next function 33 | .then(client.checkForScan.bind(client), errorhandlingfunction) // resolves when QR scan confirmed with the login url 34 | .then(client.webwxnewloginpage.bind(client), errorhandlingfunction) // gets passed login url, resolves with login data 35 | .then(client.webwxinit.bind(client), errorhandlingfunction) // initializes data for the user, (e.g. who this user is, auth data) 36 | .then(function(whatever) { // whatever will be a JSON object containing login data. 37 | return new Promise(function (resolve, reject) { 38 | client.webwxgetcontact.bind(client, whatever); 39 | // client.contacts is now populated. 40 | // Do something with contacts 41 | // eventually, resolve with what I've here called "whatever", because it is 42 | // required for subsequent functions, or save it as a variable in a wide enough scope 43 | // so that it can be utilized. 44 | }); 45 | }, errorhandlingfunction) 46 | .then(function(passedLoginData) { 47 | //start some type of user interface here: 48 | userInterfaceFunction(); 49 | return client.synccheck(passedLoginData); // passes back (actually nothing) stuff when it can no longer synccheck (e.g. logged out) 50 | }.bind(client), errorhandlingfunction); 51 | .then(handleProgramShutDownFunction, errorhandlingfunction); 52 | ``` 53 | -------------------------------------------------------------------------------- /commandLineInferface.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | var wechat = require("./wechat.js"); 5 | var weChatClient = new wechat.weChatClient(); 6 | 7 | weChatClient.getUUID() 8 | .then(weChatClient.checkForScan.bind(weChatClient), weChatClient.handleError) 9 | .then(weChatClient.webwxnewloginpage.bind(weChatClient), weChatClient.handleError) 10 | .then(weChatClient.webwxinit.bind(weChatClient), weChatClient.handleError) 11 | .then(weChatClient.webwxgetcontact.bind(weChatClient), weChatClient.handleError) 12 | .then(function(stuff) { 13 | userInterface(stuff); 14 | return weChatClient.synccheck(stuff); 15 | }.bind(weChatClient), weChatClient.handleError) 16 | .then(function (something) { 17 | weChatClient.log(-1, "No longer syncchecking", something); 18 | }, weChatClient.handleError); 19 | 20 | function saveIcon(iconURLPath) { 21 | var begin = iconURLPath.indexOf("username=") + "username=".length; 22 | var end = iconURLPath.indexOf("&skey="); 23 | var iconPath = iconURLPath.substring(begin, end); 24 | fs.writeFile("/tmp/wxicon_" + iconPath, result, "binary", function (e) { 25 | if (e) this.handleError; 26 | else { 27 | for (var j = 0; j < max; j++) { 28 | if (!completed[j]) { 29 | completed[j] = true; 30 | this.log(0, "Icon " + (j + 1) + " of " + max + " successfully written"); 31 | j = max; 32 | } 33 | } 34 | } 35 | }.bind(this)); 36 | } 37 | 38 | // FIXME FIXME FIXME 39 | 40 | 41 | function saveQR(imgQR) { 42 | var pathQR = "/tmp/wxQR_" + uuid; 43 | this.log(1, "Writing QR to file at " + pathQR); 44 | fs.writeFile(pathQR, imgQR, "binary", function (e) { 45 | if (e) this.handleError; 46 | else { 47 | this.log(0, "QR successfully written"); 48 | fs.readFile(pathQR, function (err, imgQR) { 49 | if (err) this.handleError; 50 | else { 51 | this.QRserver = serve.createServer(function (req, res) { 52 | res.writeHead(200, { "content-type": "image/jpeg" }); 53 | this.log(3, "200 GET: QR code requested from local server"); 54 | res.end(imgQR); 55 | }.bind(this)).listen(8000); 56 | this.log(1, "QR code can be scanned at http://localhost:8000"); 57 | } 58 | }.bind(this)); 59 | } 60 | }.bind(this)); 61 | }.bind(this); 62 | 63 | 64 | function userInterface(loginData) { 65 | weChatClient.log(3, "Welcome to WeChat: CLI edition! Now listening to user input"); 66 | process.stdin.resume(); 67 | process.stdin.setEncoding("utf8"); 68 | var oLoop; 69 | var iLoop; 70 | var wStep; 71 | var wNope; 72 | var wStay; 73 | var rStep; 74 | var message; 75 | toOuterLoop(); 76 | 77 | process.stdin.on("data", function(input) { 78 | input = input.trim(); 79 | if (oLoop) { 80 | if (input === "q") { 81 | process.stdin.pause(); 82 | weChatClient.log(3, "No longer listening to user input"); 83 | weChatClient.webwxlogout(loginData); 84 | weChatClient.log(1, "Logging out"); 85 | return; 86 | } else if (input === "s") { 87 | oLoop = false; 88 | } else { 89 | toOuterLoop(); 90 | } 91 | } 92 | if (!oLoop) { 93 | if (!iLoop) { 94 | if (input === "b") { 95 | toOuterLoop(); 96 | } else if (parseInt(input)) { 97 | var contactNum = parseInt(input); 98 | if (contactNum > 0 && contactNum < weChatClient.contacts.length + 1) { 99 | message.recipient = weChatClient.contacts[contactNum - 1].UserName; 100 | weChatClient.slctdUser = message.recipient; 101 | iLoop = true; 102 | } else { 103 | toInnerLoop(); 104 | } 105 | } else { 106 | toInnerLoop(); 107 | } 108 | } 109 | if (iLoop) { 110 | weChatClient.promiseWhile(function() { 111 | return new Promise(function (resolve, reject) { 112 | (parseInt(input) === -1 && !wNope) ? reject() : resolve(); 113 | }); 114 | }, function() { 115 | return new Promise(function (resolve, reject) { 116 | craftMessage(input).then(function(message) { 117 | weChatClient.webwxsendmsg(loginData, message); 118 | toThreadLoop(); 119 | wStay = true; 120 | resolve(); 121 | }, function(passedWStep) { 122 | if (passedWStep && passedWStep !== 1) { 123 | toThreadLoop(passedWStep); 124 | } 125 | }); 126 | }); 127 | }, toInnerLoop); 128 | } 129 | } 130 | }); 131 | 132 | // Creates a message to be sent. 133 | function craftMessage(input) { 134 | return new Promise(function (resolve, reject) { 135 | input = input.trim(); 136 | var type = 1; 137 | if (input !== "-1") { 138 | if (wStep === 0) { 139 | if (!wStay) { 140 | toThreadLoop(wStep); 141 | listMessageThread(); 142 | } 143 | wStep++; 144 | reject(wStep); 145 | } else if (wStep === 1) { 146 | var number = parseInt(input); 147 | if (number && number > 0) { 148 | message.type = number; 149 | weChatClient.log(3, "What did you want to say to them?"); 150 | wNope = true; 151 | } else { 152 | toThreadLoop(); 153 | } 154 | wStep++; 155 | } else if (wStep === 2) { 156 | message.content = input; 157 | //log(0, "Message crafted"); // Verbose 158 | //log(4, "Message: " + JSON.stringify(message)); // Verbose 159 | resolve(message); 160 | } 161 | } else { 162 | reject(wStep); 163 | } 164 | }); 165 | } 166 | 167 | function listMessageThread() { 168 | for (var i = 0; i < weChatClient.messages.length; i++) { 169 | var sender = weChatClient.messages[i].FromUserName; 170 | var reciever = weChatClient.messages[i].ToUserName; 171 | if ((sender === weChatClient.slctdUser) || (reciever === weChatClient.slctdUser)) { 172 | var sendTime; 173 | if (reciever === weChatClient.slctdUser) { 174 | sendTime = parseInt(weChatClient.messages[i].ClientMsgId.slice(0, -4)); 175 | } else if (sender === weChatClient.slctdUser) { 176 | sendTime = weChatClient.messages[i].CreateTime * 1000; 177 | } else { 178 | weChatClient.log(-1, "Unknown message sendTime"); 179 | sendTime = Date.now(); 180 | } 181 | var ts = weChatClient.formTimeStamp(sendTime); 182 | if (sender === weChatClient.slctdUser) { 183 | weChatClient.log(5, ts + weChatClient.messages[i].Content, -1); 184 | } else if (reciever === weChatClient.slctdUser) { 185 | weChatClient.log(4, ts + weChatClient.messages[i].Content, -1); 186 | } else { 187 | weChatClient.log(-1, "display msg error: " + ts); 188 | } 189 | } 190 | } 191 | } 192 | 193 | // prompts user with the given question, and lists their contacts. 194 | function listUsers(question) { 195 | weChatClient.log(3, question); 196 | for (var i = 0; i < weChatClient.contacts.length; i++) { 197 | weChatClient.log(3, "Contact " + (i + 1) + ": " + weChatClient.contacts[i].NickName, -1); 198 | } 199 | } 200 | 201 | function toInnerLoop() { 202 | toXLoop(false, "Type 'b' to go Back to main menu, otherwise,"); 203 | weChatClient.slctdUser = ""; 204 | listUsers("Choose the number of the contact with which you'd like to interact"); 205 | } 206 | 207 | function toThreadLoop(writePoint) { 208 | var m = "Type '-1' to go back to contacts menu at any time during the message send process"; 209 | m += ". otherwise specify a message type in the form of a number. (1 for plaintext)"; 210 | toXLoop(false, m, message.recipient); 211 | if (writePoint) wStep = 1; 212 | } 213 | 214 | // Takes the user to the "outer loop" and resets the environment. 215 | function toOuterLoop() { 216 | toXLoop(true, "Type 's' to Select a user to interact with, and 'q' to Quit/logout"); 217 | } 218 | 219 | // level is boolean for being outer loop or not; message is message to display. 220 | function toXLoop(level, instruction, recipient) { 221 | oLoop = level; 222 | iLoop = (recipient ? true : false); 223 | wStep = 0; 224 | wNope = false; 225 | wStay = false; 226 | rStep = 0; 227 | message = { 228 | "recipient": recipient || "", 229 | "content": "", 230 | "type": 1 231 | }; 232 | weChatClient.log(3, instruction, -1); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /httpsWrapper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @description: A wrapper for the node.js standard "https" module to package the 3 | * weChatClient module as a freedom module, by making freedomjs xhr requests 4 | * instead of standard GET and POST requests. 5 | * 6 | * @author: Spencer Walden 7 | * Date created: July 14th, 2015 8 | * 9 | */ 10 | 11 | "use strict"; 12 | 13 | /* 14 | * @description: constructor for the "https" module 15 | */ 16 | var https = function() { 17 | //this.xhr = freedom["core.xhr"](); 18 | } 19 | 20 | /* 21 | * @description: A substitute for the "https.get(...)" method, substituting in 22 | * an xhr request; the only difference is that this will not accept a url as a 23 | * string, it must be an object. 24 | * @param: {Object} — Takes an object (JSON formatted or not), for the url to request. 25 | * @see: The example given for the format of the Object in the request method. 26 | * @param: {Function} — A callback to perform on the data returned by the GET request. 27 | * @returns {Object} — returns itself 28 | */ 29 | https.prototype.get = function(url, callback) { 30 | return this.request(url, callback).on("error", function() { 31 | return request; 32 | }).end(); 33 | } 34 | 35 | /* 36 | * @description: A substitute for the "https.request(...)" method, replacing any 37 | * requests made with an xhr. Useful in freedomjs modules. 38 | * @param: {Object} — Takes an object (JSON formatted or not) for the url to request. 39 | * @example: var url = { 40 | * "hostname": "www.google.com", 41 | * "method": "GET", 42 | * "path": "/", 43 | * "port": 443 44 | * }; 45 | * // In this module, the port is unneccessary, it will be sent as an https (443) request 46 | * @param: {Function} — Takes a callback to perform on the data returned by the request. 47 | * @returns {Object} — Returns itself. 48 | */ 49 | https.prototype.request = function(url, callback) { 50 | var xhr = freedom["core.xhr"](); // FIXME: never closed; memory leak here. 51 | var isBinary = (url.encoding ? true : false); 52 | 53 | xhr.open(url.method, "https://" + url.hostname + url.path, true); 54 | if (isBinary) xhr.overrideMimeType("text/plain; charset=x-user-defined"); 55 | if (typeof url === "object") { 56 | for (var key in url) { 57 | if (key !== "headers") { 58 | //console.log("Setting header: " + key + " = " + url[key]); // Verbose 59 | xhr.setRequestHeader(key, url[key]); 60 | } 61 | } 62 | } 63 | xhr.on("error", function(e) { 64 | xhr.on("onerror", function() { 65 | console.error(Error(e)); 66 | throw e; 67 | }); 68 | }); 69 | xhr.on("onload", function(thing) { 70 | var response = {}; 71 | xhr.getAllResponseHeaders().then(function(headers) { 72 | response.headers = headers; 73 | //console.log(headers); 74 | }, console.error); 75 | response.setEncoding = function(encoding) { 76 | //Don't think I need to do anything here 77 | }; 78 | (isBinary ? xhr.getResponse() : xhr.getResponseText()).then(function(responseData) { 79 | response.on = function(eventName, onCallback) { 80 | if (eventName === "data") { 81 | if (isBinary) { 82 | var bytes = new Uint8Array(responseData.string.length); 83 | var magicNum = ""; 84 | var dataString = ""; 85 | var mimeType = null; 86 | for (var i = 0; i < responseData.string.length; i++) { 87 | bytes[i] = responseData.string.charCodeAt(i); 88 | dataString += String.fromCharCode(bytes[i]); 89 | } 90 | var sub = bytes.subarray(0, 4); 91 | for (var b = 0; b < 3; b++) { // each element except for last. 92 | magicNum += sub[b] + "-"; 93 | } 94 | magicNum += sub[3]; // last element 95 | // Yes, I'm reading the file headers here since I had to strip off the mimeType 96 | // earlier so it wasn't passed to me encoded >_> 97 | if (magicNum === "255-216-255-224") { // yoya 98 | mimeType = "image/jpeg"; 99 | } else if (magicNum === "137-80-78-71") { // .PNG 100 | mimeType = "image/png"; 101 | } else { 102 | mimeType = "image/webp"; 103 | console.log("Unsupported image type: " + magicNum); 104 | } 105 | var dataURL = "data:" + mimeType + ";base64," + btoa(dataString); 106 | var imageResult = { 107 | "iconURLPath": url.path, 108 | "dataURL": dataURL 109 | }; 110 | onCallback(JSON.stringify(imageResult)); 111 | } else { 112 | onCallback(responseData); 113 | } 114 | } else if (eventName === "end") { 115 | onCallback(); 116 | } 117 | }.bind(this); 118 | callback(response); 119 | }.bind(this), function(e) { 120 | console.error("[-] " + Error(e)); 121 | throw e; 122 | }); 123 | }.bind(this)); 124 | 125 | var request = {}; 126 | request.on = function(eventName, networkErrorCallback) { 127 | request.end = function(data) { 128 | var toSend = ""; 129 | if (data) { 130 | //console.log("PostData: " + data); // Verbose 131 | toSend = {string: data}; 132 | } 133 | xhr.send(toSend); 134 | return xhr; // optional...? 135 | }; 136 | return request; 137 | }; 138 | return request; 139 | }.bind(this); 140 | 141 | module.exports.https = https; 142 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wechat-webclient", 3 | "version": "0.1.1", 4 | "description": "wechat webclient as a node.js module", 5 | "main": "wechat.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/LeMasque/uProxy_wechat.git" 12 | }, 13 | "keywords": [ 14 | "wechat" 15 | ], 16 | "author": "spencer walden", 17 | "license": "Apache-2.0", 18 | "bugs": { 19 | "url": "https://github.com/LeMasque/uProxy_wechat/issues" 20 | }, 21 | "homepage": "https://github.com/LeMasque/uProxy_wechat#readme", 22 | "dependencies": { 23 | "chalk": "^1.1.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /wechat.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @author: Spencer Walden 3 | * Date: June 16th, 2015 4 | * 5 | * @description: This is proof of concept code for authentication with the WeChat 6 | * webclient. It will hopefully help with WeChat authentication 7 | * support in uProxy as a social provider. 8 | * 9 | */ 10 | 11 | /********** Requires **********/ 12 | "use strict"; 13 | var httpsWrapper = require("./httpsWrapper.js"); 14 | var https = new httpsWrapper.https(); 15 | var chalk = require("chalk"); 16 | 17 | /********** Globals **********/ 18 | 19 | /* 20 | * @description: Constructs a new weChatClient object. 21 | * @param {Boolean} — a flag to determine if this client should use an https wrapper 22 | * or not. True for a wrap, false for the standard node.js "https" module. 23 | * @param {Boolean} — a flag to determine if this should be run in debug mode. Debug 24 | * mode simply provides more console output which can be helpful to debug. 25 | */ 26 | var weChatClient = function(wrapHttps, debug) { 27 | this.debug = debug; 28 | if (!wrapHttps) { 29 | https = require("https"); 30 | } 31 | 32 | this.WEBPATH = "/cgi-bin/mmwebwx-bin/"; 33 | this.USERAGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.81 Safari/537.36"; 34 | this.HIDDENMSGTYPE = 51; // As of July 23, 2015. 35 | this.isQQuser = false; // Default to using the wechat domains 36 | this.DOMAINS = { 37 | "true": { 38 | "web": "wx.qq.com", 39 | "log": "login.weixin.qq.com", 40 | "sync": "webpush.weixin.qq.com" 41 | }, 42 | "false": { 43 | "web": "web2.wechat.com", 44 | "log": "login.wechat.com", 45 | "sync": "webpush2.wechat.com" 46 | } 47 | }; 48 | 49 | this.events = {}; 50 | this.events.onWrongDom = function() { return; }; 51 | this.events.onMessage = function(message) { return; }; 52 | this.events.onQRCode = function(qrCode) { return; }; 53 | this.events.onIcon = function(iconURLPath) { return; }; 54 | this.events.onUUID = function(url) { return; }; 55 | this.events.onLogout = function() { return; }; 56 | this.events.onModChatroom = function(modChatroom) { return; }; 57 | this.events.onModContact = function(modContact) { return; }; 58 | this.events.onInitialized = function() { return; }; 59 | this.events.onWXIDs = function(wxids) { return; }; 60 | this.events.onSpecialMessage = function(something) { return; }; 61 | 62 | this.loginData = { 63 | "skey": "", 64 | "wxsid": "", 65 | "wxuin": "", 66 | "pass_ticket": "" 67 | }; 68 | this.cookies = {}; // Cookies to be sent in requests. 69 | this.syncKeys = null; // Object with List of key/value objects, and Count=List.Length 70 | this.contacts = {}; // Object of .UserName to their corresponding user object 71 | this.chatrooms = {}; // Object of .UserName to their corresponding chatroom object. 72 | this.thisUser = null; // User object for the user that is logged in using this client. 73 | this.messages = {}; // Object of .UserName matched to a List of relevant message objects. 74 | }; 75 | 76 | module.exports.weChatClient = weChatClient; 77 | 78 | /*********************************** FUNCTIONS *********************************/ 79 | 80 | /* 81 | * combination of preLogin and postLoginInit 82 | */ 83 | weChatClient.prototype.login = function(shouldDownloadQR, shouldDownloadIcons) { 84 | return new Promise(function (resolve, reject) { 85 | this.prelogin(shouldDownloadQR) 86 | .then(this.postLoginInit.bind(this, shouldDownloadIcons), this.handleError) 87 | .then(resolve, reject); 88 | }.bind(this)); 89 | }; 90 | 91 | /* 92 | * Technically logged in, but sets up some environment (necessary) 93 | */ 94 | weChatClient.prototype.postLoginInit = function(shouldDownloadIcons) { 95 | return new Promise(function (resolve, reject) { 96 | this.webwxinit() 97 | .then(this.webwxgetcontact.bind(this, shouldDownloadIcons), this.handleError) 98 | .then(resolve, reject); 99 | }.bind(this)); 100 | }; 101 | 102 | /* 103 | * Haven't logged in yet, steps to login 104 | */ 105 | weChatClient.prototype.preLogin = function(shouldDownloadQR) { 106 | return new Promise(function (resolve, reject) { 107 | this.getUUID() 108 | .then(this.checkForScan.bind(this, shouldDownloadQR), this.handleError) 109 | .then(this.webwxnewloginpage.bind(this, shouldDownloadQR), this.handleError) 110 | .then(resolve, reject); 111 | }.bind(this)); 112 | }; 113 | 114 | weChatClient.prototype.webwxsearchcontact = function(keyword) { 115 | return new Promise(function (resolve, reject) { 116 | var params = { 117 | "lang": "en_US", 118 | "pass_ticket": this.loginData.pass_ticket 119 | }; 120 | var postData = { 121 | "BaseRequest": this.formBaseRequest(), 122 | "KeyWord": keyword 123 | }; 124 | postData = JSON.stringify(postData); 125 | var url = this.makeURL(this.DOMAINS[this.isQQuser].web, this.WEBPATH + "webwxsearchcontact", params, postData.length); 126 | var request = https.request(url, function(response) { 127 | var result = ""; 128 | if (response.headers["set-cookie"]) { 129 | this.updateCookies(response.headers["set-cookie"]); 130 | } 131 | response.on("error", this.handleError.bind(this)); 132 | response.on("data", function(chunk) { 133 | result += chunk; 134 | }); 135 | response.on("end", function() { 136 | try { 137 | this.log(0, "searchcontact results: " + result); 138 | var jason = JSON.parse(result); 139 | if (jason.BaseResponse.Ret !== 0) { 140 | this.log(-1, "searchcontact error: " + jason.BaseResponse.Ret); 141 | } 142 | resolve(); 143 | } catch(e) { 144 | this.handleError(e).bind(this); 145 | reject(); 146 | } 147 | }.bind(this)); 148 | }.bind(this)).on("error", this.handleError.bind(this)); 149 | request.end(postData); 150 | }.bind(this)); 151 | }; 152 | 153 | // Provide List (i.e. []) with each index being a .UserName for the members 154 | // of the chat group you'd like to create. possibly also a string of topic/group name. 155 | weChatClient.prototype.webwxcreatechatroom = function(memberlist) { 156 | return new Promise(function (resolve, reject) { 157 | this.log(1, "creating chatroom"); 158 | for (var i = 0; i < memberlist.length; i++) { 159 | memberlist[i] = {"UserName": memberlist[i]}; 160 | } 161 | var params = { 162 | "r": Date.now(), 163 | "lang": "en_US", 164 | "pass_ticket": this.loginData.pass_ticket 165 | }; 166 | var postData = { 167 | "MemberCount": memberlist.length, 168 | "MemberList": memberlist, 169 | "Topic": "", // stupid, but I can't change name on creation. 170 | "BaseRequest": this.formBaseRequest() 171 | }; 172 | postData = JSON.stringify(postData); 173 | var url = this.makeURL(this.DOMAINS[this.isQQuser].web, this.WEBPATH + "webwxcreatechatroom", params, postData.length); 174 | var request = https.request(url, function(response) { 175 | var result = ""; 176 | if (response.headers["set-cookie"]) { 177 | this.updateCookies(response.headers["set-cookie"]); 178 | } 179 | response.on("error", this.handleError.bind(this)); 180 | response.on("data", function(chunk) { 181 | result += chunk; 182 | }); 183 | response.on("end", function() { 184 | try { 185 | this.log(0, "webwxcreatechatroom results: " + result); 186 | var jason = JSON.parse(result); 187 | if (jason.BaseResponse.ErrMsg !== "Everything is OK") { 188 | this.log(-1, "webwxcreatechatroom error: " + jason.BaseResponse.ErrMsg); 189 | reject(jason.BaseResponse.ErrMsg); 190 | } else { 191 | resolve(jason.ChatRoomName); 192 | } 193 | } catch(e) { 194 | this.handleError(e).bind(this); 195 | reject(); 196 | } 197 | }.bind(this)); 198 | }.bind(this)).on("error", this.handleError.bind(this)); 199 | request.end(postData); 200 | }.bind(this)); 201 | }; 202 | 203 | // resolves with chatroom UserName 204 | weChatClient.prototype.webwxbatchgetcontact = function(chatroomOrChatrooms, topic) { 205 | return new Promise(function (resolve, reject) { 206 | this.log(1, "getting chatroom users"); 207 | var memberlist = []; 208 | if (typeof chatroomOrChatrooms === "string") { 209 | memberlist[0] = {"UserName": chatroomOrChatrooms, "ChatRoomId": ""}; 210 | } else { 211 | for (var i = 0; i < chatroomOrChatrooms.length; i++) { 212 | memberlist[i] = {"UserName": chatroomOrChatrooms[i], "ChatRoomId": ""}; 213 | } 214 | } 215 | var params = { 216 | "type": "ex", 217 | "r": Date.now(), 218 | "lang": "en_US", 219 | "pass_ticket": this.loginData.pass_ticket 220 | }; 221 | var postData = { 222 | "BaseRequest": this.formBaseRequest(), 223 | "Count": memberlist.length, 224 | "List": memberlist 225 | }; 226 | postData = JSON.stringify(postData); 227 | var url = this.makeURL(this.DOMAINS[this.isQQuser].web, this.WEBPATH + "webwxbatchgetcontact", params, postData.length); 228 | var request = https.request(url, function(response) { 229 | var result = ""; 230 | if (response.headers["set-cookie"]) { 231 | this.updateCookies(response.headers["set-cookie"]); 232 | } 233 | response.on("error", this.handleError.bind(this)); 234 | response.on("data", function(chunk) { 235 | result += chunk; 236 | }); 237 | response.on("end", function() { 238 | try { 239 | //this.log(0, "webwxbatchgetcontact results: " + result); // Verbose 240 | var jason = JSON.parse(result); 241 | if (jason.BaseResponse.Ret !== 0) { 242 | this.log(-1, "webwxbatchgetcontact error: " + jason.BaseResponse.Ret); 243 | } 244 | var chatroomList = jason.ContactList; 245 | for (var j = 0; j < jason.Count; j++) { 246 | this.chatrooms[chatroomList[j].UserName] = chatroomList[j]; 247 | // TBD: webwxgetheadimg(); 248 | } 249 | resolve((typeof chatroomOrChatrooms === "string" ? chatroomOrChatrooms : "")); 250 | } catch(e) { 251 | this.handleError(e).bind(this); 252 | reject(); 253 | } 254 | }.bind(this)); 255 | }.bind(this)).on("error", this.handleError.bind(this)); 256 | request.end(postData); 257 | }.bind(this)); 258 | }; 259 | 260 | // provide update type either "delmember" or "modtopic", 261 | // provide either the .UserName of the member you'd like to delete or a string with the 262 | // new name of the chatroom you'd like. 263 | // provide the .UserName of the chatroom you'd like to update. 264 | // resolves with name of chatroom 265 | weChatClient.prototype.webwxupdatechatroom = function(updatetype, topicOrDeletion, chatroom) { 266 | return new Promise(function (resolve, reject) { 267 | this.log(1, "updating chatroom"); 268 | var postType = (updatetype === "modtopic" ? "NewTopic" : "DelMemberList") 269 | var params = { 270 | "fun": updatetype, 271 | "lang": "en_US", 272 | "pass_ticket": this.loginData.pass_ticket 273 | }; 274 | var postData = { 275 | "ChatRoomName": chatroom, 276 | "BaseRequest": this.formBaseRequest() 277 | }; 278 | postData[postType] = topicOrDeletion; 279 | postData = JSON.stringify(postData); 280 | var url = this.makeURL(this.DOMAINS[this.isQQuser].web, this.WEBPATH + "webwxupdatechatroom", params, postData.length); 281 | var request = https.request(url, function(response) { 282 | var result = ""; 283 | if (response.headers["set-cookie"]) { 284 | this.updateCookies(response.headers["set-cookie"]); 285 | } 286 | response.on("error", this.handleError.bind(this)); 287 | response.on("data", function(chunk) { 288 | result += chunk; 289 | }); 290 | response.on("end", function() { 291 | try { 292 | this.log(0, "webwxupdatechatroom results: " + result); 293 | var jason = JSON.parse(result); 294 | if (jason.BaseResponse.Ret !== 0) { 295 | this.log(-1, "webwxupdatechatroom error: " + jason.BaseResponse.Ret); 296 | } 297 | resolve(chatroom); 298 | } catch(e) { 299 | this.handleError(e).bind(this); 300 | reject(); 301 | } 302 | }.bind(this)); 303 | }.bind(this)).on("error", this.handleError.bind(this)); 304 | request.end(postData); 305 | }.bind(this)); 306 | }; 307 | 308 | /* 309 | * @description: Invoking this checks wechat servers to determine if there is new 310 | * data relevant to the currently logged in user to pull down. If there is, it 311 | * will return a number (called a selector) that is greater than 0, and you should 312 | * invoke the webwxsync method in order to update the information this client has. 313 | * This method must be called approximately every 4 or greater seconds, but should 314 | * it will invoke itself when it gets data back from the server. 315 | */ 316 | weChatClient.prototype.synccheck = function() { 317 | this.log(2, "Checking for updates (e.g. new messages)"); 318 | var retcode = 0; 319 | return this.promiseWhile(function() { 320 | return new Promise(function (resolve, reject) { 321 | if (retcode === 0) resolve(retcode); 322 | else reject(retcode); 323 | }); 324 | }, function() { 325 | return new Promise(function (resolve, reject) { 326 | var syncParams = { // #encodeeverythingthatwalks 327 | "r": Date.now(), 328 | "skey": encodeURIComponent(this.loginData.skey), 329 | "sid": encodeURIComponent(this.loginData.wxsid), 330 | "uin": encodeURIComponent(this.loginData.wxuin), 331 | "deviceid": this.getDeviceID(), 332 | "synckey": encodeURIComponent(this.formSyncKeys()), 333 | "lang": "en_US", 334 | "pass_ticket": encodeURIComponent(this.loginData.pass_ticket) 335 | }; 336 | var url = this.makeURL(this.DOMAINS[this.isQQuser].sync, this.WEBPATH + "synccheck", syncParams); 337 | https.get(url, function(response) { 338 | var result = ""; 339 | if (response.headers["set-cookie"]) { 340 | this.updateCookies(response.headers["set-cookie"]); 341 | } 342 | response.on("error", this.handleError.bind(this)); 343 | response.on("data", function(chunk) { 344 | result += chunk; 345 | }); 346 | response.on("end", function() { 347 | try { 348 | //this.log(4, "Synccheck response: " + result); // Verbose 349 | var fields = result.split("=")[1].trim().slice(1, -1).split(","); 350 | retcode = parseInt(fields[0].split(":")[1].slice(1,-1), 10); 351 | var type = parseInt(fields[1].split(":")[1].slice(1,-1), 10); 352 | if (this.debug) this.log(2, "SyncCheck: { Retcode: " + retcode + ", Selector: " + type + " }"); // Verbose 353 | if (retcode !== 0) this.log(-1, "Synccheck error code: " + retcode); 354 | if (type === 0) { // when selector is zero, just loop again. 355 | if (this.debug) this.log(-1, "Syncchecked with type " + type + ". No new info.."); 356 | resolve(); 357 | } else { 358 | // type 1 is profile sync. 359 | // type 2 is SyncKey update (?) 360 | // type 4 is ModContact sync.(?) // typically associated with sendmessage 361 | // type 7 is AddMsg sync. 362 | 363 | resolve(this.webwxsync(type)); 364 | } 365 | } catch(e) { 366 | handleError(e); 367 | } 368 | }.bind(this)); 369 | }.bind(this)).on("error", this.handleError.bind(this)); 370 | }.bind(this)); 371 | }.bind(this), function() { // also is getting passed the retcode in case of code revision, ignored here. 372 | // MMWEBWX_OK = 0 , 373 | // MMWEBWX_ERR_SYS = -1 , 374 | // MMWEBWX_ERR_LOGIC = -2 , 375 | // MMWEBWX_ERR_SESSION_NOEXIST = 1100, 376 | // MMWEBWX_ERR_SESSION_INVALID = 1101, 377 | // MMWEBWX_ERR_PARSER_REQUEST = 1200, 378 | // MMWEBWX_ERR_FREQ = 1205 379 | var codes = { 380 | "0" : "No problem, feel free to continue checking for updates", 381 | "-1" : "System error", 382 | "-2" : "Logic error", 383 | "1100": "Attempted to check for updates for a nonexistant (e.g. logged out) session", 384 | "1101": "Attempted to check for updates for an invalid session", 385 | "1200": "The webservice couldn't understand your request", 386 | "1205": "Attempted to check for updates too frequently; slow your roll" 387 | }; 388 | var airMessage = "retcode " + retcode + ": " + codes[retcode]; 389 | if (retcode === 1100) 390 | this.log(-1, airMessage); 391 | else 392 | this.handleError(airMessage).bind(this); 393 | }.bind(this)); 394 | }; 395 | 396 | /* 397 | * @description: Invoking this method will pull down relevant new data from the server 398 | * and update this client with it. This data could be new messages or contacts to 399 | * delete. This method is called by synccheck and need not be called externally. 400 | * 401 | * @param {Number} — As of v0.0.9, type is not used, but there are plans to use it in 402 | * future versions. Type (will) provide(s) which type of data specifically to update. 403 | */ 404 | weChatClient.prototype.webwxsync = function (type) { 405 | return new Promise(function (resolve, reject) { 406 | var postData = JSON.stringify({ 407 | "BaseRequest": this.formBaseRequest(), 408 | "SyncKey": this.syncKeys, 409 | "rr": ~Date.now() 410 | }); 411 | var params = { 412 | "sid": this.loginData.wxsid, 413 | "skey": this.loginData.skey, 414 | "lang": "en_US", 415 | "pass_ticket": this.loginData.pass_ticket 416 | }; 417 | var url = this.makeURL(this.DOMAINS[this.isQQuser].web, this.WEBPATH + "webwxsync", params, postData.length); 418 | //this.log(2, "posting: " + postData); // Verbose 419 | //this.log(2, "requesting: " + JSON.stringify(url)); // Verbose 420 | var request = https.request(url, function(response) { 421 | var result = ""; 422 | if (response.headers["set-cookie"]) { 423 | this.updateCookies(response.headers["set-cookie"]); 424 | } 425 | response.on("error", this.handleError.bind(this)); 426 | response.on("data", function(chunk) { 427 | result += chunk; 428 | }); 429 | response.on("end", function() { 430 | var jason; 431 | try { 432 | jason = JSON.parse(result); 433 | //this.log(1, "webwxsync response: " + JSON.stringify(jason)); // Verbose 434 | if (jason.BaseResponse.Ret !== 0) { 435 | this.log(-1, "webwxsync error: " + jason.BaseResponse.Ret); 436 | } 437 | // BaseResponse 438 | // AddMsgCount: n => AddMsgList 439 | // ModContactCount: n => ModContactList 440 | // DelContactCount: n => DelContactList 441 | // ModChatRoomMemberCount: n => ModChatRoomMemberList 442 | // Profile [obj] 443 | // ContinueFlag: n 444 | // SyncKey 445 | // Skey 446 | if (jason.AddMsgCount !== 0) { 447 | for (var i = 0; i < jason.AddMsgCount; i++) { 448 | var currMsg = jason.AddMsgList[i]; 449 | var sender = this.contacts[currMsg.FromUserName] || this.chatrooms[currMsg.FromUserName]; 450 | if (typeof this.messages[sender] === "undefined") 451 | this.messages[sender] = []; 452 | this.messages[sender].push(currMsg); 453 | if (!currMsg.StatusNotifyCode) { 454 | // For only handling x type messages here ( && currMsg.MsgType === x) 455 | if (currMsg.MsgType !== this.HIDDENMSGTYPE) { 456 | this.webwxStatusNotify(1, sender.UserName); 457 | } 458 | var ts = this.formTimeStamp(currMsg.CreateTime * 1000); 459 | this.log(5, ts + sender.NickName + ": " + currMsg.Content, -1); 460 | this.events.onMessage(currMsg); 461 | } else { 462 | var notifyMsg = currMsg.Content.replace(/</g, "<"); 463 | notifyMsg = notifyMsg.replace(/>/g, ">"); 464 | if (~notifyMsg.indexOf("")) { 465 | var notifyUsers = currMsg.StatusNotifyUserName.split(","); 466 | var unaccountedForChatrooms = []; 467 | var wxids = this.extractXMLData(notifyMsg, "username").split(","); 468 | var weixinIDs = {}; 469 | for (var k = 0; k < notifyUsers.length; k++) { 470 | var kthUser = notifyUsers[k]; 471 | if (kthUser.startsWith("@@") && 472 | typeof this.chatrooms[kthUser] === "undefined") { 473 | unaccountedForChatrooms.push(kthUser); 474 | this.chatrooms[kthUser] = {"Uin": 0}; 475 | } 476 | if (kthUser.startsWith("@")) { 477 | if (this.contacts[kthUser] && this.contacts[kthUser].Uin === 0) { 478 | this.contacts[kthUser].Uin = null; 479 | weixinIDs[kthUser] = wxids[k]; // TODO: test if i work 480 | } else if (this.chatrooms[kthUser] && this.chatrooms[kthUser].Uin === 0) { 481 | this.chatrooms[kthUser].Uin = null; 482 | weixinIDs[kthUser] = wxids[k]; // TODO: test if i work 483 | } 484 | } 485 | } 486 | //weixinIDs is .UserName => wxid_xxxxxxxxxxxxxx pairs 487 | if (unaccountedForChatrooms.length > 0) { 488 | this.webwxbatchgetcontact(unaccountedForChatrooms) 489 | .then(this.events.onWXIDs.bind(this, weixinIDs), this.handleError.bind(this)); 490 | } else { 491 | this.events.onWXIDs(weixinIDs); 492 | } 493 | } 494 | } 495 | } 496 | } 497 | if (jason.ModContactCount !== 0) { 498 | for (var j = 0; j < jason.ModContactCount; j++) { 499 | var modContact = jason.ModContactList[j]; 500 | if (modContact.UserName.startsWith("@@")) { 501 | this.chatrooms[modContact.UserName] = modContact; 502 | this.events.onModChatroom(modContact); 503 | } else { 504 | this.contacts[modContact.UserName] = modContact; 505 | this.events.onModContact(modContact); 506 | } 507 | } 508 | } 509 | this.syncKeys = jason.SyncKey; 510 | //this.log(0, "Synced with type " + type); // Verbose 511 | resolve(); 512 | } catch (air) { 513 | this.handleError(air).bind(this); 514 | } 515 | }.bind(this)); 516 | }.bind(this)).on("error", this.handleError.bind(this)); 517 | request.end(postData); 518 | }.bind(this)); 519 | }; 520 | 521 | /* 522 | * @description: Sends a message from thisUser to a given contact, through wechat. 523 | * type 1 messages are just plaintext as of August 31st, 2015. 524 | * 525 | * @param {Object}: 526 | * @param {String} — "content" field, a string of what you want to say to your recipient. 527 | * @param {Number} — "type" field, a number specifying which message type to send. 528 | * @param {String} — "recipient" field, a UserName of a contact. 529 | * @example: 530 | * var message = { 531 | * "content": "Hey John! How are you?", 532 | * "type": 1, 533 | * "recipient": getContactUserNameByNickName("John Doe") 534 | * }; 535 | */ 536 | weChatClient.prototype.webwxsendmsg = function (msg) { 537 | return new Promise(function (resolve, reject) { 538 | var params = { 539 | "lang": "en_US", 540 | "pass_ticket": this.loginData.pass_ticket 541 | }; 542 | var id = this.getMessageId(); 543 | var postData = { 544 | "BaseRequest": this.formBaseRequest(), 545 | "Msg": { 546 | "Type": msg.type, 547 | "Content": msg.content, 548 | "FromUserName": this.thisUser.UserName, 549 | "ToUserName": msg.recipient, 550 | "LocalID": id, 551 | "ClientMsgId": id 552 | } 553 | }; 554 | if (typeof this.messages[msg.recipient] === "undefined") 555 | this.messages[msg.recipient] = []; 556 | this.messages[msg.recipient].push(postData.Msg); 557 | postData = JSON.stringify(postData); 558 | var url = this.makeURL(this.DOMAINS[this.isQQuser].web, this.WEBPATH + "webwxsendmsg", params, postData.length); 559 | var request = https.request(url, function(response) { 560 | var result = ""; 561 | if (response.headers["set-cookie"]) { 562 | this.updateCookies(response.headers["set-cookie"]); 563 | } 564 | response.on("error", this.handleError.bind(this)); 565 | response.on("data", function(chunk) { 566 | result += chunk; 567 | }); 568 | response.on("end", function() { 569 | try { 570 | var jason = JSON.parse(result); 571 | //this.log(4, "sendmessage response: " + result); // Verbose 572 | var ts = this.formTimeStamp(parseInt(id.slice(0, -4))); 573 | this.log(0, ts + "Message sent"); 574 | if (jason.BaseResponse.Ret !== 0) { 575 | this.log(-1, "sendmessage error: " + jason.BaseResponse.Ret); 576 | } 577 | resolve(); 578 | } catch(e) { 579 | this.handleError(e).bind(this); 580 | reject(); 581 | } 582 | }.bind(this)); 583 | }.bind(this)).on("error", this.handleError.bind(this)); 584 | request.end(postData); 585 | }.bind(this), this.handleError.bind(this)); 586 | }; 587 | 588 | /* 589 | * @description: Pushes a notification to user devices. For example, buzzing 590 | * someone's phone when they have a new message. This is invoked by other functions 591 | * internally, and shouldn't need to be called directly by the programmer. 592 | * @param {Number} — The status code; status code corresponds to different situations. 593 | * @param {String} — The .UserName string of the user who's devices should be 594 | * notified of new data. 595 | */ 596 | weChatClient.prototype.webwxStatusNotify = function(statCode, sender) { 597 | // StatusNotify is a post request. 598 | if (statCode === 3) { 599 | this.log(2, "Notifying others of login"); 600 | } 601 | return new Promise(function (resolve, reject) { 602 | var params = { 603 | "lang": "en_US" 604 | }; 605 | var postData = JSON.stringify({ 606 | "BaseRequest": this.formBaseRequest(), 607 | "Code": statCode, // 3 for init, 1 for typical messages 608 | "FromUserName": this.thisUser.UserName, 609 | "ToUserName": (!sender ? this.thisUser.UserName : sender), 610 | "ClientMsgId": Date.now() 611 | }); 612 | var url = this.makeURL(this.DOMAINS[this.isQQuser].web, this.WEBPATH + "webwxstatusnotify", params, postData.length); 613 | var request = https.request(url, function(response) { 614 | var data = ""; 615 | if (response.headers["set-cookie"]) { 616 | this.updateCookies(response.headers["set-cookie"]); 617 | } 618 | response.on("error", this.handleError.bind(this)); 619 | response.on("data", function(chunk) { 620 | data += chunk; 621 | }); 622 | response.on("end", function() { 623 | try { 624 | var jason = JSON.parse(data); 625 | //this.log(2, JSON.stringify(jason)); // verbose 626 | if (jason.BaseResponse.ErrMsg) { 627 | this.log(-1, jason.BaseResponse.ErrMsg); 628 | } 629 | if (statCode === 3) this.log(0, "Other devices notified of login"); 630 | else if (statCode === 1) this.log(0, "Other devices notified of message"); 631 | resolve(); 632 | } catch(e) { 633 | this.handleError.bind(this, e); 634 | } 635 | }.bind(this)); 636 | }.bind(this)).on("error", this.handleError.bind(this)); 637 | request.end(postData); 638 | }.bind(this)); 639 | }; 640 | 641 | // Either sticky a contact on the top of your list, or change how their name appears to you. 642 | // action can be either "modremarkname" or "topcontact" 643 | // property is only used when the action is "modremarkname". It is the name which you'd like to 644 | // change your contact to have displayed to you. 645 | // user is a object. 646 | weChatClient.prototype.webwxoplog = function(action, property, user) { 647 | this.log(1, "oplogging"); 648 | return new Promise(function (resolve, reject) { 649 | var params = { 650 | "lang": "en_US", 651 | "pass_ticket": this.loginData.pass_ticket 652 | }; 653 | var postData = { 654 | "UserName": user.UserName, 655 | "BaseRequest": this.formBaseRequest() 656 | }; 657 | var actionId = 2; 658 | if (action === "topcontact") { 659 | actionId = 3; 660 | postData["OP"] = (user.ContactFlag / 2048 ? 0 : 1); 661 | } else { 662 | postData["RemarkName"] = property; 663 | } 664 | postData.CmdId = actionId; 665 | postData = JSON.stringify(postData); 666 | var url = this.makeURL(this.DOMAINS[this.isQQuser].web, this.WEBPATH + "webwxoplog", params, postData.length); 667 | var request = https.request(url, function(response) { 668 | if (response.headers["set-cookie"]) { 669 | this.updateCookies(response.headers["set-cookie"]); 670 | } 671 | var result = ""; 672 | response.on("error", this.handleError.bind(this)); 673 | response.on("data", function(chunk) { 674 | result += chunk; 675 | }); 676 | response.on("end", function() { 677 | try { 678 | var jason = JSON.parse(result); 679 | if (jason.BaseResponse.Ret !== 0) { 680 | this.log(-1, "Webwxoplog error: " + jason.BaseResponse.ErrMsg); 681 | reject(jason.BaseResponse.Ret); 682 | } else { 683 | this.log(0, "Webwxoplog success"); 684 | resolve(); 685 | } 686 | } catch (e) { 687 | this.handleError.bind(this, e); 688 | } 689 | }.bind(this)); 690 | }.bind(this)).on("error", this.handleError.bind(this)); 691 | request.end(postData); 692 | }.bind(this)); 693 | }; 694 | 695 | /* 696 | * @description: Logs the current user out, destroying their web session with wechat. 697 | * This will cause synccheck to fail with a code of 1011; this is to be expected. 698 | */ 699 | weChatClient.prototype.webwxlogout = function() { 700 | return new Promise(function (resolve, reject) { 701 | var params = { 702 | "redirect": 1, // They typically put 1 here, redirects if 0 anyways -- 1 for consistency 703 | "type": 0, 704 | "skey": this.loginData.skey 705 | }; 706 | var postData = "sid=" + this.loginData.wxsid + "&uin=" + this.loginData.wxuin; 707 | var url = this.makeURL(this.DOMAINS[this.isQQuser].web, this.WEBPATH + "webwxlogout", params, postData.length); 708 | var request = https.request(url, function(response) { 709 | if (response.headers["set-cookie"]) { 710 | this.updateCookies(response.headers["set-cookie"]); 711 | } 712 | var result = ""; 713 | response.on("error", this.handleError.bind(this)); 714 | response.on("data", function(chunk) { 715 | result += chunk; 716 | }); 717 | response.on("end", function() { 718 | this.log(0, "Logged out"); 719 | this.events.onLogout(); 720 | resolve(); 721 | }.bind(this)); 722 | }.bind(this)).on("error", this.handleError.bind(this)); 723 | request.end(postData); 724 | }.bind(this)); 725 | }; 726 | 727 | /* 728 | * @description: Gets all of the current user's contacts list's contact's icon photos. 729 | */ 730 | weChatClient.prototype.webwxgeticon = function() { 731 | this.log(1, "Getting contacts' icons"); 732 | var count = 1; 733 | for (var user in this.contacts) { 734 | var iconURLPath = this.contacts[user].HeadImgUrl; 735 | var the_earl_of_iconia = this.makeURL(this.DOMAINS[this.isQQuser].web, iconURLPath, ""); 736 | // if something in the normal https module doesn't exist, means a wrapper is in use, so... 737 | if (!https.createServer) the_earl_of_iconia["encoding"] = "binary"; 738 | https.get(the_earl_of_iconia, function(response) { 739 | if (response.headers["set-cookie"]) { 740 | this.updateCookies(response.headers["set-cookie"]); 741 | } 742 | response.setEncoding("binary"); 743 | var result = ""; 744 | response.on("error", this.handleError.bind(this)); 745 | response.on("data", function(chunk) { 746 | result += chunk; 747 | }); 748 | response.on("end", function() { 749 | this.log(0, "Got icon " + count++ + " of " + Object.keys(this.contacts).length); 750 | this.events.onIcon(result); 751 | }.bind(this)); 752 | }.bind(this)).on("error", this.handleError.bind(this)); 753 | } 754 | }; 755 | 756 | /* 757 | * @description: Gets the current user's contacts, populating the contacts object. 758 | * @param {Boolean} — whether or not to have this code download the icons of the ContactList. 759 | */ 760 | weChatClient.prototype.webwxgetcontact = function (GetIcon) { 761 | this.log(1, "Getting ContactList"); 762 | return new Promise(function (resolve, reject) { 763 | var clParams = { 764 | "lang": "en_US", 765 | "pass_ticket": this.loginData.pass_ticket, 766 | "r": Date.now(), 767 | "skey": this.loginData.skey 768 | }; 769 | var url = this.makeURL(this.DOMAINS[this.isQQuser].web, this.WEBPATH + "webwxgetcontact", clParams); 770 | //this.log(4, JSON.stringify(url)); // Verbose 771 | https.get(url, function(response) { 772 | var result = ""; 773 | if (response.headers["set-cookie"]) { 774 | this.updateCookies(response.headers["set-cookie"]); 775 | } 776 | response.on("error", this.handleError.bind(this)); 777 | response.on("data", function(chunk) { 778 | result += chunk; 779 | }); 780 | response.on("end", function() { 781 | try { 782 | //this.log(4, "Contacts received: " + result); // Verbose 783 | var jason = JSON.parse(result); 784 | if (jason.BaseResponse.ErrMsg) { 785 | this.log(-1, jason.BaseResponse.ErrMsg); 786 | } 787 | for (var i = 0; i < jason.MemberList.length; i++) { 788 | if (jason.MemberList[i].UserName.startsWith("@")) { 789 | this.contacts[jason.MemberList[i].UserName] = jason.MemberList[i]; 790 | //TODO: consider a Uin to UserName dictionary as well. 791 | } 792 | } 793 | this.log(0, "Got ContactList"); 794 | if (GetIcon) this.webwxgeticon(); 795 | resolve(); 796 | } catch (e) { 797 | this.handleError(e).bind(this); 798 | } 799 | }.bind(this)); 800 | }.bind(this)).on("error", this.handleError.bind(this)); 801 | }.bind(this)); 802 | }; 803 | 804 | /* 805 | * @description: Retrieves authentication information (e.g. cookies) to be used henceforth 806 | * with wechat. The authentication information is handled internally and shouldn't 807 | * require any efforts from the programmer to include it in further transactions with 808 | * the service. 809 | * @param {String} — Takes a url to get the information from. 810 | */ 811 | weChatClient.prototype.webwxnewloginpage = function (shouldDownloadQR, redirectURL) { 812 | if (!~redirectURL.indexOf("&fun=")) 813 | redirectURL += "&fun=new&version=v2"; 814 | else 815 | this.log(1, "redirect: " + redirectURL, -1); 816 | var url = this.makeURL(this.DOMAINS[this.isQQuser].web, redirectURL.substring(redirectURL.indexOf(this.WEBPATH)), ""); 817 | this.log(1, "Getting login data"); 818 | return new Promise(function (resolve, reject) { 819 | https.get(url, function(response) { 820 | var xml = ""; 821 | if (response.headers["set-cookie"]) { 822 | this.updateCookies(response.headers["set-cookie"]); 823 | } 824 | response.on("error", this.handleError.bind(this)); 825 | response.on("data", function(chunk) { 826 | xml += chunk; 827 | }); 828 | response.on("end", function() { 829 | if (~xml.indexOf("")) { 830 | var referral = this.extractXMLData(xml, "redirecturl"); 831 | this.isQQuser = !this.isQQuser; 832 | this.events.onWrongDom(shouldDownloadQR).then(resolve, reject); 833 | } else { 834 | for (var key in this.loginData) { 835 | this.loginData[key] = this.extractXMLData(xml, key); 836 | this.log(4, "Got xml data: " + key + " = " + this.loginData[key]); // Verbose 837 | } 838 | this.log(4, "Cookies: " + this.formCookies()); // Verbose 839 | this.log(0, "Got login data"); 840 | resolve(); 841 | } 842 | }.bind(this)); 843 | }.bind(this)).on("error", this.handleError.bind(this)); 844 | }.bind(this)); 845 | }; 846 | 847 | /* 848 | * @description: Gets and sets the current user (i.e. thisUser) 849 | */ 850 | weChatClient.prototype.webwxinit = function () { 851 | this.log(1, "Logging in"); 852 | return new Promise(function (resolve, reject) { 853 | var params = { 854 | "r": ~Date.now(), 855 | "lang": "en_US", 856 | "pass_ticket": this.loginData.pass_ticket 857 | }; 858 | var postData = JSON.stringify({ "BaseRequest": this.formBaseRequest() }); 859 | var url = this.makeURL(this.DOMAINS[this.isQQuser].web, this.WEBPATH + "webwxinit", params, postData.length); 860 | var request = https.request(url, function(response) { 861 | var data = ""; 862 | if (response.headers["set-cookie"]) { 863 | this.updateCookies(response.headers["set-cookie"]); 864 | } 865 | response.on("error", this.handleError.bind(this)); 866 | response.on("data", function(chunk) { 867 | data += chunk; 868 | }); 869 | response.on("end", function() { 870 | try { 871 | //this.log(2, "data: " + data); // verbose 872 | var jason = JSON.parse(data); 873 | if (jason.BaseResponse.Ret) { 874 | this.log(-1, jason.BaseResponse.Ret); 875 | } 876 | this.thisUser = jason.User; 877 | this.syncKeys = jason.SyncKey; 878 | this.log(0, "\"" + this.thisUser.NickName + "\" is now logged in"); 879 | this.webwxStatusNotify(3); 880 | this.events.onInitialized(); 881 | resolve(); 882 | } catch (e) { 883 | handleError(e); 884 | } 885 | }.bind(this)); 886 | }.bind(this)).on("error", this.handleError.bind(this)); 887 | request.end(postData); 888 | }.bind(this)); 889 | }; 890 | 891 | /* 892 | * @description: Gets a fresh UUID from wechat for getting a QR code. UUID's expire 893 | * after about 5 minutes, and you will need to call this function again and start 894 | * the login process all over if that happens. 895 | * @returns {String} — resolves with the UUID as a string on success. 896 | */ 897 | weChatClient.prototype.getUUID = function() { 898 | var uuidURLParameters = { 899 | "appid": "wx782c26e4c19acffb", 900 | "redirect_uri": encodeURIComponent("https://" + this.DOMAINS[this.isQQuser].web + this.WEBPATH + "webwxnewloginpage"), 901 | "fun": "new", 902 | "lang": "en_US" 903 | }; 904 | var url = this.makeURL(this.DOMAINS[this.isQQuser].log, "/jslogin", uuidURLParameters); 905 | this.log(1, "Getting UUID"); 906 | return new Promise(function(resolve, reject) { 907 | https.get(url, function(response) { 908 | var data = ""; 909 | response.on("error", this.handleError.bind(this)); 910 | response.on("data", function(chunk) { 911 | data += chunk; 912 | }); 913 | response.on("end", function() { 914 | var uuid = data.split(";")[1].split(" = ")[1].trim().slice(1,-1); 915 | this.log(0, "Got UUID " + uuid); 916 | this.events.onUUID("https://" + this.DOMAINS[this.isQQuser].log + "/qrcode/" + uuid); 917 | resolve(uuid); 918 | }.bind(this)); 919 | }.bind(this)).on("error", this.handleError.bind(this)); 920 | }.bind(this)); 921 | }; 922 | 923 | /* 924 | * @description: Gets the QR code corresponding to the given UUID. 925 | * @param {String} — The UUID corresponding to the QR code you want. 926 | * @returns {String} — resolves with the binary for the QR code on success. 927 | */ 928 | weChatClient.prototype.getQR = function(uuid) { 929 | var url = this.makeURL(this.DOMAINS[this.isQQuser].log, "/qrcode/" + uuid, { "t": "webwx" }); 930 | this.log(1, "Getting QR code"); 931 | return new Promise(function(resolve, reject) { 932 | https.get(url, function(response) { 933 | var imgQR = ""; 934 | response.setEncoding("binary"); 935 | response.on("error", this.handleError.bind(this)); 936 | response.on("data", function(chunk) { 937 | imgQR += chunk; 938 | }); 939 | response.on("end", function() { 940 | this.log(0, "Got QR code"); 941 | this.events.onQRCode(imgQR); 942 | resolve(imgQR); 943 | }.bind(this)); 944 | }.bind(this)).on("error", this.handleError.bind(this)); 945 | }.bind(this)); 946 | }; 947 | 948 | /* 949 | * @description: Calling this will check with the wechat servers to see if the QR 950 | * code associated with the UUID you provisioned has been scanned by a wechat phone app. 951 | * After about 5 minutes, the UUID will expire and you will need to request a new one. 952 | * @param {String} — The UUID you provisioned and recieved as the result of calling the 953 | * getUUID function. 954 | * @params {Boolean} — A flag to indicate if this client should get the QR code or not. 955 | * This flag being set to false is useful in situations where you might just want to 956 | * provide the URL with which a user can access the QR code, rather than get the QR 957 | * code through this client. 958 | * @returns {String} — On success, this will resolve with a String of the url you need to 959 | * access in order to get login authentication data (e.g. cookies). On failure, will throw 960 | * an error. 961 | */ 962 | weChatClient.prototype.checkForScan = function(getQR, uuid) { 963 | if (getQR) 964 | this.getQR(uuid); 965 | var result = { "code": 999 }; //initialize to nonexistant http code. 966 | var tip; 967 | this.log(2, "Checking for response codes indicating QR code scans"); 968 | return this.promiseWhile(function() { 969 | return new Promise(function (resolve, reject) { 970 | //test if url exists in the result, and the QR hasn't expired. 971 | if ((result.code !== 400) && (!result.url)) 972 | resolve(result); 973 | else // Want this case, means we got redirect url. 974 | reject(result); 975 | }); 976 | }, function() { // Check server for code saying there's been a scan. 977 | return new Promise(function (resolve, reject) { 978 | if (typeof tip !== "number") tip = 1; // tip is set to 1 on first request, zero otherwise 979 | else { 980 | this.log(2, "Checking for response code"); 981 | tip = 0; 982 | } 983 | var params = { 984 | "loginicon": true, 985 | "uuid": uuid, 986 | "tip": tip, 987 | "r": ~Date.now(), 988 | "lang": "en_US" 989 | }; 990 | var the_Czech_earl = this.makeURL(this.DOMAINS[this.isQQuser].log, this.WEBPATH + "login", params); 991 | //this.log(3, the_Czech_earl); 992 | https.get(the_Czech_earl, function(response) { 993 | var data = ""; 994 | response.on("error", this.handleError.bind(this)); 995 | response.on("data", function(chunk) { 996 | data += chunk; 997 | }); 998 | response.on("end", function() { 999 | //this.log(3, data); 1000 | var values = data.split(";"); 1001 | result.code = parseInt(values[0].split("=")[1], 10); 1002 | var respCode = "Got response code " + result.code + ": "; 1003 | var logMessages = { 1004 | "200": "Login confirmed, got redirect URL", 1005 | "201": "QR code scanned, confirm login on phone", 1006 | "400": "UUID expired", 1007 | "408": "Nothing eventful; QR code not scanned, usually" 1008 | }; 1009 | var logResponseCode = function(code) { 1010 | var sign = -0.5 * parseInt(code / 100, 10) + 1; 1011 | var logged = respCode + (logMessages[code] ? logMessages[code] : "Abnormal code"); 1012 | if (code === 400) { 1013 | reject(Error(logged)); 1014 | this.handleError(logged).bind(this); 1015 | } else { 1016 | if (code === 200) { 1017 | var temp = values[1].trim(); 1018 | result.url = temp.slice(temp.indexOf("https://"), -1); 1019 | } 1020 | resolve(result); 1021 | this.log(sign, logged); 1022 | } 1023 | }.bind(this); 1024 | logResponseCode(result.code); 1025 | }.bind(this)); 1026 | }.bind(this)).on("error", this.handleError.bind(this)); 1027 | }.bind(this)); 1028 | }.bind(this), function(onRejectparam) { // this will be our result object here 1029 | return new Promise(function (resolve, reject) { 1030 | // When we reject the condition, we got the redirect url. 1031 | if (onRejectparam.code === 200) { 1032 | resolve(onRejectparam.url); // resolve with url. 1033 | } else this.handleError(onRejectparam).bind(this); 1034 | }.bind(this)); 1035 | }.bind(this)); 1036 | }; 1037 | 1038 | 1039 | /**************************** HELPER FUNCTIONS *********************************/ 1040 | 1041 | /* 1042 | * @description: Generates a random string of numbers appended to an 'e'. This 1043 | * should never need to be called directly by the programmer, and is simply a 1044 | * string used in certain transactions with the server. 1045 | * @returns {String} — The pseudorandom string of numbers starting with the letter 'e'. 1046 | */ 1047 | weChatClient.prototype.getDeviceID = function() { 1048 | return "e" + ("" + Math.random().toFixed(15)).substring(2, 17); 1049 | }; 1050 | 1051 | /* 1052 | * @description: Takes some values and formats them into an object that can be used 1053 | * to make http(s) requests with. 1054 | * @param {String} — String representing the domain you'd like to access. 1055 | * @example: "www.github.com" 1056 | * @param {String} — String representing the path of the website you'd like to access. 1057 | * @example: Following with our domain example, "/freedomjs/freedom-social-wechat" 1058 | * To construct the full URL with no query parameters "https://www.github.com/freedomjs/freedom-social-wechat" 1059 | * @param {Object} — Object containing key value pairs of the query parameters of the 1060 | * request. 1061 | * @example: Say we want to access the URL 1062 | * "https://www.google.com/search?client=ubuntu&channel=fs&q=specifying&ie=utf-8&oe=utf-8" 1063 | * we take each query paramter and put it in the following format: 1064 | * var parameters = { 1065 | * "client": "ubuntu", 1066 | * "channel": "fs", 1067 | * "q": "specifying", 1068 | * "ie": "utf-8", 1069 | * "oe": "utf-8" 1070 | * }; 1071 | * and pass the paramters object we just made. 1072 | * @param {Number} — (Optional) This is the length of the data we send as part of a POST 1073 | * request. This value is optional, since in GET requests you don't send any POST data. 1074 | * If you want to form a POST request URL, you MUST include a postDataLen however. 1075 | * @returns {Object} — returns an URL object, to be used in making http(s) requests. 1076 | */ 1077 | weChatClient.prototype.makeURL = function(domain, path, params, postDataLen) { 1078 | path += "?"; 1079 | for (var key in params) 1080 | path += key + "=" + params[key] + "&"; 1081 | path = path.slice(0, -1); // removes trailing & or ? 1082 | var result = { 1083 | "hostname": domain, 1084 | "port": 443, //443 for https, 80 for http 1085 | "path": path, 1086 | "method": (postDataLen ? "POST" : "GET") 1087 | }; 1088 | if (this.cookies) { 1089 | result["headers"] = { 1090 | "User-Agent": this.USERAGENT, 1091 | "Connection": "keep-alive", 1092 | "Cookie": this.formCookies() 1093 | }; 1094 | if (postDataLen) { 1095 | result.headers["Content-Length"] = postDataLen; 1096 | result.headers["Content-Type"] = "application/json;charset=UTF-8"; 1097 | } 1098 | } 1099 | return result; 1100 | }; 1101 | 1102 | /* 1103 | * @description: Generic error handling function. Will produce a stack trace if available 1104 | * and will throw the error, potentially stopping further execution of the program. 1105 | * @param {Error || String} — Will accept an Error object, or a String as the message to 1106 | * display and the Error to throw. 1107 | */ 1108 | weChatClient.prototype.handleError = function(air) { 1109 | if (!air instanceof Error) 1110 | air = Error(air); 1111 | this.log(-2, air); 1112 | this.log(-1, air.stack); 1113 | throw air; 1114 | }; 1115 | 1116 | /* 1117 | * @description: Logging function, that displays messages in a consistent format. 1118 | * @param {Number} — A number to choose what type of message to display. 1119 | * Positive numbers are used for different types of notifications (1-5 are supported), 1120 | * Negative numbers for warnings or Errors (-1, -2 are supported), 1121 | * And the number zero is used for success messages. 1122 | * @param {String} — The message to be displayed in the console. 1123 | * @param {String || Number} — If the sign specified is positive, and the value passed 1124 | * is -1 (Number), then the ellipses automatically added will be overridden to be omitted. 1125 | * Otherwise, You can simply add a value here that you would like to be displayed. 1126 | * This is mainly used as just some type of debugging method. 1127 | */ 1128 | weChatClient.prototype.log = function(sign, message, output) { 1129 | var result; 1130 | var pColorize = [ 1131 | function(text) { return chalk.cyan(text); }, // "thread" 1 1132 | function(text) { return chalk.yellow(text); }, // "thread" 2 1133 | function(text) { return chalk.magenta(text); }, // program output (e.g. QRserver messages) 1134 | function(text) { return text; }, // Verbose output, and messages from thisUser 1135 | function(text) { return chalk.inverse(text); } // messages to thisUser 1136 | ]; 1137 | var nColorize = [ 1138 | function(text) { return chalk.red(text); }, // warning 1139 | function(text) { return chalk.bgRed(text); } // error 1140 | ]; 1141 | if (sign === 0) { 1142 | result = chalk.green("[+] " + message + "!"); 1143 | } else if (sign > 0) { 1144 | result = pColorize[sign - 1]("[*] " + message + (output === -1 ? "" : "...")); 1145 | } else { // sign < 0 1146 | result = nColorize[-sign - 1]("[-] " + message + "."); 1147 | } 1148 | var complete = result + (output && output !== -1 ? " " + output : ""); 1149 | if (sign < 0) 1150 | console.error(complete); 1151 | else 1152 | console.log(complete); 1153 | }; 1154 | 1155 | /* 1156 | * @description: This is a while loop, but for promises. This will check a condition, 1157 | * run a main body function until that condition is false, at which point it will 1158 | * run a rejection function. 1159 | * @param {Function} — A condition function that returns a Promise, resolving When 1160 | * a condition is met, rejecting when it is false. 1161 | * @param {Function} — A function that returns a Promise, resolving when it's task has 1162 | * completed successfully, rejecting on failure. This function will be run while the 1163 | * condition function resolves. 1164 | * @param {Function} — A function that returns a Promise, resolving with whatever value 1165 | * you want the promiseWhile call to resolve with. This function will run when the 1166 | * condition function rejects, i.e. the condition is no longer true. 1167 | * @returns {Promise} — resolve with whatever value you resolved with in the "onReject" 1168 | * function, rejects if something goes wrong in the body. 1169 | */ 1170 | weChatClient.prototype.promiseWhile = function(condition, body, onReject) { 1171 | return new Promise(function (resolve,reject) { 1172 | function loop() { 1173 | condition().then(function (result) { 1174 | // When it completes, loop again. Reject on failure... 1175 | body().then(loop, reject); 1176 | }, function (result) { 1177 | resolve(onReject(result)); 1178 | }); 1179 | } 1180 | loop(); 1181 | }.bind(this)); 1182 | }; 1183 | 1184 | /* 1185 | * @description: Formats the syncKeys for transmission in requests. 1186 | * @returns {String} — Formatted syncKeys 1187 | */ 1188 | weChatClient.prototype.formSyncKeys = function() { 1189 | var result = ""; 1190 | for (var i = 0; i < this.syncKeys.List.length; i++) 1191 | result += this.syncKeys.List[i].Key + "_" + this.syncKeys.List[i].Val + "|"; 1192 | return result.slice(0, -1); // removes trailing "|" 1193 | }; 1194 | 1195 | /* 1196 | * @description: Formats the cookies so they can be sent in requests. 1197 | * @returns {String} — Formatted cookies 1198 | */ 1199 | weChatClient.prototype.formCookies = function() { 1200 | var result = ""; 1201 | for (var key in this.cookies) 1202 | result += key + "=" + this.cookies[key] + "; "; 1203 | return result.slice(0, -2) // removes trailing "; " 1204 | }; 1205 | 1206 | /* 1207 | * @description: Updates the cookies Object (will create new cookies if the cookie 1208 | * previously didn't exist) 1209 | * @param {List} — The setCookie headers in the response from a request to a 1210 | * URL. 1211 | */ 1212 | weChatClient.prototype.updateCookies = function(setCookies) { 1213 | for (var i = 0; i < setCookies.length; i++) { 1214 | 1215 | var cookie = setCookies[i].split("; ")[0]; // cookie is now of form: key=value 1216 | //this.log(4, "Got cookie: " + cookie); // Verbose 1217 | 1218 | // don't use split here in case the value of cookie has an "=" in it. 1219 | // instead, get the index of the first occurance of "=" and separate. 1220 | var key = cookie.substr(0, cookie.indexOf("=")); 1221 | var value = cookie.substr(cookie.indexOf("=") + 1); 1222 | 1223 | this.cookies[key] = value; 1224 | } 1225 | }; 1226 | 1227 | /* 1228 | * @description: Generates a message ID. This is solely used in the "webwxsendmsg" function, 1229 | * as part of compliance with wechat message sending formats. 1230 | * @returns {String} — LocalID and/or ClientMsgId for a message. 1231 | */ 1232 | weChatClient.prototype.getMessageId = function() { 1233 | return Date.now() + Math.random().toFixed(3).replace(".", ""); 1234 | }; 1235 | 1236 | /* 1237 | * @description: Generates a nicely formatted timestamp. 1238 | * @param {Date} — A Date object for the Date/Time for which you want a formatted timestamp. 1239 | * @returns {String} — The formatted timestamp. 1240 | */ 1241 | weChatClient.prototype.formTimeStamp = function(sendTime) { 1242 | var time = new Date(sendTime); 1243 | var hh = time.getHours(); 1244 | var min = time.getMinutes(); 1245 | var sec = time.getSeconds(); 1246 | var mm = (min < 10 ? "0" + min : min); 1247 | var ss = (sec < 10 ? "0" + sec : sec); 1248 | var ts = "<" + hh + ":" + mm + ":" + ss + "> "; 1249 | return ts; 1250 | }; 1251 | 1252 | /* 1253 | * @description: Extracts the data from a tag in an XML blob. 1254 | * @param {String} — The XML where the tag you want data from resides. 1255 | * @param {String} — The tag you want the data extracted from 1256 | * @returns {String} — The data from the tag. 1257 | */ 1258 | weChatClient.prototype.extractXMLData = function(xml, tagName) { 1259 | var open = "<" + tagName + ">"; 1260 | var close = ""; 1261 | var begin = xml.indexOf(open) + open.length; 1262 | var end = xml.indexOf(close); 1263 | return xml.substring(begin, end); 1264 | }; 1265 | 1266 | /* 1267 | * @description: Creates a properly formatted "BaseRequest", an object that is sent 1268 | * in many wechat interactions. 1269 | * @returns {Object} — The properly formatted BaseRequest object. 1270 | */ 1271 | weChatClient.prototype.formBaseRequest = function() { 1272 | return { 1273 | "Uin": this.loginData.wxuin, 1274 | "Sid": this.loginData.wxsid, 1275 | "Skey": this.loginData.skey, 1276 | "DeviceID": this.getDeviceID() 1277 | }; 1278 | }; 1279 | --------------------------------------------------------------------------------