diff --git a/README.md b/README.md index a96069619f60b41d2db70285f79d6d307b36955d..3611681eb5883d8c15000ef4885c295018115dda 100644 --- a/README.md +++ b/README.md @@ -16,22 +16,17 @@ Wechaty is a Bot Framework for Wechat **Personal** Account that help you easy cr # ATTENTION -## Wechaty is just converted to Typescript from Javascript. +## Wechaty was converted to Typescript from Javascript on 12th Oct 2016. Details: https://github.com/wechaty/wechaty/issues/40 ```diff -+ Typescripted ... -- Please do not clone/pull until you are really want to play with typescript wechaty in alpha stage. ++ Typescriptilized ... ``` VSCode is recommended for typescript because we can get benifit of [intelligent code completion, parameter info, and member lists](https://code.visualstudio.com/docs/languages/javascript). -The last Javascript version is: [v0.4.0](https://github.com/wechaty/wechaty/releases/tag/v0.4.0) (2016/10/9) - -Or install v0.4 via `npm install wechaty` - -Thanks. +The last Javascript version is: [v0.4.0](https://github.com/wechaty/wechaty/releases/tag/v0.4.0) (2016/10/9) , or install v0.4 by `npm install wechaty`. - See Also: [Why we decided to move from plain JavaScript to TypeScript for Babylon.js](https://www.eternalcoding.com/?p=103) @@ -713,8 +708,8 @@ npm test ## v0.5.0 master (2016/10) The First Typescript Version 1. [#40](https://github.com/wechaty/wechaty/issues/40) Converted to Typescript (2016/10/11) -1. [#41](https://github.com/wechaty/wechaty/issues/41) added `say()` method to Contact/Room instance, and to `this` inside wechaty event listeners, to make them `Sayable` -1. BREAKING CHANGE: global event `scan` arguments changed from 1 to 2: now is (url: string, code: number) instead of {url, code} before. +1. [#41](https://github.com/wechaty/wechaty/issues/41) Sayablization: Make Wechaty/Contact/Room `Sayable`, and all `this` inside wechaty event listeners are `Sayable` too. +1. BREAKING CHANGE: global event `scan` listener arguments changed from 1 to 2: now is `function(this: Sayable, url: string, code: number)` instead of `function({url, code})` before. ## [v0.4.0](https://github.com/wechaty/wechaty/releases/tag/v0.4.0) (2016/10/9) The Latest Javascript Version 1. [#32](https://github.com/wechaty/wechaty/issues/32) Extend Room Class with: @@ -724,9 +719,9 @@ npm test 1. Add/Del/Topic for Room 1. Other methods like nick/member/has/etc... 1. [#33](https://github.com/wechaty/wechaty/issues/33) New Class `FriendRequest` with: - 1. `Wechaty.on('friend', (contact, request) => {})` with Wechaty new Event `friend` - 1. `accept()` to accept a friend request - 1. `send()` to send new friend request + 1. `Wechaty.on('friend', function(contact: Contact, request: FriendRequest) {})` with Wechaty new Event `friend` + 1. `request.accept()` to accept a friend request + 1. `requestsend()` to send new friend request ## v0.3.13 (2016/09) 1. Managed by Cloud Manager: https://app.wechaty.io diff --git a/example/room-bot.ts b/example/room-bot.ts index eab84e01a84bb4b5ad6d493bcee524225a255783..9563e7e91f5c2f33dbb59d585207dfdd79db78ec 100644 --- a/example/room-bot.ts +++ b/example/room-bot.ts @@ -19,28 +19,20 @@ * * put name of one of your friend here, or create room will not work. * - * ::: ___CHANGE ME___ ::: - * vvvvvvvvvvvvvvv + * ::::::::: ___CHANGE ME___ ::::::::: + * vvvvvvvvv + * vvvvvvvvv + * vvvvvvvvv */ const HELPER_CONTACT_NAME = 'Bruce LEE' - - - - - - - - - - - - - - - - - +/** + * ^^^^^^^^^ + * ^^^^^^^^^ + * ^^^^^^^^^ + * ::::::::: ___CHANGE ME___ ::::::::: + * + */ import { Config @@ -312,7 +304,6 @@ function checkRoomJoin(room: Room, invitee: Contact|Contact[] , inviter: Contact , invitee ) - room.topic('ding - warn ' + inviter.name()) setTimeout(_ => { Array.isArray(invitee) diff --git a/src/config.ts b/src/config.ts index af568fe357d6a3386a4d2a6baf7f7fdb7cb4b0c6..9eeae34a1bcf45cd4d9ebe1663405f5b1dd93cb0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -144,4 +144,6 @@ export interface Sayable { say(content: string, replyTo?: any): Promise } +export * from './brolog-env' + export default Config diff --git a/src/puppet-web/browser.ts b/src/puppet-web/browser.ts index c8e899ab7b342b503e6d4913ac1224af630d1389..c7464d69b9d83137d1b9e861dd7b40cdc033fc92 100644 --- a/src/puppet-web/browser.ts +++ b/src/puppet-web/browser.ts @@ -29,8 +29,8 @@ type BrowserSetting = { class Browser extends EventEmitter { - private _targetState - private _currentState + private _targetState: string + private _currentState: string public driver: WebDriver @@ -54,7 +54,7 @@ class Browser extends EventEmitter { } // currentState : 'opening' | 'open' | 'closing' | 'close' - private currentState(newState?) { + public currentState(newState?) { if (newState) { log.verbose('PuppetWebBrowser', 'currentState(%s)', newState) this._currentState = newState @@ -264,32 +264,35 @@ class Browser extends EventEmitter { return driver } - public async quit(restart?: boolean): Promise { - log.verbose('PuppetWebBrowser', 'quit()') - - if (!restart) { - this.targetState('close') - this.currentState('closing') + public async restart(): Promise { + log.verbose('PuppetWebBrowser', 'restart()') + await this.quit() + if (this.targetState() !== 'open' + && this.currentState() !== 'opening') { + await this.init() } + } - // this.live = false + public async quit(): Promise { + log.verbose('PuppetWebBrowser', 'quit()') + + this.targetState('close') + this.currentState('closing') if (!this.driver) { log.verbose('PuppetWebBrowser', 'driver.quit() skipped because no driver') this.currentState('close') - return Promise.resolve('no driver') + return 'no driver' } else if (!this.driver.getSession()) { this.driver = null log.verbose('PuppetWebBrowser', 'driver.quit() skipped because no driver session') this.currentState('close') - return Promise.resolve('no driver session') + return 'no driver session' } - // return co.call(this, function* () { try { - log.silly('PuppetWebBrowser', 'quit() co()') await this.driver.close() // http://stackoverflow.com/a/32341885/1123955 log.silly('PuppetWebBrowser', 'quit() driver.close()-ed') await this.driver.quit() @@ -304,10 +307,8 @@ class Browser extends EventEmitter { * */ await this.clean() - - this.currentState('close') log.silly('PuppetWebBrowser', 'quit() co() end') - // }).catch(e => { + } catch (e) { // console.log(e) // log.warn('PuppetWebBrowser', 'err: %s %s %s %s', e.code, e.errno, e.syscall, e.message) @@ -323,12 +324,11 @@ class Browser extends EventEmitter { if (crashRegex.test(e.message)) { log.warn('PuppetWebBrowser', 'driver.quit() browser crashed') } else { log.warn('PuppetWebBrowser', 'driver.quit() exception: %s', e.message) } - // XXX fail safe to `close` ? - this.currentState('close') - /* fail safe */ } + this.currentState('close') + return } @@ -494,61 +494,58 @@ class Browser extends EventEmitter { * check whether browser is full functional * */ - public readyLive(): Promise { + public async readyLive(): Promise { log.verbose('PuppetWebBrowser', 'readyLive()') + if (this.dead()) { - return Promise.reject(new Error('this.dead() true')) + log.silly('PuppetWebBrowser', 'readyLive() dead() is true') + return false } - return new Promise((resolve, reject) => { - this.execute('return 1+1') - .then(r => { - if (r === 2) { - resolve(true) // browser ok, living - return - } - const errMsg = 'deadEx() found dead browser coz 1+1 = ' + r + ' (not 2)' - log.verbose('PuppetWebBrowser', errMsg) - this.dead(errMsg) - reject(new Error(errMsg)) // browser not ok, dead - return - }) - .catch(e => { - const errMsg = 'deadEx() found dead browser coz 1+1 = ' + e.message - log.verbose('PuppetWebBrowser', errMsg) - this.dead(errMsg) - reject(new Error(errMsg)) // browser not live - return - }) - }) + + let two + + try { + two = await this.execute('return 1+1') + } catch (e) { + two = e && e.message + } + + if (two === 2) { + return true // browser ok, living + } + + const errMsg = 'found dead browser coz 1+1 = ' + two + ' (not 2)' + log.warn('PuppetWebBrowser', 'readyLive() %s', errMsg) + this.dead(errMsg) + return false // browser not ok, dead } - public dead(forceReason?) { - let errMsg + public dead(forceReason?: string): boolean { + log.verbose('PuppetWebBrowser', 'dead(%s)', forceReason ? forceReason : '') + + let msg let dead = false if (forceReason) { dead = true - errMsg = forceReason - // } else if (!this.live) { + msg = forceReason } else if (this.targetState() !== 'open') { dead = true - // errMsg = 'browser not live' - errMsg = 'targetState not open' + msg = 'targetState not open' } else if (!this.driver || !this.driver.getSession()) { dead = true - errMsg = 'no driver or session' + msg = 'no driver or session' } if (dead) { - log.warn('PuppetWebBrowser', 'dead() because %s', errMsg) - // this.live = false + log.warn('PuppetWebBrowser', 'dead() because %s', msg) this.currentState('closing') - this.quit().then(_ => this.currentState('close')) + this.restart().then(_ => this.currentState('close')) // must use nextTick here, or promise will hang... 2016/6/10 - process.nextTick(_ => { - log.verbose('PuppetWebBrowser', 'dead() emit a `dead` event because %s', errMsg) - this.emit('dead', errMsg) + setImmediate(_ => { + log.verbose('PuppetWebBrowser', 'dead() emit a `dead` event because %s', msg) + this.emit('dead', msg) }) } return dead diff --git a/src/puppet-web/event.ts b/src/puppet-web/event.ts index c43ad8bbcef28f77fbb88b613e75221576652abb..6f5cc166d7c64c9520cf5af3e3c2b41e701c4ab7 100644 --- a/src/puppet-web/event.ts +++ b/src/puppet-web/event.ts @@ -47,7 +47,7 @@ const PuppetWebEvent = { , onServerMessage } -async function onBrowserDead(e): Promise { +async function onBrowserDead(this: PuppetWeb, e): Promise { log.verbose('PuppetWebEvent', 'onBrowserDead(%s)', e && e.message || e) // because this function is async, so maybe entry more than one times. // guard by variable: isBrowserBirthing to prevent the 2nd time entrance. @@ -88,9 +88,9 @@ async function onBrowserDead(e): Promise { log.verbose('PuppetWebEvent', 'onBrowserDead() try to reborn browser') - await this.browser.quit(true) - .catch(error => { // fail safe - log.warn('PuppetWebEvent', 'browser.quit() exception: %s, %s', error.message, error.stack) + await this.browser.restart() + .catch((err: Error) => { // fail safe + log.warn('PuppetWebEvent', 'browser.quit() exception: %s', err.stack) }) log.verbose('PuppetWebEvent', 'onBrowserDead() old browser quited') @@ -135,7 +135,7 @@ async function onBrowserDead(e): Promise { return } -function onServerDing(data) { +function onServerDing(this: PuppetWeb, data) { log.silly('PuppetWebEvent', 'onServerDing(%s)', data) // this.watchDog(data) this.emit('watchdog', { data }) @@ -172,8 +172,8 @@ function onServerConnection(data) { log.verbose('PuppetWebEvent', 'onServerConnection: %s', data) } -function onServerDisconnect(data) { - log.verbose('PuppetWebEvent', 'onServerDisconnect: %s', data) +async function onServerDisconnect(this: PuppetWeb, data): Promise { + log.verbose('PuppetWebEvent', 'onServerDisconnect(%s)', data) if (this.userId) { log.verbose('PuppetWebEvent', 'onServerDisconnect() there has userId set. emit a logout event and set userId to null') @@ -206,31 +206,33 @@ function onServerDisconnect(data) { return } - this.browser.readyLive() - .then(r => { // browser is alive, and we have a bridge to it - log.verbose('PuppetWebEvent', 'onServerDisconnect() re-initing bridge') - // must use setTimeout to wait a while. - // because the browser has just refreshed, need some time to re-init to be ready. - // if the browser is not ready, bridge init will fail, - // caused browser dead and have to be restarted. 2016/6/12 - setTimeout(_ => { - if (!this.bridge) { - // XXX: sometimes this.bridge gone in this timeout. why? - // what's happend between the last if(!this.bridge) check and the timeout call? - throw new Error('bridge gone after setTimeout? why???') - } - this.bridge.init() - .then(ret => { - log.verbose('PuppetWebEvent', 'onServerDisconnect() bridge re-inited: %s', ret) - }) - .catch(e => log.error('PuppetWebEvent', 'onServerDisconnect() exception: [%s]', e)) - }, 1000) // 1 second instead of 10 seconds? try. (should be enough to wait) - return - }) - .catch(e => { // browser is in indeed dead, or almost dead. readyLive() will auto recover itself. - log.verbose('PuppetWebEvent', 'onServerDisconnect() browser dead, waiting it recover itself: %s', e.message) + const live = await this.browser.readyLive() + + if (!live) { // browser is in indeed dead, or almost dead. readyLive() will auto recover itself. + log.verbose('PuppetWebEvent', 'onServerDisconnect() browser dead after readyLive() check. waiting it recover itself') return - }) + } + + // browser is alive, and we have a bridge to it + log.verbose('PuppetWebEvent', 'onServerDisconnect() re-initing bridge') + // must use setTimeout to wait a while. + // because the browser has just refreshed, need some time to re-init to be ready. + // if the browser is not ready, bridge init will fail, + // caused browser dead and have to be restarted. 2016/6/12 + setTimeout(_ => { + if (!this.bridge) { + // XXX: sometimes this.bridge gone in this timeout. why? + // what's happend between the last if(!this.bridge) check and the timeout call? + const e = new Error('bridge gone after setTimeout? why???') + log.warn('PuppetWebEvent', 'onServerDisconnect() setTimeout() %s', e.message) + throw e + } + this.bridge.init() + .then(ret => log.verbose('PuppetWebEvent', 'onServerDisconnect() setTimeout() bridge re-inited: %s', ret)) + .catch(e => log.error('PuppetWebEvent', 'onServerDisconnect() setTimeout() exception: [%s]', e)) + }, 1000) // 1 second instead of 10 seconds? try. (should be enough to wait) + return + } /** @@ -246,7 +248,7 @@ function onServerDisconnect(data) { * 3. browser quit(crash?) * 4. ... */ -function onServerUnload(data) { +function onServerUnload(this: PuppetWeb, data) { log.warn('PuppetWebEvent', 'onServerUnload(%s)', data) // onServerLogout.call(this, data) // XXX: should emit event[logout] from browser @@ -286,7 +288,7 @@ function onServerLog(data) { log.silly('PuppetWebEvent', 'onServerLog(%s)', data) } -async function onServerLogin(data, attempt = 0): Promise { +async function onServerLogin(this: PuppetWeb, data, attempt = 0): Promise { log.verbose('PuppetWebEvent', 'onServerLogin(%s, %d)', data, attempt) this.scan = null @@ -332,7 +334,7 @@ async function onServerLogin(data, attempt = 0): Promise { return } -function onServerLogout(data) { +function onServerLogout(this: PuppetWeb, data) { this.emit('logout', this.user || this.userId) if (!this.user && !this.userId) { @@ -348,7 +350,7 @@ function onServerLogout(data) { // }) } -async function onServerMessage(data): Promise { +async function onServerMessage(this: PuppetWeb, data): Promise { let m = new Message(data) // co.call(this, function* () { diff --git a/src/puppet-web/puppet-web.ts b/src/puppet-web/puppet-web.ts index 7bb429a9132f1c7a5a0677f17b08ee02b950a27b..e14db85d65f202d07ba12cb50c91851fbd516a5e 100644 --- a/src/puppet-web/puppet-web.ts +++ b/src/puppet-web/puppet-web.ts @@ -171,7 +171,7 @@ export class PuppetWeb extends Puppet { // }) } - private async initBrowser(): Promise { + public async initBrowser(): Promise { log.verbose('PuppetWeb', 'initBrowser()') const browser = new Browser({ head: this.setting.head @@ -199,7 +199,7 @@ export class PuppetWeb extends Puppet { return browser // follow func name meaning } - private initBridge(): Promise { + public initBridge(): Promise { log.verbose('PuppetWeb', 'initBridge()') const bridge = new Bridge( this // use puppet instead of browser, is because browser might change(die) duaring run time @@ -358,7 +358,7 @@ export class PuppetWeb extends Puppet { }) } public logined() { return !!(this.user) } - public ding(data: any): Promise { + public ding(data?: any): Promise { if (!this.bridge) { return Promise.reject(new Error('ding fail: no bridge(yet)!')) } diff --git a/src/puppet-web/server.ts b/src/puppet-web/server.ts index a108445312189544787517f9fc89df3980950403..1ca3ed19a3cc9aee484af3bc522b348b121ffd98 100644 --- a/src/puppet-web/server.ts +++ b/src/puppet-web/server.ts @@ -130,7 +130,7 @@ class Server extends EventEmitter { this.emit('connection', client) client.on('disconnect', e => { - log.silly('PuppetWebServer', 'socket.io disconnect: %s', e) + log.silly('PuppetWebServer', 'initEventsFromClient() on(discohnnect) socket.io disconnect: %s', e) // 1. Browser reload / 2. Lost connection(Bad network) this.socketClient = undefined this.emit('disconnect', e) @@ -138,7 +138,7 @@ class Server extends EventEmitter { client.on('error' , e => { // log.error('PuppetWebServer', 'initEventsFromClient() client on error: %s', e) - log.error('PuppetWebServer', 'initEventsFromClient() client on error: %s', e.stack) + log.error('PuppetWebServer', 'initEventsFromClient() on(error): %s', e.stack) }) // Events from Wechaty@Broswer --to--> Server diff --git a/src/state-monitor.ts b/src/state-monitor.ts index 380b64dc8ef6ec3be30131f519cd8de7e0971849..1ae94eb87ca03b772b47328bd363228bd0df857a 100644 --- a/src/state-monitor.ts +++ b/src/state-monitor.ts @@ -10,6 +10,7 @@ * * Helper Class for Manage State Change */ +import { log } from './config' /** * A - State A @@ -21,7 +22,9 @@ export class StateMonitor { private _current: A|B private _stable: boolean - constructor(initState: A|B) { + constructor(private client: string, initState: A|B) { + log.verbose('StateMonitor', 'constructor(%s, %s)', client, initState) + this.target(initState) this.current(initState) this.stable(true) @@ -29,22 +32,42 @@ export class StateMonitor { public target(newState?: A|B): A|B { if (newState) { + log.verbose('StateMonitor', 'target(%s) %s state change from %s to %s' + , this.client, newState + , this._target, newState + ) this._target = newState + } else { + log.verbose('StateMonitor', 'target() %s state is %s', this.client, this._target) } return this._target } public current(newState?: A|B, stable = true): A|B { if (newState) { + log.verbose('StateMonitor', 'current(%s, %s) %s state change from %s:%s to %s:%s' + , newState, stable + , this.client + , this._current, this._stable + , newState, stable + ) this._current = newState - this.stable(stable) + this._stable = stable + } else { + log.verbose('StateMonitor', 'current() %s state is %s', this.client, this._current) } return this._current } - public stable(isStable?: boolean) { - if (typeof isStable === 'boolean') { - this._stable = isStable + public stable(stable?: boolean) { + if (typeof stable === 'boolean') { + log.verbose('StateMonitor', 'stable(%s) %s state change from %s to %s' + , stable + , this.client + , this._stable, stable) + this._stable = stable + } else { + log.verbose('StateMonitor', 'stable() %s state is %s', this.client, this._stable) } return this._stable }