提交 1fbb3e7b 编写于 作者: Huan (李卓桓)'s avatar Huan (李卓桓)

code clean & better error catch & log

上级 a3195954
......@@ -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.
......
......@@ -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() { }
}
......
......@@ -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]}`))
......
......@@ -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
......
......@@ -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)
})
})
......
......@@ -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
}
......
......@@ -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 // <Contact> 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
......@@ -24,10 +24,11 @@ class Puppet extends EventEmitter {
* @param <Message> message - the message to be sent
* @return <Promise>
*/
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
......
......@@ -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() {
}
......
......@@ -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
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册