From 1fbb3e7b70a6368494fa311f6fee228879d5cdf6 Mon Sep 17 00:00:00 2001 From: Zhuohuan LI Date: Sat, 11 Jun 2016 11:33:31 +0800 Subject: [PATCH] code clean & better error catch & log --- README.md | 6 +- src/contact.js | 18 +-- src/message.js | 37 +++--- src/puppet-web-bridge.js | 85 ++++++++---- src/puppet-web-browser.js | 140 ++++++++++---------- src/puppet-web-server.js | 69 +++++----- src/puppet-web.js | 265 ++++++++++++++++++++++---------------- src/puppet.js | 3 +- src/room.js | 11 +- src/wechaty.js | 56 +++++--- 10 files changed, 401 insertions(+), 289 deletions(-) diff --git a/README.md b/README.md index e825f9f4..5f3665e6 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,12 @@ Connecting ChatBots. -Wechaty is a Chatbot Library for Wechat **Personal** Account. Supports [linux](https://travis-ci.org/zixia/wechaty), [win32](https://ci.appveyor.com/project/zixia/wechaty) and darwin(OSX/Mac). +Wechaty is a Chatbot Library for Wechat **Personal** Account. > Easy creating personal account wechat robot in 9 lines of code. +Supports [linux](https://travis-ci.org/zixia/wechaty), [win32](https://ci.appveyor.com/project/zixia/wechaty) and darwin(OSX/Mac). + [![Join the chat at https://gitter.im/zixia/wechaty](https://badges.gitter.im/zixia/wechaty.svg)](https://gitter.im/zixia/wechaty?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![node](https://img.shields.io/node/v/wechaty.svg?maxAge=2592000)](https://nodejs.org/) [![Repo Size](https://reposs.herokuapp.com/?path=zixia/wechaty)]() @@ -23,6 +25,8 @@ Can you image that? I'm dying... So a tireless bot working for me 24x7 on wechat, moniting/filtering the most important message is badly needed. For example: highlights discusstion which contains the KEYWORDS I want to follow up(especially in a noisy room). ;-) +At last, It's also built for my personal Automatically Testing study purpose. + # Examples Wechaty is super easy to use: 10 lines of javascript is enough for your first wechat robot. diff --git a/src/contact.js b/src/contact.js index 1a7f7e43..754d0b5a 100644 --- a/src/contact.js +++ b/src/contact.js @@ -42,6 +42,9 @@ class Contact { name() { return this.obj.name } remark() { return this.obj.remark } + stranger() { return this.obj.stranger } + star() { return this.obj.star } + get(prop) { return this.obj[prop] } ready(contactGetter) { log.silly('Contact', 'ready(' + typeof contactGetter + ')') @@ -63,7 +66,7 @@ class Contact { this.obj = this.parse(data) return this }).catch(e => { - log.error('Contact', `contactGetter(${this.id}) rejected: %s`, e) + log.error('Contact', `contactGetter(${this.id}) exception: %s`, e.message) throw e }) } @@ -77,19 +80,6 @@ class Contact { Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj[k]}`)) } - get(prop) { return this.obj[prop] } - - // KISS: don't cross reference - // send(message) { - // const msg = new Contact.puppet.Message() // create a empty message - // msg.set('from', this) - // msg.set('content', message) - - // return Contact.puppet.send(message) - // } - stranger() { return this.obj.stranger } - star() { return this.obj.star } - static find() { } static findAll() { } } diff --git a/src/message.js b/src/message.js index bc2b2d4f..576e5161 100644 --- a/src/message.js +++ b/src/message.js @@ -6,6 +6,7 @@ * https://github.com/zixia/wechaty * */ +const co = require('co') const Contact = require('./contact') const Room = require('./room') @@ -35,11 +36,6 @@ class Message { , date: rawObj.MMDisplayTime // Javascript timestamp of milliseconds , self: undefined // to store the logined user id - - // , from: Contact.load(rawObj.MMActualSender) - // , to: Contact.load(rawObj.ToUserName) - // , room: rawObj.MMIsChatRoom ? Room.load(rawObj.FromUserName) : null - // , date: new Date(rawObj.MMDisplayTime*1000) } } toString() { return this.obj.content } @@ -74,6 +70,9 @@ class Message { content() { return this.obj.content } room() { return this.obj.room } + type() { return Message.Type[this.obj.type] } + count() { return Message.counter } + self() { if (!this.obj.self) { log.warn('Message', 'self not set') @@ -85,17 +84,20 @@ class Message { ready() { log.silly('Message', 'ready()') - const from = Contact.load(this.obj.from) - const to = Contact.load(this.obj.to) - const room = this.obj.room ? Room.load(this.obj.room) : null - - return from.ready() // Contact from - .then(() => to.ready()) // Contact to - .then(() => room && room.ready()) // Room member list - .then(() => this) // return this for chain - .catch(e => { // REJECTED - log.error('Message', 'ready() rejected: %s', e) - throw e + return co.call(this, function* () { + const from = Contact.load(this.obj.from) + const to = Contact.load(this.obj.to) + const room = this.obj.room ? Room.load(this.obj.room) : null + + yield from.ready() // Contact from + yield to.ready() // Contact to + if (room) { // Room member list + yield room.ready() + } + return this // return this for chain + }).catch(e => { // Exception + log.error('Message', 'ready() exception: %s', e.message) + throw e }) } @@ -112,9 +114,6 @@ class Message { return this } - type() { return Message.Type[this.obj.type] } - count() { return Message.counter } - dump() { console.error('======= dump message =======') Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj[k]}`)) diff --git a/src/puppet-web-bridge.js b/src/puppet-web-bridge.js index 1dca6de3..fa815a5e 100644 --- a/src/puppet-web-bridge.js +++ b/src/puppet-web-bridge.js @@ -9,6 +9,7 @@ * https://github.com/zixia/wechaty-lib * */ +const co = require('co') const retryPromise = require('retry-promise').default const log = require('./npmlog-env') @@ -16,33 +17,57 @@ const log = require('./npmlog-env') class Bridge { constructor(options) { if (!options || !options.puppet) { throw new Error('Bridge need a puppet')} - log.verbose('Bridge', `new Bridge({puppet: ${options.puppet.constructor.name}, port: ${options.port}})`) + log.verbose('PuppetwebBridge', 'new Bridge({puppet: %s, port: %s})' + , options.puppet.constructor.name + , options.port) this.puppet = options.puppet this.port = options.port || 8788 // W(87) X(88), ascii char code ;-] } - toString() { return `Class Bridge({browser: ${this.options.browser}, port: ${this.options.port}})` } + toString() { return `Bridge({puppet: ${this.options.puppet.constructor.name}, port: ${this.options.port}})` } init() { - log.verbose('Bridge', 'init()') + log.verbose('PuppetwebBridge', 'init()') return this.inject() } - logout() { - log.verbose('Bridge', 'quit()') + logout() { + log.verbose('PuppetwebBridge', 'quit()') return this.proxyWechaty('logout') + .catch(e => { + log.error('PuppetwebBridge', 'logout() exception: %s', e.message) + throw e + }) } quit() { - log.verbose('Bridge', 'quit()') + log.verbose('PuppetwebBridge', 'quit()') return this.proxyWechaty('quit') + .catch(e => { + log.error('PuppetwebBridge', 'quit() exception: %s', e.message) + throw e + }) } // @Deprecated: use `scan` event instead - getLoginStatusCode() { return this.proxyWechaty('getLoginStatusCode') } + // getLoginStatusCode() { return this.proxyWechaty('getLoginStatusCode') } // @Deprecated: use `scan` event instead - getLoginQrImgUrl() { return this.proxyWechaty('getLoginQrImgUrl') } + // getLoginQrImgUrl() { return this.proxyWechaty('getLoginQrImgUrl') } - getUserName() { return this.proxyWechaty('getUserName') } + getUserName() { + return this.proxyWechaty('getUserName') + .catch(e => { + log.error('PuppetwebBridge', 'getUserName() exception: %s', e.message) + throw e + }) + } + + send(toUserName, content) { + return this.proxyWechaty('send', toUserName, content) + .catch(e => { + log.error('PuppetwebBridge', 'send() exception: %s', e.message) + throw e + }) + } getContact(id) { const max = 30 @@ -54,7 +79,7 @@ class Bridge { const timeout = max * (backoff * max) / 2 return retryPromise({ max: max, backoff: backoff }, function (attempt) { - log.silly('Bridge', 'getContact() retryPromise: attampt %s/%s time for timeout %s' + log.silly('PuppetwebBridge', 'getContact() retryPromise: attampt %s/%s time for timeout %s' , attempt, max, timeout) return this.proxyWechaty('getContact', id) @@ -64,16 +89,18 @@ class Bridge { } return r }) + .catch(e => { + log.error('PuppetwebBridge', 'proxyWechaty(getContact, %s) exception: %s', id, e.message) + throw e + }) }.bind(this)) .catch(e => { - log.error('Bridge', 'getContact() retryPromise finally FAIL: %s', e) + log.error('PuppetwebBridge', 'retryPromise() getContact() finally FAIL: %s', e.message) throw e }) ///////////////////////////////// } - send(toUserName, content) { return this.proxyWechaty('send', toUserName, content) } - getInjectio() { const fs = require('fs') const path = require('path') @@ -83,19 +110,17 @@ class Bridge { ) } inject() { - log.verbose('Bridge', 'inject()') - const injectio = this.getInjectio() - return this.execute(injectio, this.port) - .then(r => { - log.verbose('Bridge', `injected, got [${r}]. now initing...`) - return this.proxyWechaty('init') - }) - .then(r => { - log.verbose('Bridge', 'Wechaty.init() return: %s', r) + log.verbose('PuppetwebBridge', 'inject()') + return co.call(this, function* () { + const injectio = this.getInjectio() + let r = yield this.execute(injectio, this.port) + log.verbose('PuppetwebBridge', 'inject() injected, got [%s]', r) + r = yield this.proxyWechaty('init') + log.verbose('PuppetwebBridge', 'inject() Wechaty.init() return: %s', r) return r }) .catch (e => { - log.error('Bridge', 'inject() exception: %s', e.message) + log.error('PuppetwebBridge', 'inject() exception: %s', e.message) throw e }) } @@ -113,11 +138,21 @@ class Bridge { const argsDecoded = `JSON.parse(decodeURIComponent(window.atob('${argsEncoded}')))` const wechatyScript = `return (typeof Wechaty !== 'undefined' && Wechaty.${wechatyFunc}.apply(undefined, ${argsDecoded}))` - log.silly('Bridge', 'proxyWechaty: ' + wechatyScript) + log.silly('PuppetwebBridge', 'proxyWechaty(%s, ...args) %s', wechatyFunc, wechatyScript) return this.execute(wechatyScript) + .catch(e => { + log.error('PuppetwebBridge', 'proxyWechaty() exception: %s', e.message) + throw e + }) } - execute(script, ...args) { return this.puppet.browser.execute(script, ...args) } + execute(script, ...args) { + return this.puppet.browser.execute(script, ...args) + .catch(e => { + log.error('PuppetwebBridge', 'execute() exception: %s', e.message) + throw e + }) + } } module.exports = Bridge diff --git a/src/puppet-web-browser.js b/src/puppet-web-browser.js index 460b111f..e9fa3828 100644 --- a/src/puppet-web-browser.js +++ b/src/puppet-web-browser.js @@ -9,6 +9,7 @@ */ const fs = require('fs') +const co = require('co') const path = require('path') const util = require('util') const EventEmitter = require('events') @@ -17,10 +18,10 @@ const retryPromise = require('retry-promise').default // https://github.com/ola const log = require('./npmlog-env') -class Browser extends EventEmitter{ +class Browser extends EventEmitter { constructor(options) { super() - log.verbose('Browser', 'constructor()') + log.verbose('PuppetWebBrowser', 'constructor()') options = options || {} if (typeof options.head === 'undefined') { this.head = false // default @@ -31,7 +32,7 @@ class Browser extends EventEmitter{ this.live = false } - toString() { return `Class Browser({head:${this.head})` } + toString() { return `Browser({head:${this.head})` } init() { return this.initDriver() @@ -39,37 +40,36 @@ class Browser extends EventEmitter{ this.live = true return this }) - // XXX: if no `.catch` here, promise will hang! - // XXX: https://github.com/SeleniumHQ/selenium/issues/2233 - .catch(e => { throw e }) - - // console.log(p) - // return p.catch() - // .catch(e => { // XXX: must has a `.catch` here, or promise will hang! 2016/6/7 - // log.error('Browser', 'init() rejectec: %s', e) - // throw e - // }) + .catch(e => { + // XXX: must has a `.catch` here, or promise will hang! 2016/6/7 + // XXX: if no `.catch` here, promise will hang! + // XXX: https://github.com/SeleniumHQ/selenium/issues/2233 + log.error('PuppetWebBrowser', 'init() exception: %s', e.message) + throw e + }) } initDriver() { - log.verbose('Browser', 'initDriver(head: %s)', this.head) - if (this.head) { - if (/firefox/i.test(this.head)) { - this.driver = new WebDriver.Builder() - .setAlertBehavior('ignore') - .forBrowser('firefox') - .build() - } else { // default to chrome - this.driver = new WebDriver.Builder() - .setAlertBehavior('ignore') - .forBrowser('chrome') - .build() - } - } else { - this.driver = this.getPhantomJsDriver() - } - // console.log(this.driver) + log.verbose('PuppetWebBrowser', 'initDriver(head: %s)', this.head) return new Promise((resolve, reject) => { + if (this.head) { + if (/firefox/i.test(this.head)) { + this.driver = new WebDriver.Builder() + .setAlertBehavior('ignore') + .forBrowser('firefox') + .build() + } else if (/chrome/i.test(this.head)) { + this.driver = new WebDriver.Builder() + .setAlertBehavior('ignore') + .forBrowser('chrome') + .build() + } else { // unsupported browser head + throw new Error('unsupported head: ' + this.head) + } + } else { // no head default to phantomjs + this.driver = this.getPhantomJsDriver() + } + // XXX: if no `setTimeout()` here, promise will hang! // XXX: https://github.com/SeleniumHQ/selenium/issues/2233 setTimeout(() => { resolve(this.driver) }, 0) @@ -79,10 +79,11 @@ class Browser extends EventEmitter{ open(url) { url = url || 'https://wx.qq.com' - log.verbose('Browser', `open()ing at ${url}`) + log.verbose('PuppetWebBrowser', `open(${url})`) return this.driver.get(url) .catch(e => { + log.error('PuppetWebBrowser', 'open() exception: %s', e.message) this.dead(e.message) throw e.message }) @@ -99,50 +100,49 @@ class Browser extends EventEmitter{ .set('phantomjs.cli.args', [ '--ignore-ssl-errors=true' // this help socket.io connect with localhost , '--load-images=false' - , '--remote-debugger-port=9000' + , '--remote-debugger-port=8080' // , '--webdriver-logfile=/tmp/wd.log' // , '--webdriver-loglevel=DEBUG' ]) - log.silly('Browser', 'phantomjs binary: ' + phantomjsExe) + log.silly('PuppetWebBrowser', 'phantomjs binary: ' + phantomjsExe) return new WebDriver.Builder() .withCapabilities(customPhantom) .build() } - // selenium-webdriver/lib/capabilities.js - // 66 BROWSER_NAME: 'browserName', - // name() { return this.driver.getCapabilities().get('browserName') } - quit() { - log.verbose('Browser', 'quit()') + log.verbose('PuppetWebBrowser', 'quit()') this.live = false if (!this.driver) { - log.verbose('Browser', 'driver.quit() skipped because no driver') + log.verbose('PuppetWebBrowser', 'driver.quit() skipped because no driver') return Promise.resolve('no driver') } else if (!this.driver.getSession()) { this.driver = null - log.verbose('Browser', 'driver.quit() skipped because no driver session') + log.verbose('PuppetWebBrowser', 'driver.quit() skipped because no driver session') return Promise.resolve('no driver session') } - log.verbose('Browser', 'driver.quit') return this.driver.close() // http://stackoverflow.com/a/32341885/1123955 .then(() => this.driver.quit()) .catch(e => { // console.log(e) - // log.warn('Browser', 'err: %s %s %s %s', e.code, e.errno, e.syscall, e.message) + // log.warn('PuppetWebBrowser', 'err: %s %s %s %s', e.code, e.errno, e.syscall, e.message) const crashMsgs = [ 'ECONNREFUSED' , 'WebDriverError: .* not reachable' , 'NoSuchWindowError: no such window: target window already closed' ] const crashRegex = new RegExp(crashMsgs.join('|'), 'i') - if (crashRegex.test(e.message)) { log.warn('Browser', 'driver.quit() browser crashed') } - else { log.warn('Browser', 'driver.quit() rejected: %s', e.message) } + if (crashRegex.test(e.message)) { log.warn('PuppetWebBrowser', 'driver.quit() browser crashed') } + else { log.warn('PuppetWebBrowser', 'driver.quit() exception: %s', e.message) } }) .then(() => { this.driver = null }) .then(() => this.clean()) + .catch(e => { + log.error('PuppetWebBrowser', 'quit() exception: %s', e.message) + throw e + }) } clean() { @@ -155,7 +155,7 @@ class Browser extends EventEmitter{ const timeout = max * (backoff * max) / 2 return retryPromise({ max: max, backoff: backoff }, attempt => { - log.silly('Browser', 'clean() retryPromise: attampt %s time for timeout %s' + log.silly('PuppetWebBrowser', 'clean() retryPromise: attampt %s time for timeout %s' , attempt, timeout) return new Promise((resolve, reject) => { @@ -173,7 +173,7 @@ class Browser extends EventEmitter{ }) }) .catch(e => { - log.error('Browser', 'retryPromise failed: %s', e) + log.error('PuppetWebBrowser', 'retryPromise failed: %s', e) throw e }) } @@ -195,7 +195,7 @@ class Browser extends EventEmitter{ // convert expiry from seconds to milliseconds. https://github.com/SeleniumHQ/selenium/issues/2245 if (cookie.expiry) { cookie.expiry = cookie.expiry * 1000 } - log.silly('Browser', 'addCookies("%s", "%s", "%s", "%s", "%s", "%s")' + log.silly('PuppetWebBrowser', 'addCookies("%s", "%s", "%s", "%s", "%s", "%s")' , cookie.name, cookie.value, cookie.path, cookie.domain, cookie.secure, cookie.expiry ) @@ -203,14 +203,14 @@ class Browser extends EventEmitter{ .addCookie(cookie.name, cookie.value, cookie.path , cookie.domain, cookie.secure, cookie.expiry) .catch(e => { - log.warn('Browser', 'addCookies() rejected: %s', e.message) + log.warn('PuppetWebBrowser', 'addCookies() exception: %s', e.message) throw e }) } execute(script, ...args) { - //log.verbose('Browser', `Browser.execute(${script})`) - // log.verbose('Browser', `Browser.execute() driver.getSession: %s`, util.inspect(this.driver.getSession())) + //log.verbose('PuppetWebBrowser', `Browser.execute(${script})`) + // log.verbose('PuppetWebBrowser', `Browser.execute() driver.getSession: %s`, util.inspect(this.driver.getSession())) if (this.dead()) { return Promise.reject('browser dead') } return this.driver.executeScript.apply(this.driver, arguments) @@ -236,9 +236,9 @@ class Browser extends EventEmitter{ } if (dead) { - log.warn('Browser', 'dead() because %s', errMsg) - // must use nextTick here, or promise will hang... 2016/6/10 + log.warn('PuppetWebBrowser', 'dead() because %s', errMsg) this.live = false + // must use nextTick here, or promise will hang... 2016/6/10 process.nextTick(() => { this.emit('dead', errMsg) }) @@ -247,19 +247,23 @@ class Browser extends EventEmitter{ } checkSession(session) { - log.verbose('Browser', `checkSession(${session})`) + log.verbose('PuppetWebBrowser', `checkSession(${session})`) if (this.dead()) { return Promise.reject('checkSession() - browser dead')} return this.driver.manage().getCookies() .then(cookies => { // log.silly('PuppetWeb', 'checkSession %s', require('util').inspect(cookies.map(c => { return {name: c.name/*, value: c.value, expiresType: typeof c.expires, expires: c.expires*/} }))) - log.silly('Browser', 'checkSession %s', cookies.map(c => c.name).join(',')) + log.silly('PuppetWebBrowser', 'checkSession %s', cookies.map(c => c.name).join(',')) return cookies }) + .catch(e => { + log.error('PuppetWebBrowser', 'checkSession() getCookies() exception: %s', e.message) + throw e + }) } cleanSession(session) { - log.verbose('Browser', `cleanSession(${session})`) + log.verbose('PuppetWebBrowser', `cleanSession(${session})`) if (this.dead()) { return Promise.reject('cleanSession() - browser dead')} if (!session) { return Promise.reject('cleanSession() no session') } @@ -267,14 +271,14 @@ class Browser extends EventEmitter{ return new Promise((resolve, reject) => { require('fs').unlink(filename, err => { if (err && err.code!=='ENOENT') { - log.silly('Browser', 'cleanSession() unlink session file %s fail: %s', filename, err.message) + log.silly('PuppetWebBrowser', 'cleanSession() unlink session file %s fail: %s', filename, err.message) } resolve() }) }) } saveSession(session) { - log.verbose('Browser', `saveSession(${session})`) + log.verbose('PuppetWebBrowser', `saveSession(${session})`) if (this.dead()) { return Promise.reject('saveSession() - browser dead')} if (!session) { return Promise.reject('saveSession() no session') } @@ -296,23 +300,27 @@ class Browser extends EventEmitter{ }) // log.silly('PuppetWeb', 'saving %d cookies for session: %s', cookies.length // , util.inspect(cookies.map(c => { return {name: c.name /*, value: c.value, expiresType: typeof c.expires, expires: c.expires*/} }))) - log.silly('Browser', 'saving %d cookies for session: %s', cookies.length, cookies.map(c => c.name).join(',')) + log.silly('PuppetWebBrowser', 'saving %d cookies for session: %s', cookies.length, cookies.map(c => c.name).join(',')) const jsonStr = JSON.stringify(cookies) fs.writeFile(filename, jsonStr, function(err) { if(err) { - log.error('Browser', 'saveSession() fail to write file %s: %s', filename, err.Error) + log.error('PuppetWebBrowser', 'saveSession() fail to write file %s: %s', filename, err.Error) return reject(err) } - log.verbose('Browser', 'saved session(%d cookies) to %s', cookies.length, session) + log.verbose('PuppetWebBrowser', 'saved session(%d cookies) to %s', cookies.length, session) return resolve(cookies) - }.bind(this)) + }) + }) + .catch(e => { + log.error('PuppetWebBrowser', 'saveSession() getCookies() exception: %s', e.message) + reject(e) }) }) } loadSession(session) { - log.verbose('Browser', `loadSession(${session})`) + log.verbose('PuppetWebBrowser', `loadSession(${session})`) if (this.dead()) { return Promise.reject('loadSession() - browser dead')} if (!session) { return Promise.reject('loadSession() no session') } @@ -321,19 +329,19 @@ class Browser extends EventEmitter{ return new Promise((resolve, reject) => { fs.readFile(filename, (err, jsonStr) => { if (err) { - if (err) { log.silly('Browser', 'loadSession(%s) skipped because error code: %s', session, err.code) } + if (err) { log.silly('PuppetWebBrowser', 'loadSession(%s) skipped because error code: %s', session, err.code) } return reject('error code:' + err.code) } const cookies = JSON.parse(jsonStr) const ps = this.addCookies(cookies) - return Promise.all(ps) + Promise.all(ps) .then(() => { - log.verbose('Browser', 'loaded session(%d cookies) from %s', cookies.length, session) + log.verbose('PuppetWebBrowser', 'loaded session(%d cookies) from %s', cookies.length, session) resolve(cookies) }) .catch(e => { - log.error('Browser', 'loadSession rejected: %s', e) + log.error('PuppetWebBrowser', 'loadSession() addCookies() exception: %s', e.message) reject(e) }) }) diff --git a/src/puppet-web-server.js b/src/puppet-web-server.js index 319a13ff..ff0e7c8a 100644 --- a/src/puppet-web-server.js +++ b/src/puppet-web-server.js @@ -27,20 +27,24 @@ class Server extends EventEmitter { this.port = options.port || 8788 // W(87) X(88), ascii char code ;-] } - toString() { return `Class Wechaty.Puppet.Web.Server({port:${this.port}})` } + toString() { return `Server({port:${this.port}})` } init() { - log.verbose('Server', 'init()') - this.initEventsToClient() + log.verbose('PuppetwebServer', 'init()') return new Promise((resolve, reject) => { + // this.initEventsToClient() + this.express = this.createExpress() this.httpsServer = this.createHttpsServer(this.express , r => resolve(r), e => reject(e) ) this.socketServer = this.createSocketIo(this.httpsServer) }).then(r => { - log.verbose('Server', 'full init()-ed') + log.verbose('PuppetWebServer', 'full init()-ed') return true + }).catch(e => { + log.error('PuppetWebServer', 'init() exception: %s', e.message) + throw e }) } @@ -51,19 +55,20 @@ class Server extends EventEmitter { return https.createServer({ key: require('./ssl-pem').key , cert: require('./ssl-pem').cert - }, express) - .listen(this.port, () => { - log.verbose('Server', `createHttpsServer listen on port ${this.port}`) + }, express) // XXX: is express must exist here? try to get rid it later. 2016/6/11 + .listen(this.port, err => { + if (err) { + log.error('PuppetWebServer', 'createHttpsServer() exception: %s', err) + if (typeof reject === 'function') { + reject(err) + } + return + } + log.verbose('PuppetwebServer', `createHttpsServer() listen on port ${this.port}`) if (typeof resolve === 'function') { resolve(this) } }) - .on('error', e => { - log.error('Server', 'createHttpsServer:' + e) - if (typeof reject === 'function') { - reject(e) - } - }) } /** @@ -78,7 +83,7 @@ class Server extends EventEmitter { next() }) e.get('/ding', function(req, res) { - log.silly('Server', '%s GET /ding', new Date()) + log.silly('PuppetwebServer', 'createExpress() %s GET /ding', new Date()) res.end('dong') }) return e @@ -92,7 +97,7 @@ class Server extends EventEmitter { // log: true }) socketServer.sockets.on('connection', (s) => { - log.verbose('Server', 'got connection from browser') + log.verbose('PuppetWebServer', 'createSocketIo() got connection from browser') if (this.socketClient) { this.socketClient = null } // close() ??? this.socketClient = s this.initEventsFromClient(s) @@ -101,19 +106,19 @@ class Server extends EventEmitter { } initEventsFromClient(client) { - log.verbose('Server', 'initEventFromClient()') + log.verbose('PuppetWebServer', 'initEventFromClient()') this.emit('connection', client) client.on('disconnect', e => { - log.verbose('Server', 'socket.io disconnect: %s', e) + log.verbose('PuppetwebServer', 'socket.io disconnect: %s', e) // 1. Browser reload / 2. Lost connection(Bad network) this.socketClient = null this.emit('disconnect', e) }) - client.on('error' , e => log.error('Server', 'socketio client error: %s', e)) - client.on('ding' , e => log.silly('Server', 'got ding: %s', e)) + client.on('error' , e => log.error('PuppetwebServer', 'initEventsFromClient() client on error: %s', e.message)) + client.on('ding' , e => log.silly('PuppetwebServer', 'initEventsFromClient() client on ding: %s', e)) // Events from Wechaty@Broswer --to--> Server ;[ @@ -126,34 +131,34 @@ class Server extends EventEmitter { , 'dong' ].map(e => { client.on(e, data => { - log.silly('Server', `recv event[${e}](${data}) from browser`) + log.silly('PuppetwebServer', `initEventsFromClient() client on event[${e}](${data}) from browser, emit it`) this.emit(e, data) }) }) } - initEventsToClient() { - log.verbose('Server', 'initEventToClient()') - this.on('ding', data => { - log.silly('Server', `recv event[ding](${data}), sending to client`) - if (this.socketClient) { this.socketClient.emit('ding', data) } - else { log.warn('Server', 'this.socketClient not exist')} - }) - } + // initEventsToClient() { + // log.verbose('PuppetwebServer', 'initEventToClient()') + // this.on('ding', data => { + // log.silly('PuppetwebServer', `recv event[ding](${data}), sending to client`) + // if (this.socketClient) { this.socketClient.emit('ding', data) } + // else { log.warn('PuppetwebServer', 'this.socketClient not exist')} + // }) + // } quit() { - log.verbose('Server', 'quit()') + log.verbose('PuppetwebServer', 'quit()') if (this.socketServer) { - log.verbose('Server', 'close socketServer') + log.verbose('PuppetwebServer', 'closing socketServer') this.socketServer.close() this.socketServer = null } if (this.socketClient) { - log.verbose('Server', 'close socketClient') + log.verbose('PuppetwebServer', 'closing socketClient') this.socketClient = null } if (this.httpsServer) { - log.verbose('Server', 'close httpsServer') + log.verbose('PuppetwebServer', 'closing httpsServer') this.httpsServer.close() this.httpsServer = null } diff --git a/src/puppet-web.js b/src/puppet-web.js index 5f46d365..ea87e550 100644 --- a/src/puppet-web.js +++ b/src/puppet-web.js @@ -4,7 +4,7 @@ * * Class PuppetWeb * - * use to control wechat web. + * use to control wechat in web browser. * * Licenst: ISC * https://github.com/zixia/wechaty @@ -16,13 +16,13 @@ * Class PuppetWeb * ***************************************/ -const util = require('util') -const fs = require('fs') -const co = require('co') +const util = require('util') +const fs = require('fs') +const co = require('co') const log = require('./npmlog-env') -const Puppet = require('./puppet') +const Puppet = require('./puppet') const Message = require('./message') const Contact = require('./contact') const Room = require('./room') @@ -35,9 +35,9 @@ class PuppetWeb extends Puppet { constructor(options) { super() options = options || {} - this.port = options.port || 8788 // W(87) X(88), ascii char code ;-] - this.head = options.head - this.session = options.session // if not set session, then dont store session. + this.port = options.port || 8788 // W(87) X(88), ascii char code ;-] + this.head = options.head + this.session = options.session // if not set session, then dont store session. this.user = null // of user self } @@ -60,14 +60,12 @@ class PuppetWeb extends Puppet { yield this.initBridge() log.verbose('PuppetWeb', 'initBridge() done') - - return this }) - .catch(e => { // Reject - log.error('PuppetWeb', e) + .catch(e => { // Reject + log.error('PuppetWeb', 'init exception: %s', e.message) throw e }) - .then(() => { // Finally + .then(() => { // Finally log.verbose('PuppetWeb', 'init() done') return this // for Chaining }) @@ -78,31 +76,31 @@ class PuppetWeb extends Puppet { return co.call(this, function* () { if (this.bridge) { - yield this.bridge.quit().catch(e => { - log.warn('PuppetWeb', 'quite() bridge.quite() rejected: %s', e) + yield this.bridge.quit().catch(e => { // fail safe + log.warn('PuppetWeb', 'quite() bridge.quit() exception: %s', e.message) }) this.bridge = null - } else { log.warn('PuppetWeb', 'quit() without bridge') } + } else { log.warn('PuppetWeb', 'quit() without a bridge') } if (this.server) { yield this.server.quit() this.server = null - } else { log.warn('PuppetWeb', 'quit() without server') } + } else { log.warn('PuppetWeb', 'quit() without a server') } if (this.browser) { - yield (this.browser.quit().catch(e => { // fall safe - log.warn('PuppetWeb', 'quit() browser.quit() fail: %s', e) + yield (this.browser.quit().catch(e => { // fail safe + log.warn('PuppetWeb', 'quit() browser.quit() exception: %s', e.message) })) this.browser = null - } else { log.warn('PuppetWeb', 'quit() without browser') } + } else { log.warn('PuppetWeb', 'quit() without a browser') } yield this.initAttach(null) }) - .catch(e => { // Reject - log.error('PuppetWeb', 'quit() co rejected: %s', e) + .catch(e => { // Reject + log.error('PuppetWeb', 'quit() exception: %s', e.message) throw e }) - .then(() => { // Finally, Fall Safe + .then(() => { // Finally, Fail Safe log.verbose('PuppetWeb', 'quit() done') return this // for Chaining }) @@ -112,12 +110,11 @@ class PuppetWeb extends Puppet { log.verbose('PuppetWeb', 'initAttach()') Contact.attach(puppet) Room.attach(puppet) - return Promise.resolve(true) + return Promise.resolve(!!puppet) } initBrowser() { - log.verbose('PuppetWeb', 'initBrowser') - this.browser = new Browser({ head: this.head }) - + log.verbose('PuppetWeb', 'initBrowser()') + this.browser = new Browser({head: this.head}) this.browser.on('dead', this.onBrowserDead.bind(this)) // fastUrl is used to open in browser for we can set cookies. @@ -128,28 +125,29 @@ class PuppetWeb extends Puppet { yield this.browser.init() yield this.browser.open(fastUrl) if (this.session) { - yield this.browser.loadSession(this.session) - .catch(e => { log.verbose('PuppetWeb', 'loadSession rejected: %s', e) /* fail safe */ }) + yield this.browser.loadSession(this.session).catch(e => { // fail safe + log.verbose('PuppetWeb', 'browser.loadSession() exception: %s', e.message) + }) } yield this.browser.open() }).catch(e => { - log.error('PuppetWeb', 'initBrowser rejected: %s', e) + log.error('PuppetWeb', 'initBrowser() exception: %s', e.message) throw e }) } initBridge() { log.verbose('PuppetWeb', 'initBridge()') this.bridge = new Bridge({ - puppet: this // use puppet instead of browser, is because browser might be changed duaring run time + puppet: this // use puppet instead of browser, is because browser might change(die) duaring run time , port: this.port }) return this.bridge.init() .catch(e => { if (this.browser.dead()) { - log.warn('PuppetWeb', 'initBridge() found browser dead, wait to restore') + log.warn('PuppetWeb', 'initBridge() found browser dead, wait it to restore') } else { - log.error('PuppetWeb', 'initBridge() init fail: %s', e.message) + log.error('PuppetWeb', 'initBridge() exception: %s', e.message) throw e } }) @@ -172,60 +170,113 @@ class PuppetWeb extends Puppet { 'dong' ].map(e => { server.on(e, data => { - log.verbose('PuppetWeb', 'Server event[%s]: %s', e, data) + log.verbose('PuppetWeb', 'Server event[%s]: %s', e, typeof data) this.emit(e, data) }) }) this.server = server return this.server.init() + .catch(e => { + log.error('PuppetWeb', 'initServer() exception: %s', e.message) + throw e + }) } onBrowserDead(data) { log.verbose('PuppetWeb', 'onBrowserDead(%s)', data) return co.call(this, function* () { - log.verbose('PuppetWeb', 'onBrowserDead() try to fix browser') + log.verbose('PuppetWeb', 'try to reborn browser') yield this.browser.quit() - .then(() => { - log.verbose('PuppetWeb', 'onBrowserDead() browser quited') - }) - .catch(e => { // fall safe - log.warn('PuppetWeb', 'quit() browser.quit() fail: %s', e) + .catch(e => { // fail safe + log.warn('PuppetWeb', 'browser.quit() exception: %s', e.message) }) + log.verbose('PuppetWeb', 'old browser quited') yield this.initBrowser() - log.verbose('PuppetWeb', 'onBrowserDead() browser inited') + log.verbose('PuppetWeb', 'new browser inited') yield this.bridge.init() - log.verbose('PuppetWeb', 'onBrowserDead() bridge inited') + log.verbose('PuppetWeb', 'bridge re-inited') }) .then(() => { - log.verbose('PuppetWeb', 'onBrowserDead fixed browser') + log.verbose('PuppetWeb', 'onBrowserDead() new browser borned') }) .catch(e => { - log.error('PuppetWeb', 'onBrowserDead rejected: %s', e) + log.error('PuppetWeb', 'onBrowserDead() exception: %s', e.message) + throw e }) } onServerScan(data) { - log.verbose('PuppetWeb', 'onServerScan: %s', Object.keys(data).join(',')) - + log.verbose('PuppetWeb', 'onServerScan(%d)', data && data.code) if (this.session) { this.browser.cleanSession(this.session) - .catch(() => {/* fall safe */}) + .catch(() => {/* fail safe */}) } this.emit('scan', data) } onServerConnection(data) { - log.verbose('PuppetWeb', 'onServerConnection: %s', data.constructor.name) + log.verbose('PuppetWeb', 'onServerConnection: %s', typeof data) } onServerDisconnect(data) { log.verbose('PuppetWeb', 'onServerDisconnect: %s', data) - log.verbose('PuppetWeb', 'onServerDisconnect: unloaded? call onServerUnload to try to fix connection') - this.onServerUnload(data) + /** + * conditions: + * 1. browser crash(i.e.: be killed) + * 2. quiting + */ + if (!this.browser) { // no browser, quiting? + log.verbose('PuppetWeb', 'onServerDisconnect() no browser. maybe Im quiting, do nothing') + return + } else if (this.browser.dead()) { // browser is dead + log.verbose('PuppetWeb', 'onServerDisconnect() found dead browser. wait it to restore') + return + } else if (!this.bridge) { // no bridge, quiting??? + log.verbose('PuppetWeb', 'onServerDisconnect() no bridge. maybe Im quiting, do nothing') + return + } else { // browser is alive, and we have a bridge to it + log.verbose('PuppetWeb', 'onServerDisconnect() re-initing bridge') + process.nextTick(() => { + this.bridge.init() + .then(r => log.verbose('PuppetWeb', 'onServerDisconnect() bridge re-inited: %s', r)) + .catch(e => log.error('PuppetWeb', 'onServerDisconnect() exception: %s', e.message)) + }) + return + } + } + /** + * `unload` event is sent from js@browser to webserver via socketio + * after received `unload`, we should re-inject the Wechaty js code into browser. + * possible conditions: + * 1. browser refresh + * 2. browser navigated to a new url + * 3. browser quit(crash?) + * 4. ... + */ + onServerUnload(data) { + log.warn('PuppetWeb', 'onServerUnload(%s)', typeof data) + // this.onServerLogout(data) // XXX: should emit event[logout] from browser + + if (!this.browser) { + log.warn('PuppetWeb', 'onServerUnload() found browser gone, should be quiting now') + return + } else if (!this.bridge) { + log.warn('PuppetWeb', 'onServerUnload() found bridge gone, should be quiting now') + return + } else if (this.browser.dead()) { + log.error('PuppetWeb', 'onServerUnload() found browser dead. wait it to restore itself') + return + } + // re-init bridge + return process.nextTick(() => { + this.bridge.init() + .then(r => log.verbose('PuppetWeb', 'onServerUnload() bridge.init() done: %s', r)) + .catch(e => log.error('PuppetWeb', 'onServerUnload() bridge.init() exceptoin: %s', e.message)) + }) } onServerLog(data) { log.verbose('PuppetWeb', 'onServerLog: %s', data) @@ -241,23 +292,33 @@ class PuppetWeb extends Puppet { setTimeout(this.onServerLogin.bind(this), 500) return } - log.silly('PuppetWeb', 'userName: %s', userName) + log.verbose('PuppetWeb', 'bridge.getUserName: %s', userName) this.user = yield Contact.load(userName).ready() - log.verbose('PuppetWeb', `user ${this.user.name()} logined`) + log.verbose('PuppetWeb', `onServerLogin() user ${this.user.name()} logined`) this.emit('login', this.user) if (this.session) { - yield this.browser.saveSession(this.session).catch(() => {/* fall safe */}) + yield this.browser.saveSession(this.session) + .catch(e => { // fail safe + log.warn('PuppetWeb', 'browser.saveSession exception: %s', e.message) + }) } - - }).catch(e => log.error('PuppetWeb', 'onServerLogin co rejected: %s', e)) + }).catch(e => { + log.error('PuppetWeb', 'onServerLogin() exception: %s', e.message) + }) } onServerLogout(data) { if (this.user) { this.emit('logout', this.user) this.user = null - } else { log.verbose('PuppetWeb', 'onServerLogout without this.user. still not logined?') } - // this.browser.cleanSession() + } else { log.verbose('PuppetWeb', 'onServerLogout() without this.user initialized') } + + if (this.session) { + this.browser.cleanSession(this.session) + .catch(e => { + log.warn('PuppetWeb', 'onServerLogout() browser.cleanSession() exception: %s', e.message) + }) + } } onServerMessage(data) { const m = new Message(data) @@ -266,53 +327,11 @@ class PuppetWeb extends Puppet { } else { log.warn('PuppetWeb', 'onServerMessage() without this.user') } - m.ready().then(() => this.emit('message', m)) - } - /** - * `unload` event is sent from js@browser to webserver via socketio - * after received `unload`, we re-inject the Wechaty js code into browser. - */ - onServerUnload(data) { - log.warn('PuppetWeb', 'server received unload event') - // this.onServerLogout(data) // XXX: should emit event[logout] from browser - - if (!this.browser) { - log.verbose('PuppetWeb', 'onServerUnload() found browser gone, should be quiting now') - return - } - - if (this.browser.dead()) { - log.warn('PuppetWeb', 'onServerUnload() found browser dead. wait deadguard to restore') - return - } - - if (!this.bridge) { - log.verbose('PuppetWeb', 'onServerUnload() found bridge gone, should be quiting now') - return - } - return process.nextTick(() => { - this.bridge.init() - .then(r => log.verbose('PuppetWeb', 'onServerUnload() bridge.re-init()ed:' + r)) - .catch(e => log.error('PuppetWeb', 'onServerUnload() err: ' + e.message)) + m.ready() // TODO: EventEmitter2 for video/audio/app/sys.... + .then(() => this.emit('message', m)) + .catch(e => { + log.error('PuppetWeb', 'onServerMessage() message ready exception: %s', e.message) }) - - // return this.quit() - // .then(this.init.bind(this)) - // .catch(e => { - // log.warn('PuppetWeb', 'onServerUnload fail: %s', e) - // throw e - // }) -/* - return this.browser.quit() - .then(r => log.verbose('PuppetWeb', 'browser.quit()ed:' + r)) - .then(r => this.browser.init()) - .then(r => log.verbose('PuppetWeb', 'browser.re-init()ed:' + r)) - .then(r => this.browser.open()) - .then(r => log.verbose('PuppetWeb', 'browser.re-open()ed:' + r)) - .then(r => this.bridge.init()) - .then(r => log.verbose('PuppetWeb', 'bridge.re-init()ed:' + r)) - .catch(e => log.error('PuppetWeb', 'onServerUnload() err: ' + e)) -*/ } send(message) { @@ -320,8 +339,8 @@ class PuppetWeb extends Puppet { const room = message.get('room') let content = message.get('content') - let destination = to + let destination = to if (room) { destination = room // if (to && to!==room) { @@ -331,33 +350,59 @@ class PuppetWeb extends Puppet { log.silly('PuppetWeb', `send(${destination}, ${content})`) return this.bridge.send(destination, content) + .catch(e => { + log.error('PuppetWeb', 'send() exception: %s', e.message) + throw e + }) } reply(message, replyContent) { if (message.self()) { - throw new Error('dont reply message send by myself') + return Promise.reject('will not to reply message of myself') } + const m = new Message() .set('content' , replyContent) .set('from' , message.obj.to) .set('to' , message.obj.from) .set('room' , message.obj.room) - // FIXME: find a alternate way to check a message create by `self` .set('self' , this.user.id) - log.verbose('PuppetWeb', 'reply() not sending message: %s', util.inspect(m)) + // log.verbose('PuppetWeb', 'reply() by message: %s', util.inspect(m)) return this.send(m) + .catch(e => { + log.error('PuppetWeb', 'reply() exception: %s', e.message) + throw e + }) } /** * logout from browser, then server will emit `logout` event */ - logout() { return this.bridge.logout() } + logout() { + return this.bridge.logout() + .catch(e => { + log.error('PuppetWeb', 'logout() exception: %s', e.message) + throw e + }) + } - getContact(id) { return this.bridge.getContact(id) } + getContact(id) { + return this.bridge.getContact(id) + .catch(e => { + log.error('PuppetWeb', 'getContact(%d) exception: %s', id, e.message) + throw e + }) + } logined() { return !!(this.user) } - + ding(data) { + return this.bridge.proxyWechaty('ding', data) + .catch(e => { + log.warn('PuppetWeb', 'ding(%s) rejected: %s', data, e.message) + throw e + }) + } } module.exports = PuppetWeb diff --git a/src/puppet.js b/src/puppet.js index df8990f7..fa74c07c 100644 --- a/src/puppet.js +++ b/src/puppet.js @@ -24,10 +24,11 @@ class Puppet extends EventEmitter { * @param message - the message to be sent * @return */ - send(message) { throw new Error('To Be Implemented') } + send(message) { throw new Error('To Be Implemented') } reply(message, reply) { throw new Error('To Be Implemented') } logout() { throw new Error('To Be Implementsd') } + quit() { throw new Error('To Be Implementsd') } ding() { throw new Error('To Be Implementsd') } getContact(id) { // for unit testing diff --git a/src/room.js b/src/room.js index 38c53a58..4c8e0495 100644 --- a/src/room.js +++ b/src/room.js @@ -22,7 +22,7 @@ class Room { toStringEx() { return `Room(${this.obj.name}[${this.id}])` } ready(contactGetter) { - log.silly('Room', `ready(${contactGetter})`) + log.silly('Room', 'ready(%s)', contactGetter ? contactGetter.constructor.name : '') if (!this.id) { log.warn('Room', 'ready() on a un-inited Room') return Promise.resolve(this) @@ -38,12 +38,13 @@ class Room { this.obj = this.parse(data) return this }).catch(e => { - log.error('Room', `contactGetter(${this.id}) rejected: ` + e) - throw new Error('contactGetter: ' + e) + log.error('Room', 'contactGetter(%s) exception: %s', this.id, e.message) + throw e }) } name() { return this.obj.name } + get(prop) { return this.obj[prop] } parse(rawObj) { return !rawObj ? {} : { @@ -61,7 +62,7 @@ class Room { return memberList.map(m => { return { id: m.UserName - , name: m.DisplayName + , name: m.DisplayName // nick name for this room? } }) } @@ -75,8 +76,6 @@ class Room { Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj[k]}`)) } - get(prop) { return this.obj[prop] } - static find() { } diff --git a/src/wechaty.js b/src/wechaty.js index 5179e0c1..8ad2388a 100644 --- a/src/wechaty.js +++ b/src/wechaty.js @@ -45,7 +45,7 @@ class Wechaty extends EventEmitter { return this // for chaining }).catch(e => { - log.error('Wechaty', 'init() be rejected: %s', e) + log.error('Wechaty', 'init() exception: %s', e.message) throw e }) } @@ -53,9 +53,9 @@ class Wechaty extends EventEmitter { switch (this.options.puppet) { case 'web': this.puppet = new Puppet.Web({ - head: this.options.head - , port: this.options.port - , session: this.options.session + head: this.options.head + , port: this.options.port + , session: this.options.session }) break default: @@ -64,12 +64,11 @@ class Wechaty extends EventEmitter { return Promise.resolve(this.puppet) } initEventHook() { - // scan qrCode this.puppet.on('scan', (e) => { - this.emit('scan', e) + this.emit('scan', e) // Scan QRCode }) this.puppet.on('message', (e) => { - this.emit('message', e) + this.emit('message', e) // Receive Message }) this.puppet.on('login', (e) => { this.emit('login', e) @@ -90,15 +89,42 @@ class Wechaty extends EventEmitter { return Promise.resolve() } - quit() { return this.puppet.quit() } - logout() { return this.puppet.logout() } + quit() { + return this.puppet.quit() + .catch(e => { + log.error('Wechaty', 'quit() exception: %s', e.message) + throw e + }) + } + logout() { + return this.puppet.logout() + .catch(e => { + log.error('Wechaty', 'logout() exception: %s', e.message) + throw e + }) - send(message) { return this.puppet.send(message) } - reply(message, reply) { return this.puppet.reply(message, reply) } + } - ding() { - // TODO: test through the server & browser - return 'dong' + send(message) { + return this.puppet.send(message) + .catch(e => { + log.error('Wechaty', 'send() exception: %s', e.message) + throw e + }) + } + reply(message, reply) { + return this.puppet.reply(message, reply) + .catch(e => { + log.error('Wechaty', 'reply() exception: %s', e.message) + throw e + }) + } + ding(data) { + return this.puppet.ding(data) + .catch(e => { + log.error('Wechaty', 'ding() exception: %s', e.message) + throw e + }) } } @@ -114,5 +140,5 @@ Object.assign(Wechaty, { /** * Expose `Wechaty`. */ -Wechaty.log = log +Wechaty.log = log // for convenionce use npmlog with environment variable LEVEL module.exports = Wechaty.default = Wechaty.Wechaty = Wechaty -- GitLab