From 99ca26f9efaba4720fb33fcda245b3767b7c163b Mon Sep 17 00:00:00 2001 From: "Zhuohuan LI (CARPE DIEM)" Date: Sat, 1 Oct 2016 21:26:04 +0800 Subject: [PATCH] #32 group add/del --- doc/webwxapp.js | 6 +- package.json | 1 + src/puppet-web/bridge.js | 36 +++++- src/puppet-web/browser.js | 33 +++--- src/puppet-web/puppet-web.js | 66 ++++++++++- src/puppet-web/wechaty-bro.js | 217 +++++++++++++++++++++------------- src/room.js | 166 +++++++++++++++++++++----- test/room.spec.js | 8 -- 8 files changed, 396 insertions(+), 137 deletions(-) diff --git a/doc/webwxapp.js b/doc/webwxapp.js index aa8367cf..9a6e08c8 100644 --- a/doc/webwxapp.js +++ b/doc/webwxapp.js @@ -3809,7 +3809,11 @@ angular.module("Services", []), }).error(function() {}), i.promise }, - pickContacts: function(e, t, o) { + pickContacts: function( + e: string[] + , t: {} filter + , o: boolean? + ) { for (var n, r, a, i = [], c = this, s = t.all || {}, l = 0; l < e.length; l++) switch (n = e[l], a = t[n] || {}, diff --git a/package.json b/package.json index 9ff28858..ade12481 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "testdev": "cross-env LC_ALL=C WECHATY_LOG=silly ava --serial --verbose --fail-fast --timeout=3m", "ava": "cross-env LC_ALL=C WECHATY_LOG=verbose ava \"{src,test}/**/*.spec.js\"", "start": "node bin/client", + "dev": "node t.js", "demo": "node example/ding-dong-bot.js" }, "repository": { diff --git a/src/puppet-web/bridge.js b/src/puppet-web/bridge.js index fb8ae011..69c64fb4 100644 --- a/src/puppet-web/bridge.js +++ b/src/puppet-web/bridge.js @@ -108,9 +108,9 @@ class Bridge { /** * Do not insert `return` in front of the code. - * because the new line `\n` will cause return nothing at all + * because the new line `\n` will cause return nothing at all */ - return 'rejectioReturnValue = ' + return 'rejectioReturnValue = ' + fs.readFileSync( path.join(__dirname, 'wechaty-bro.js') , 'utf8' @@ -143,6 +143,38 @@ class Bridge { }) } + roomFind(filterFunction) { + return this.proxyWechaty('roomFind', filterFunction) + .catch(e => { + log.error('PuppetWebBridge', 'roomFind() exception: %s', e.message) + throw e + }) + } + + roomDelMember(roomId, contactId) { + return this.proxyWechaty('roomDelMember', roomId, contactId) + .catch(e => { + log.error('PuppetWebBridge', 'roomDelMember(%s, %s) exception: %s', roomId, contactId, e.message) + throw e + }) + } + + roomAddMember(roomId, contactId) { + return this.proxyWechaty('roomAddMember', roomId, contactId) + .catch(e => { + log.error('PuppetWebBridge', 'roomAddMember(%s, %s) exception: %s', roomId, contactId, e.message) + throw e + }) + } + + roomCreate(contactIdList) { + return this.proxyWechaty('roomCreate', contactIdList) + .catch(e => { + log.error('PuppetWebBridge', 'roomCreate(%s) exception: %s', contactIdList, e.message) + throw e + }) + } + send(toUserName, content) { return this.proxyWechaty('send', toUserName, content) .catch(e => { diff --git a/src/puppet-web/browser.js b/src/puppet-web/browser.js index cb19c029..6cfc23cd 100644 --- a/src/puppet-web/browser.js +++ b/src/puppet-web/browser.js @@ -33,7 +33,7 @@ class Browser extends EventEmitter { this.sessionFile = sessionFile // a file to save session cookies // this.live = false - + this.targetState('close') this.currentState('close') } @@ -79,9 +79,9 @@ class Browser extends EventEmitter { /** * XXX - * + * * when open url, there could happen a quit() call. - * should check here: if we are in `close` target state, we should clean up + * should check here: if we are in `close` target state, we should clean up */ if (this.targetState() === 'open') { this.currentState('open') @@ -174,7 +174,7 @@ class Browser extends EventEmitter { .withCapabilities(customChrome) .build() } - + getPhantomJsDriver() { // setup custom phantomJS capability https://github.com/SeleniumHQ/selenium/issues/2069 const phantomjsExe = require('phantomjs-prebuilt').path @@ -187,9 +187,9 @@ class Browser extends EventEmitter { , '--ssl-protocol=any' // http://stackoverflow.com/a/26503588/1123955 //, '--ssl-protocol=TLSv1' // https://github.com/ariya/phantomjs/issues/11239#issuecomment-42362211 - // issue: Secure WebSocket(wss) do not work with Self Signed Certificate in PhantomJS #12 + // issue: Secure WebSocket(wss) do not work with Self Signed Certificate in PhantomJS #12 // , '--ssl-certificates-path=D:\\cygwin64\\home\\zixia\\git\\wechaty' // http://stackoverflow.com/a/32690349/1123955 - // , '--ssl-client-certificate-file=cert.pem' // + // , '--ssl-client-certificate-file=cert.pem' // ] if (process.env.WECHATY_DEBUG) { @@ -215,7 +215,7 @@ class Browser extends EventEmitter { const driver = new WebDriver.Builder() .withCapabilities(customPhantom) .build() - + /** * FIXME: ISSUE #21 - https://github.com/zixia/wechaty/issues/21 * @@ -237,7 +237,7 @@ this.onResourceRequested = function(request, net) { // https://github.com/detro/ghostdriver/blob/f976007a431e634a3ca981eea743a2686ebed38e/src/session.js#L233 // driver.manage().timeouts().pageLoadTimeout(2000) - + return driver } @@ -270,10 +270,10 @@ this.onResourceRequested = function(request, net) { log.silly('PuppetWebBrowser', 'quit() this.driver = null') /** - * - * if we use AVA to test, then this.clean will cause problems + * + * if we use AVA to test, then this.clean will cause problems * because there will be more than one instance of browser with the same nodejs process id - * + * */ yield this.clean() @@ -335,7 +335,7 @@ this.onResourceRequested = function(request, net) { getBrowserPids() { log.silly('PuppetWebBrowser', 'getBrowserPids()') - + return new Promise((resolve, reject) => { require('ps-tree')(process.pid, (err, children) => { if (err) { @@ -415,6 +415,11 @@ this.onResourceRequested = function(request, net) { // log.verbose('PuppetWebBrowser', `Browser.execute() driver.getSession: %s`, util.inspect(this.driver.getSession())) if (this.dead()) { return Promise.reject(new Error('browser dead')) } +// XXX +console.log('#############') +console.log(script) +console.log(args) + return this.driver.executeScript.apply(this.driver, arguments) .catch(e => { // this.dead(e) @@ -478,7 +483,7 @@ this.onResourceRequested = function(request, net) { // this.live = false this.currentState('closing') this.quit().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) @@ -548,7 +553,7 @@ this.onResourceRequested = function(request, net) { else { return true } }) } - + return new Promise((resolve, reject) => { this.driver.manage().getCookies() .then(cookieFilter) diff --git a/src/puppet-web/puppet-web.js b/src/puppet-web/puppet-web.js index e1b5697c..1ea5071b 100644 --- a/src/puppet-web/puppet-web.js +++ b/src/puppet-web/puppet-web.js @@ -204,7 +204,7 @@ class PuppetWeb extends Puppet { log.warn('PuppetWeb', 'initBridge() found targetState != live, no init anymore') return Promise.resolve('skipped') } - + return bridge.init() .catch(e => { if (!this.browser){ @@ -229,9 +229,9 @@ class PuppetWeb extends Puppet { /** * @depreciated 20160825 zixia - * + * * when `unload` there should always be a `disconnect` event? - */ + */ // server.on('unload' , Event.onServerUnload.bind(this)) server.on('connection', Event.onServerConnection.bind(this)) @@ -322,7 +322,7 @@ class PuppetWeb extends Puppet { if (!this.bridge) { throw new Error('PuppetWeb has no bridge for getContact()') } - + return this.bridge.getContact(id) .catch(e => { log.error('PuppetWeb', 'getContact(%d) exception: %s', id, e.message) @@ -340,6 +340,64 @@ class PuppetWeb extends Puppet { throw e }) } + + roomFind(filterFunction) { + if (!this.bridge) { + return Promise.reject(new Error('findRoom fail: no bridge(yet)!')) + } + return this.bridge.roomFind(filterFunction) + .catch(e => { + log.warn('PuppetWeb', 'roomFind(%s) rejected: %s', filterFunction, e.message) + throw e + }) + } + + roomDelMember(room, contact) { + if (!this.bridge) { + return Promise.reject(new Error('roomDelMember fail: no bridge(yet)!')) + } + const roomId = room.get('id') + const contactId = contact.get('id') + return this.bridge.roomDelMember(roomId, contactId) + .catch(e => { + log.warn('PuppetWeb', 'roomDelMember(%s) rejected: %s', contact, e.message) + throw e + }) + } + + roomAddMember(room, contact) { + if (!this.bridge) { + return Promise.reject(new Error('fail: no bridge(yet)!')) + } + const roomId = room.get('id') + const contactId = contact.get('id') + return this.bridge.roomAddMember(roomId, contactId) + .catch(e => { + log.warn('PuppetWeb', 'roomAddMember(%s) rejected: %s', contact, e.message) + throw e + }) + + } + + roomCreate(contactList) { + if (!this.bridge) { + return Promise.reject(new Error('fail: no bridge(yet)!')) + } + + if (!contactList || ! typeof contactList === 'array') { + throw new Error('contactList not found') + } + + const contactIdList = contactList.map(c => c.get('id')) + + return this.bridge.roomCreate(contactIdList) + .catch(e => { + log.warn('PuppetWeb', 'roomCreate(%s) rejected: %s', contact, e.message) + throw e + }) + + } + } module.exports = PuppetWeb.default = PuppetWeb.PuppetWeb = PuppetWeb diff --git a/src/puppet-web/wechaty-bro.js b/src/puppet-web/wechaty-bro.js index 308b97e0..57056421 100644 --- a/src/puppet-web/wechaty-bro.js +++ b/src/puppet-web/wechaty-bro.js @@ -29,87 +29,6 @@ /*global angular*/ (function(port) { - port = port || 8788 - - /* - * WechatyBro injectio must return this object. - * PuppetWebBridge need this to decide if injection is successful. - */ - var retObj = { - code: 200 // 2XX ok, 4XX/5XX error. HTTP like - , message: 'any message' - , port: port - } - - if (typeof this.WechatyBro !== 'undefined') { - retObj.code = 201 - retObj.message = 'WechatyBro already injected?' - return retObj - } - - var WechatyBro = { - glue: { - // will be initialized by glueToAngular() function - } - - // glue funcs - // , getLoginStatusCode: function() { return WechatyBro.glue.loginScope.code } - // , getLoginQrImgUrl: function() { return WechatyBro.glue.loginScope.qrcodeUrl } - , angularIsReady: angularIsReady - - // variable - , vars: { - loginStatus: false - , initStatus: false - - , socket: null - , eventsBuf: [] - , scanCode: null - , heartBeatTimmer: null - } - - // funcs - , init: init // initialize WechatyBro @ Browser - , send: send // send message to wechat user - , clog: clog // log to Console - , slog: slog // log to SocketIO - , log: log // log to both Console & SocketIO - , ding: ding // simple return 'dong' - , quit: quit // quit wechat - , emit: emit // send event to server - , logout: logout // logout current logined user - - , getContact: getContact - , getUserName: getUserName - , getMsgImg: getMsgImg - - // test purpose - , isLogin: isLogin - , initClog: initClog - } - - this.WechatyBro = WechatyBro - retObj.code = 200 - retObj.message = 'WechatyBro Inject Succ' - return retObj - - /** - * Two return mode of WebDriver (should be one of them at a time) - * 1. a callback. return a value by call callback with args - * 2. direct return - */ - // var callback = arguments[arguments.length - 1] - // if (typeof callback === 'function') { - // return callback(retObj) - // } else { - // return retObj - // } - - retObj.code = 500 - retObj.message = 'SHOULD NOT RUN TO HERE' - return retObj - - ///////////////////////////////////////////////////////////////////////////// function init() { if (!angularIsReady()) { @@ -273,12 +192,14 @@ var accountFactory = injector.get('accountFactory') var appFactory = injector.get('appFactory') + var chatroomFactory = injector.get('chatroomFactory') var chatFactory = injector.get('chatFactory') var contactFactory = injector.get('contactFactory') var confFactory = injector.get('confFactory') var loginFactory = injector.get('loginFactory') var http = injector.get('$http') + var state = injector.get('$state') var mmHttp = injector.get('mmHttp') var appScope = angular.element('[ng-controller="appController"]').scope() @@ -323,8 +244,10 @@ WechatyBro.glue = { injector: injector , http: http + , state , accountFactory: accountFactory + , chatroomFactory , chatFactory: chatFactory , confFactory: confFactory , contactFactory: contactFactory @@ -528,4 +451,136 @@ : null } + function roomFind(filterFunction) { + var contactFactory = WechatyBro.glue.contactFactory + + var match + if (!filterFunction) { + match = function() { return true } + } else { + match = eval(filterFunction) + } + // log(match.toString()) + return contactFactory.getAllChatroomContact() + .filter(r => match(r.NickName)) + .map(r => r.UserName) + } + + function roomDelMember(ChatRoomName, UserName) { + const chatroomFactory = WechatyBro.glue.chatroomFactory + return chatroomFactory.delMember(ChatRoomName, UserName) + } + + function roomAddMember(ChatRoomName, UserName) { + const chatroomFactory = WechatyBro.glue.chatroomFactory + // XXX + log(ChatRoomName) + log(UserName) + return chatroomFactory.addMember(ChatRoomName, UserName) + } + + function roomCreate(UserNameList) { + const UserNameListArg = UserNameList.map(n => { return { UserName: n } }) + + const chatroomFactory = WechatyBro.glue.chatroomFactory + chatroomFactory.create(UserNameListArg) + .then(r => { + if (r.BaseResponse && 0 == r.BaseResponse.Ret || -2013 == e.BaseResponse.Ret) { + // be careful: key name is userName, not UserName! 20161001 + WechatyBro.glue.state.go('chat', { userName: r.ChatRoomName }) + } + }) + .catch(e => { + // TBD + console.log(e) + }) + return 'no callback (yet)' + } + ///////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////// + + port = port || 8788 + + /* + * WechatyBro injectio must return this object. + * PuppetWebBridge need this to decide if injection is successful. + */ + var retObj = { + code: 200 // 2XX ok, 4XX/5XX error. HTTP like + , message: 'any message' + , port: port + } + + if (typeof this.WechatyBro !== 'undefined') { + retObj.code = 201 + retObj.message = 'WechatyBro already injected?' + return retObj + } + + var WechatyBro = { + glue: { + // will be initialized by glueToAngular() function + } + + // glue funcs + // , getLoginStatusCode: function() { return WechatyBro.glue.loginScope.code } + // , getLoginQrImgUrl: function() { return WechatyBro.glue.loginScope.qrcodeUrl } + , angularIsReady: angularIsReady + + // variable + , vars: { + loginStatus: false + , initStatus: false + + , socket: null + , eventsBuf: [] + , scanCode: null + , heartBeatTimmer: null + } + + // funcs + , init: init // initialize WechatyBro @ Browser + , send: send // send message to wechat user + , clog: clog // log to Console + , slog: slog // log to SocketIO + , log: log // log to both Console & SocketIO + , ding: ding // simple return 'dong' + , quit: quit // quit wechat + , emit: emit // send event to server + , logout: logout // logout current logined user + + , getContact: getContact + , getUserName: getUserName + , getMsgImg: getMsgImg + + , roomFind + , roomCreate + , roomAddMember + , roomDelMember + + // test purpose + , isLogin: isLogin + , initClog: initClog + } + + this.WechatyBro = WechatyBro + retObj.code = 200 + retObj.message = 'WechatyBro Inject Succ' + + /** + * Two return mode of WebDriver (should be one of them at a time) + * 1. a callback. return a value by call callback with args + * 2. direct return + */ + var callback = arguments[arguments.length - 1] + if (typeof callback === 'function') { + return callback(retObj) + } + return retObj + + // retObj.code = 500 + // retObj.message = 'SHOULD NOT RUN TO HERE' + // return retObj + }.apply(window, arguments)) diff --git a/src/room.js b/src/room.js index 0594eb41..5275f6c8 100644 --- a/src/room.js +++ b/src/room.js @@ -22,15 +22,25 @@ class Room { toString() { return this.id } toStringEx() { return `Room(${this.obj.name}[${this.id}])` } + isReady() { + return this.obj.members && this.obj.members.length + } + + refresh() { + this.obj = {} + return this.ready() + } + 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) - } else if (this.obj.members && this.obj.members.length) { + const e = new Error('ready() on a un-inited Room') + log.warn('Room', e.message) + return Promise.reject(e) + } else if (this.isReady()) { return Promise.resolve(this) } else if (this.obj.id) { - log.warn('Room', 'ready() ready but members list empty in room %s. reloading', this.obj.name) + log.warn('Room', 'ready() has obj.id but members list empty in room %s. reloading', this.obj.name) } contactGetter = contactGetter || Room.puppet.getContact.bind(Room.puppet) @@ -50,7 +60,10 @@ class Room { get(prop) { return this.obj[prop] } parse(rawObj) { - return !rawObj ? {} : { + if (!rawObj) { + return {} + } + return { id: rawObj.UserName , encryId: rawObj.EncryChatRoomId // ??? , name: rawObj.NickName @@ -79,33 +92,132 @@ class Room { Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj[k]}`)) } - static find() { - return new Room('-1') + del(contact) { + log.verbose('Room', 'del(%s) from %s', contact, this) + + if (!contact) { + throw new Error('contact not found') + } + return Room.puppet.roomDelMember(this, contact) } - - static findAll() { - return [ - new Room('-2') - , new Room('-3') - ] + + quit() { + throw new Error('wx web not implement yet') + // WechatyBro.glue.chatroomFactory.quit("@@1c066dfcab4ef467cd0a8da8bec90880035aa46526c44f504a83172a9086a5f7" } -} -Room.init = function() { Room.pool = {} } -Room.init() -Room.load = function(id) { - if (!id) { return null } + add(contact) { + log.verbose('Room', 'add(%s) to %s', contact, this) + + if (!contact) { + throw new Error('contact not found') + } - if (id in Room.pool) { - return Room.pool[id] + return Room.puppet.roomAddMember(this, contact) } - return Room.pool[id] = new Room(id) -} -Room.attach = function(puppet) { - // if (!puppet) { - // throw new Error('Room.attach got no puppet to attach!') - // } - Room.puppet = puppet + + static create(contactList) { + log.verbose('Room', 'create(%s)', contactList.join(',')) + + if (!contactList || ! typeof contactList === 'array') { + throw new Error('contactList not found') + } + return Room.puppet.roomCreate(contactList) + } + + // private + static _find({ + name + }) { + log.silly('Room', '_find(%s)', name) + + if (!name) { + throw new Error('name not found') + } + + let filterFunction + if (name instanceof RegExp) { + filterFunction = `c => ${name.toString()}.test(c)` + } else if (typeof name === 'string') { + filterFunction = `c => c === ${name}` + } else { + throw new Error('unsupport name type') + } + + return Room.puppet.roomFind(filterFunction) + .then(idList => { + return idList + }) + .catch(e => { + log.error('Room', '_find() rejected: %s', e.message) + throw e + }) + } + + static find({ + name + }) { + log.verbose('Room', 'find(%s)', name) + + return Room._find({name}) + .then(idList => { + if (!idList || !Array.isArray(idList)){ + throw new Error('_find return error') + } + if (idList.length < 1) { + return null + } + const id = idList[0] + return Room.load(id) + }) + .catch(e => { + log.error('Room', 'find() rejected: %s', e.message) + return [] // fail safe + }) + } + + static findAll({ + name + }) { + log.verbose('Room', 'findAll(%s)', name) + + return Room._find({name}) + .then(idList => { + // console.log(idList) + if (!idList || !Array.isArray(idList)){ + throw new Error('_find return error') + } + if (idList.length < 1) { + return [] + } + return idList.map(i => Room.load(i)) + }) + .catch(e => { + log.error('Room', 'findAll() rejected: %s', e.message) + throw e + }) + } + + static init() { Room.pool = {} } + + static load(id) { + if (!id) { return null } + + if (id in Room.pool) { + return Room.pool[id] + } + return Room.pool[id] = new Room(id) + } + + static attach(puppet) { + // if (!puppet) { + // throw new Error('Room.attach got no puppet to attach!') + // } + Room.puppet = puppet + } + } +Room.init() + module.exports = Room diff --git a/test/room.spec.js b/test/room.spec.js index 29fd0b11..d4db56c5 100644 --- a/test/room.spec.js +++ b/test/room.spec.js @@ -9,14 +9,6 @@ import { Room.attach(new Puppet()) -// const test = require('tape') -// const Message = require('../src/message') -// const Room = require('../src/room') -// const Contact = require('../src/contact') -// const Puppet = require('../src/puppet') -// const log = require('../src/brolog-env') - - test('Room smoke testing', async t => { const UserName = '@0bb3e4dd746fdbd4a80546aef66f4085' -- GitLab