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

merge

sudo: required
dist: trusty
sudo: false
language: node_js
node_js:
......@@ -24,7 +23,7 @@ before_install:
install:
- if [ "$TRAVIS_OS_NAME" == 'osx' ]; then brew update; brew cleanup; brew cask cleanup; fi
- if [ "$TRAVIS_OS_NAME" == 'osx' ]; then brew uninstall --force brew-cask; brew update; fi
# - if [ "$TRAVIS_OS_NAME" == 'osx' ]; then brew uninstall --force brew-cask; brew update; fi
- if [ "$TRAVIS_OS_NAME" == 'osx' ]; then brew install moreutils; fi
- if [ "$TRAVIS_OS_NAME" == 'osx' ]; then brew install shellcheck; fi
- if [ "$TRAVIS_OS_NAME" == 'osx' ]; then brew install jq; fi
......@@ -34,8 +33,7 @@ script:
- echo $TRAVIS_OS_NAME
- node --version
- npm --version
- shellcheck bin/*.sh
- npm run test:linux
- npm run test
- npm run test:npm && echo 'Npm packing test is passed'
notifications:
......
......@@ -26,26 +26,40 @@ WECHATY CONTRIBUTORS
# Change Log
## [Unreleased](https://github.com/chatie/wechaty/tree/HEAD)
[Full Changelog](https://github.com/chatie/wechaty/compare/v0.9.0...HEAD)
## [v0.12.0](https://github.com/chatie/wechaty/tree/v0.12.0) (2017-10-30)
[Full Changelog](https://github.com/chatie/wechaty/compare/v0.9.0...v0.12.0)
**Implemented enhancements:**
- Promote `rx-queue` to SOLO NPM Module [\#938](https://github.com/Chatie/wechaty/issues/938)
- Add Wechaty.logonoff\(\) method [\#926](https://github.com/Chatie/wechaty/issues/926)
- Registe Wechaty Listeners with a try {} catch {} block in order to prevent listener function crash the framework. [\#878](https://github.com/Chatie/wechaty/issues/878)
- Upgrade CircleCI from 1.0 to 2.0 [\#863](https://github.com/Chatie/wechaty/issues/863)
- Switch Docker Node Image from `alphin` to official `node:7` [\#862](https://github.com/Chatie/wechaty/issues/862)
- Click Web Wechat `Switch Account` Automatically to get qrcode immediately when bot logout [\#636](https://github.com/Chatie/wechaty/issues/636)
- Upgrade docker image from Node.js v7 to v8 [\#577](https://github.com/Chatie/wechaty/issues/577)
- \[todo\] switch unit test tool from AVA to TAPE [\#513](https://github.com/Chatie/wechaty/issues/513)
- \[ci\] Provide a Mock PuppetWeb Instance for Integration Test [\#237](https://github.com/Chatie/wechaty/issues/237)
**Fixed bugs:**
- \[CI\] Homebrew must be run under Ruby 2.3! You're running 2.0.0. \(RuntimeError\) [\#936](https://github.com/Chatie/wechaty/issues/936)
- "PromiseRejectionHandledWarning: Promise rejection was handled asynchronously" when Wechat says "当前登录环境异常" [\#925](https://github.com/Chatie/wechaty/issues/925)
- TypeError: cookieList.filter is not a function [\#919](https://github.com/Chatie/wechaty/issues/919)
- TypeError: Cannot read property 'error' of null [\#918](https://github.com/Chatie/wechaty/issues/918)
- ERR PuppetWebBridge init\(\) initPage\(\) onLoad\(\) exception: undefined [\#917](https://github.com/Chatie/wechaty/issues/917)
- Sometimes Wechaty can't login \(with puppeteer\) [\#899](https://github.com/Chatie/wechaty/issues/899)
- \[ci\] WebDriver Error: "no such session" [\#756](https://github.com/Chatie/wechaty/issues/756)
- Click Web Wechat `Switch Account` Automatically to get qrcode immediately when bot logout [\#636](https://github.com/Chatie/wechaty/issues/636)
- \[ci\] execute proxyWechaty\(init\) error: 503, init\(\) without a ready angular env [\#329](https://github.com/Chatie/wechaty/issues/329)
- \[ci log\] watchdog reset after 120 seconds [\#195](https://github.com/Chatie/wechaty/issues/195)
- Selenium WebDriver driver.getSession\(\) wait a long time [\#86](https://github.com/Chatie/wechaty/issues/86)
**Closed issues:**
- 在登录失败时的异常提示优化 [\#898](https://github.com/Chatie/wechaty/issues/898)
- CANT RUN THE WECHATY-GETTING-STARTED, PUPPETWEBROWSER valid\(\) getSession\(\) [\#891](https://github.com/Chatie/wechaty/issues/891)
- Error after restart vps invalid driver at ttl 0 [\#788](https://github.com/Chatie/wechaty/issues/788)
- webdriver.executeScript wait a long time\(26s\) before page load [\#2](https://github.com/Chatie/wechaty/issues/2)
**Merged pull requests:**
......@@ -65,7 +79,6 @@ WECHATY CONTRIBUTORS
- chrome-headless support [\#739](https://github.com/Chatie/wechaty/issues/739)
- Add Transpond Message [\#726](https://github.com/Chatie/wechaty/issues/726)
- Cannot send pdf file using MediaMessage [\#710](https://github.com/Chatie/wechaty/issues/710)
- Click Web Wechat `Switch Account` Automatically to get qrcode immediately when bot logout [\#636](https://github.com/Chatie/wechaty/issues/636)
- Use Sentry.io to report exceptions [\#580](https://github.com/Chatie/wechaty/issues/580)
- \[enhancement\] Add pdf, docx etc support to MediaMessage\(now only picture is supported\) [\#538](https://github.com/Chatie/wechaty/issues/538)
- use babel-node to run javascript\(.js\) file inside docker [\#507](https://github.com/Chatie/wechaty/issues/507)
......@@ -88,7 +101,6 @@ WECHATY CONTRIBUTORS
- \[tslint\] stuck on v5.3 and can not upgrade [\#762](https://github.com/Chatie/wechaty/issues/762)
- Cannot send pdf file using MediaMessage [\#710](https://github.com/Chatie/wechaty/issues/710)
- CI, green keeper and package-lock under npm 5 [\#656](https://github.com/Chatie/wechaty/issues/656)
- Click Web Wechat `Switch Account` Automatically to get qrcode immediately when bot logout [\#636](https://github.com/Chatie/wechaty/issues/636)
- watchDogReset\(\) watchdog reset after 60 seconds \(phantomjs head\) [\#633](https://github.com/Chatie/wechaty/issues/633)
- \[test\] Unit Test for `mentioned` feature does not run at all [\#623](https://github.com/Chatie/wechaty/issues/623)
- error TS2345: Argument of type 'string | MemberQueryFilter' is not assignable to parameter of type 'MemberQueryFilter' [\#622](https://github.com/Chatie/wechaty/issues/622)
......@@ -123,12 +135,9 @@ WECHATY CONTRIBUTORS
**Closed issues:**
- vscode setting config error [\#843](https://github.com/Chatie/wechaty/issues/843)
- An in-range update of sinon-test is breaking the build 🚨 [\#814](https://github.com/Chatie/wechaty/issues/814)
- add static method `Message.findAll\(\)` [\#765](https://github.com/Chatie/wechaty/issues/765)
- cannot find Chrome binary [\#746](https://github.com/Chatie/wechaty/issues/746)
- UnhandledPromiseRejectionWarning: Unhandled promise rejection \(rejection id: 2\): Error: no puppet instance [\#738](https://github.com/Chatie/wechaty/issues/738)
- An in-range update of @types/glob is breaking the build 🚨 [\#734](https://github.com/Chatie/wechaty/issues/734)
- An in-range update of phantomjs-prebuilt is breaking the build 🚨 [\#730](https://github.com/Chatie/wechaty/issues/730)
- MediaMessage.filename\(\) cannot get correct img name. [\#722](https://github.com/Chatie/wechaty/issues/722)
- MediaMessage.ext\(\) cannot work as expect [\#721](https://github.com/Chatie/wechaty/issues/721)
- the latest docker version 139 cannot send file [\#720](https://github.com/Chatie/wechaty/issues/720)
......@@ -154,7 +163,6 @@ WECHATY CONTRIBUTORS
- some strange session error [\#523](https://github.com/Chatie/wechaty/issues/523)
- static Contact.find\(\) / static Contact.findAll\(\) throws exception [\#520](https://github.com/Chatie/wechaty/issues/520)
- Cannot set alias of Contact Object getting from `message.from\(\)` method when Contact is not a friend [\#509](https://github.com/Chatie/wechaty/issues/509)
- An in-range update of brolog is breaking the build 🚨 [\#499](https://github.com/Chatie/wechaty/issues/499)
- room.member\(\) can not return right result [\#437](https://github.com/Chatie/wechaty/issues/437)
- windows run program send images throw out error [\#427](https://github.com/Chatie/wechaty/issues/427)
- group names have HTML in them [\#382](https://github.com/Chatie/wechaty/issues/382)
......@@ -230,11 +238,8 @@ WECHATY CONTRIBUTORS
**Closed issues:**
- An in-range update of state-switch is breaking the build 🚨 [\#468](https://github.com/Chatie/wechaty/issues/468)
- An in-range update of state-switch is breaking the build 🚨 [\#467](https://github.com/Chatie/wechaty/issues/467)
- Always getSession timeout [\#463](https://github.com/Chatie/wechaty/issues/463)
- how to create more bots at once [\#460](https://github.com/Chatie/wechaty/issues/460)
- An in-range update of bl is breaking the build 🚨 [\#459](https://github.com/Chatie/wechaty/issues/459)
- how do we get avatar link? [\#424](https://github.com/Chatie/wechaty/issues/424)
- can't run the example [\#423](https://github.com/Chatie/wechaty/issues/423)
- 有没有查找好友的方法? [\#411](https://github.com/Chatie/wechaty/issues/411)
......@@ -243,7 +248,6 @@ WECHATY CONTRIBUTORS
- cannot remark friend in centos system [\#406](https://github.com/Chatie/wechaty/issues/406)
- MediaMessage in ding-dong-bot example can not be create [\#399](https://github.com/Chatie/wechaty/issues/399)
- wechaty can auto receive money\(red envolop/transfer\) on account. [\#398](https://github.com/Chatie/wechaty/issues/398)
- An in-range update of chromedriver is breaking the build 🚨 [\#395](https://github.com/Chatie/wechaty/issues/395)
- \[bug\] room.say\(\) return contact's alias when bot set alias for some one [\#394](https://github.com/Chatie/wechaty/issues/394)
- `Room.fresh\(\)`not work; `Room.alias\(\)`returns null [\#391](https://github.com/Chatie/wechaty/issues/391)
- should add`phantomjs-prebuilt` in package.json [\#385](https://github.com/Chatie/wechaty/issues/385)
......@@ -338,13 +342,7 @@ WECHATY CONTRIBUTORS
- too many levels of symbolic links [\#165](https://github.com/Chatie/wechaty/issues/165)
- node dist/example/ding-dong-bot.js example运行异常 [\#159](https://github.com/Chatie/wechaty/issues/159)
- An in-range update of tslint is breaking the build 🚨 [\#157](https://github.com/Chatie/wechaty/issues/157)
- deploying to server problems \(running headless\) [\#154](https://github.com/Chatie/wechaty/issues/154)
- An in-range update of @types/selenium-webdriver is breaking the build 🚨 [\#148](https://github.com/Chatie/wechaty/issues/148)
- An in-range update of tslint is breaking the build 🚨 [\#144](https://github.com/Chatie/wechaty/issues/144)
- An in-range update of tslint is breaking the build 🚨 [\#140](https://github.com/Chatie/wechaty/issues/140)
- An in-range update of @types/node is breaking the build 🚨 [\#137](https://github.com/Chatie/wechaty/issues/137)
- An in-range update of @types/sinon is breaking the build 🚨 [\#135](https://github.com/Chatie/wechaty/issues/135)
- wechaty mybot.js start error [\#126](https://github.com/Chatie/wechaty/issues/126)
- Room-join' para inviteeList\[\] cannot always work well when contain emoji [\#125](https://github.com/Chatie/wechaty/issues/125)
- \[help\] install wechaty and its types [\#124](https://github.com/Chatie/wechaty/issues/124)
......
FROM node:7
FROM ubuntu:17.10
LABEL maintainer="Huan LI <zixia@zixia.net>"
ENV NPM_CONFIG_LOGLEVEL warn
......@@ -21,7 +21,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ttf-freefont \
vim \
wget \
&& rm -rf /tmp/* /var/lib/apt/lists/*
&& rm -rf /tmp/* /var/lib/apt/lists/* \
&& apt-get purge --auto-remove
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - \
&& apt-get update && apt-get install -y --no-install-recommends nodejs \
&& rm -rf /tmp/* /var/lib/apt/lists/* \
&& apt-get purge --auto-remove
# https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md
# https://github.com/ebidel/try-puppeteer/blob/master/backend/Dockerfile
......@@ -29,14 +35,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
# Note: this also installs the necessary libs so we don't need the previous RUN command.
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
&& apt-get update \
&& apt-get install -y google-chrome-unstable \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get update && apt-get install -y --no-install-recommends \
google-chrome-unstable \
&& rm -rf /tmp/* /var/lib/apt/lists/* \
&& apt-get purge --auto-remove
# Add chatie user.
RUN groupadd -r bot && useradd -r -g bot -d /bot -m -G audio,video,sudo bot \
RUN groupadd bot && useradd -g bot -d /bot -m -G audio,video,sudo bot \
&& mkdir -p /bot/Downloads \
&& chown -R bot:bot /bot \
&& echo "bot ALL=NOPASSWD:ALL" >> /etc/sudoers
......@@ -82,7 +87,7 @@ LABEL org.label-schema.license="Apache-2.0" \
org.label-schema.description="Wechat for Bot" \
org.label-schema.usage="https://github.com/chatie/wechaty/wiki/Docker" \
org.label-schema.url="https://www.chatie.io" \
org.label-schema.vendor="AKA Mobi" \
org.label-schema.vendor="Chatie" \
org.label-schema.vcs-ref="$SOURCE_COMMIT" \
org.label-schema.vcs-url="https://github.com/chatie/wechaty" \
org.label-schema.docker.cmd="docker run -ti --rm zixia/wechaty <code.js>" \
......
......@@ -21,8 +21,7 @@ test_script:
- npm --version
# https://bugs.chromium.org/p/chromium/issues/detail?id=158372#c6
- wmic datafile where name="C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe" get Version /value
- npm run lint
- npm run test
- npm run test:win32
# Don't actually build.
build: off
......
......@@ -119,7 +119,7 @@ function wechaty::runBot() {
# NPM module install will have problem in China.
# i.e. chromedriver need to visit a google host to download binarys.
#
echo "Please make sure you had installed all the NPM modules which is depended by your bot script."
echo "Please make sure you had installed all the NPM modules which is depended on your bot script."
# yarn < /dev/null || return $? # yarn will close stdin??? cause `read` command fail after yarn
}
......@@ -261,7 +261,7 @@ function main() {
;;
test)
WECHATY_LOG=silent npm run test:linux
WECHATY_LOG=silent npm run test:unit
;;
#
......
......@@ -47,7 +47,7 @@ let token = config.token
if (!token) {
log.error('Client', 'token not found: please set WECHATY_TOKEN in environment before run io-client')
// process.exit(-1)
token = config.DEFAULT_TOKEN
token = config.default.DEFAULT_TOKEN
log.warn('Client', `set token to "${token}" for demo purpose`)
}
......
......@@ -56,7 +56,7 @@ import {
const APIAI_API_KEY = '7217d7bce18c4bcfbe04ba7bdfaf9c08'
const brainApiAi = ApiAi(APIAI_API_KEY)
const bot = Wechaty.instance({ profile: config.DEFAULT_PROFILE })
const bot = Wechaty.instance({ profile: config.default.DEFAULT_PROFILE })
console.log(`
Welcome to api.AI Wechaty Bot.
......
......@@ -54,7 +54,7 @@ Please wait... I'm trying to login in...
`
console.log(welcome)
const bot = Wechaty.instance({ profile: config.DEFAULT_PROFILE })
const bot = Wechaty.instance({ profile: config.default.DEFAULT_PROFILE })
bot
.on('login' , function(this, user) {
......
......@@ -50,37 +50,33 @@ const welcome = `
=============== Powered by Wechaty ===============
-------- https://github.com/chatie/wechaty --------
I'm a bot, my super power is talk in Wechat.
I'm a bot, my superpower is talk in Wechat.
If you send me a 'ding', I will reply you a 'dong'!
__________________________________________________
Hope you like it, and you are very welcome to
upgrade me for more super powers!
upgrade me to more superpowers!
Please wait... I'm trying to login in...
`
console.log(welcome)
const bot = Wechaty.instance({ profile: config.DEFAULT_PROFILE })
const bot = Wechaty.instance({ profile: config.default.DEFAULT_PROFILE })
bot
.on('logout' , user => log.info('Bot', `${user.name()} logouted`))
.on('login' , user => {
log.info('Bot', `${user.name()} logined`)
bot.say('Wechaty login')
})
.on('error' , e => {
log.info('Bot', 'error: %s', e)
bot.say('Wechaty error: ' + e.message)
log.info('Bot', `${user.name()} login`)
bot.say('Wechaty login').catch(console.error)
})
.on('scan', (url, code) => {
if (!/201|200/.test(String(code))) {
const loginUrl = url.replace(/\/qrcode\//, '/l/')
QrcodeTerminal.generate(loginUrl)
}
console.log(`${url}\n[${code}] Scan QR Code in above url to login: `)
console.log(`${url}\n[${code}] Scan QR Code above url to log in: `)
})
.on('message', async m => {
try {
......@@ -111,12 +107,22 @@ bot
bot.start()
.catch(e => {
log.error('Bot', 'init() fail: %s', e)
bot.quit()
bot.stop()
process.exit(-1)
})
bot.on('error', async e => {
log.error('Bot', 'error: %s', e)
if (bot.logonoff()) {
await bot.say('Wechaty error: ' + e.message).catch(console.error)
}
await bot.stop()
})
finis((code, signal) => {
const exitMsg = `Wechaty exit ${code} because of ${signal} `
console.log(exitMsg)
bot.say(exitMsg)
if (bot.logonoff()) {
bot.say(exitMsg).catch(console.error)
}
})
......@@ -57,7 +57,7 @@ Please wait... I'm trying to login in...
`
console.log(welcome)
const bot = Wechaty.instance({ profile: config.DEFAULT_PROFILE })
const bot = Wechaty.instance({ profile: config.default.DEFAULT_PROFILE })
bot
.on('login' , user => log.info('Bot', `${user.name()} logined`))
......
......@@ -41,7 +41,7 @@ Please wait... I'm trying to login in...
`
console.log(welcome)
Wechaty.instance({ profile: config.DEFAULT_PROFILE })
Wechaty.instance({ profile: config.default.DEFAULT_PROFILE })
.on('scan', (url, code) => {
if (!/201|200/.test(String(code))) {
......
......@@ -38,7 +38,7 @@ import {
// MsgType,
Wechaty,
} from '../'
const bot = Wechaty.instance({ profile: config.DEFAULT_PROFILE })
const bot = Wechaty.instance({ profile: config.default.DEFAULT_PROFILE })
bot
.on('scan', (url, code) => {
......
......@@ -86,7 +86,7 @@ Please wait... I'm trying to login in...
`
console.log(welcome)
const bot = Wechaty.instance({ profile: config.DEFAULT_PROFILE })
const bot = Wechaty.instance({ profile: config.default.DEFAULT_PROFILE })
bot
.on('scan', (url, code) => {
......
......@@ -42,7 +42,7 @@ import {
Wechaty,
} from '../'
const bot = Wechaty.instance({ profile: config.DEFAULT_PROFILE })
const bot = Wechaty.instance({ profile: config.default.DEFAULT_PROFILE })
bot
.on('scan', (url, code) => {
......
......@@ -53,7 +53,7 @@ import {
const TULING123_API_KEY = '18f25157e0446df58ade098479f74b21'
const tuling = new Tuling123(TULING123_API_KEY)
const bot = Wechaty.instance({ profile: config.DEFAULT_PROFILE })
const bot = Wechaty.instance({ profile: config.default.DEFAULT_PROFILE })
console.log(`
Welcome to Tuling Wechaty Bot.
......
......@@ -2,6 +2,7 @@ export {
config,
log,
Sayable,
VERSION,
} from './src/config'
export { Contact } from './src/contact'
......@@ -24,8 +25,6 @@ export { PuppetWeb } from './src/puppet-web/'
export { Room } from './src/room'
export { Misc } from './src/misc'
export const VERSION = require('./package.json').version
import Wechaty from './src/wechaty'
export { Wechaty }
export default Wechaty
{
"name": "wechaty",
"version": "0.11.8",
"version": "0.12.3",
"description": "Wechat for Bot(Personal Account)",
"main": "dist/index.js",
"types": "dist/index.d.ts",
......@@ -15,23 +15,24 @@
},
"scripts": {
"ts-node": "ts-node",
"dist": "npm run clean && tsc && jq \"del (.files)\" < package.json > dist/package.json && shx cp src/puppet-web/*.js dist/src/puppet-web/",
"dist": "npm run clean && tsc && shx cp src/puppet-web/*.js dist/src/puppet-web/",
"doc": "npm run dist && echo '# Wechaty v'$(jq -r .version package.json)' Documentation\n' > docs/index.md && jsdoc2md dist/src/{wechaty,room,contact,friend-request,message}.js dist/src/puppet-web/friend-request.js>> docs/index.md",
"coverage": "nyc report --reporter=text-lcov | coveralls",
"changelog": "github_changelog_generator -u chatie -p wechaty && sed -i'.bak' /greenkeeper/d CHANGELOG.md && ts-node script/sort-contributiveness.ts < CHANGELOG.md > CHANGELOG.new.md 2>/dev/null && cat CHANGELOG.md >> CHANGELOG.new.md && mv CHANGELOG.new.md CHANGELOG.md",
"changelog": "github_changelog_generator -u chatie -p wechaty && sed -i'.bak' /greenkeeper/d CHANGELOG.md && sed -i'.bak' '/An in-range update of/d' CHANGELOG.md && ts-node script/sort-contributiveness.ts < CHANGELOG.md > CHANGELOG.new.md 2>/dev/null && cat CHANGELOG.md >> CHANGELOG.new.md && mv CHANGELOG.new.md CHANGELOG.md",
"doctor": "npm run check-node-version && ts-node bin/doctor",
"clean": "shx rm -fr dist/*",
"check-node-version": "check-node-version --node \">= 7\"",
"lint": "npm run check-node-version && npm run lint:ts && npm run lint:es && npm run lint:sh",
"lint:es": "eslint \"{bin,example,src,test}/**/*.js\" --ignore-pattern=\"test/fixture/**\"",
"lint:ts": "npm run clean && echo tslint v`tslint --version` && tslint --project tsconfig.json --type-check \"{bin,example,src,test}/**/*.ts\" --exclude \"test/fixture/**\" --exclude \"dist/\" && tsc --noEmit",
"lint:ts": "npm run clean && echo tslint v`tslint --version` && tslint --project tsconfig.json \"{bin,example,src,test}/**/*.ts\" --exclude \"test/fixture/**\" --exclude \"dist/\" && tsc --noEmit",
"lint:sh": "bash -n bin/*.sh",
"sloc": "sloc bin example src test index.ts --details --format cli-table --keys total,source,comment && sloc bin example src test index.ts",
"pretest": "npm run clean && npm run lint",
"test": "blue-tape -r ts-node/register -r source-map-support/register \"src/**/*.spec.ts\" \"tests/**/*.spec.ts\"",
"posttest": "npm run sloc",
"test": "npm run clean && npm run lint && npm run test:unit && npm run test:shell && npm run sloc",
"test:linux": "npm run pretest && parallel ts-node -- ./src/**/*.spec.ts ./test/**/*.spec.ts && npm run posttest",
"test:npm": "npm run dist && export TMPDIR=/tmp/wechaty.$$ && npm pack && mkdir $TMPDIR && mv wechaty-*.*.*.tgz $TMPDIR && cp test/fixture/smoke-testing.js $TMPDIR && cd $TMPDIR && npm init -y && npm i wechaty-*.*.*.tgz && node smoke-testing.js",
"test:shell": "shellcheck bin/*.sh",
"test:unit": "blue-tape -r ts-node/register -r source-map-support/register \"src/**/*.spec.ts\" \"tests/**/*.spec.ts\"",
"test:win32": "npm run test:unit",
"io-client": "ts-node bin/io-client",
"demo": "ts-node example/ding-dong-bot.ts",
"start": "npm run demo"
......@@ -88,62 +89,62 @@
"node": ">= 7"
},
"dependencies": {
"@types/node": "^8.0.33",
"@types/ws": "^3.2.0",
"bl": "^1.2.1",
"brolog": "^1.2.8",
"hot-import": "^0.1.21",
"mime": "^2.0.3",
"puppeteer": "^0.12.0",
"raven": "^2.2.1",
"request": "^2.83.0",
"retry-promise": "^1.0.0",
"rxjs": "^5.4.3",
"state-switch": "^0.2.2",
"watchdog": "^0.1.16",
"ws": "^3.2.0",
"xml2js": "^0.4.19"
"@types/node": "^8.0",
"@types/ws": "^3.2",
"bl": "^1.2",
"brolog": "^1.2",
"hot-import": "^0.1",
"mime": "^2.0",
"puppeteer": "^0.12",
"raven": "^2.2",
"read-pkg-up": "^2.0",
"request": "^2.83",
"retry-promise": "^1.0",
"rx-queue": "^0.3.1",
"rxjs": "^5.5",
"state-switch": "^0.2",
"watchdog": "^0.3",
"ws": "^3.2",
"xml2js": "^0.4"
},
"devDependencies": {
"@types/blue-tape": "^0.1.31",
"@types/body-parser": "^1.16.5",
"@types/express": "^4.0.37",
"@types/fluent-ffmpeg": "^2.1.4",
"@types/glob": "^5.0.33",
"@types/mime": "^2.0.0",
"@types/puppeteer": "^0.12.0",
"@types/raven": "^2.1.2",
"@types/request": "^2.0.4",
"@types/sinon": "^2.3.5",
"@types/xml2js": "^0.4.0",
"apiai": "^4.0.3",
"babel-cli": "^6.26.0",
"babel-eslint": "^8.0.1",
"babel-preset-env": "^1.6.0",
"blue-tape": "^1.0.0",
"body-parser": "^1.18.2",
"check-node-version": "^2.1.0",
"cookie-parser": "^1.4.3",
"coveralls": "^3.0.0",
"cross-env": "^5.0.5",
"eslint": "^4.8.0",
"express": "^4.15.2",
"finis": "^0.0.3",
"fluent-ffmpeg": "^2.1.2",
"glob": "^7.1.2",
"jsdoc-to-markdown": "^3.0.0",
"nyc": "^11.2.1",
"qrcode-terminal": "^0.11.0",
"shx": "^0.2.2",
"sinon": "^4.0.1",
"sinon-test": "^2.1.1",
"sloc": "^0.2.0",
"ts-node": "^3.3.0",
"tslint": "^5.7.0",
"tslint-jsdoc-rules": "^0.1.2",
"tuling123-client": "^0.0.1",
"typescript": "^2.5.3",
"yarn": "^1.1.0"
"@types/blue-tape": "^0.1",
"@types/express": "^4.0",
"@types/fluent-ffmpeg": "^2.1",
"@types/glob": "^5.0",
"@types/mime": "^2.0",
"@types/puppeteer": "^0.12",
"@types/raven": "^2.1",
"@types/read-pkg-up": "^2.0",
"@types/request": "^2.0",
"@types/sinon": "^2.3",
"@types/xml2js": "^0.4",
"apiai": "^4.0",
"babel-cli": "^6.26",
"babel-eslint": "^8.0",
"babel-preset-env": "^1.6",
"blue-tape": "^1.0",
"check-node-version": "^2.1",
"cookie-parser": "^1.4",
"coveralls": "^3.0",
"cross-env": "^5.0",
"eslint": "^4.8",
"express": "^4.15",
"finis": "^0.2",
"fluent-ffmpeg": "^2.1",
"glob": "^7.1",
"jsdoc-to-markdown": "^3.0",
"nyc": "^11.2",
"qrcode-terminal": "^0.11",
"shx": "^0.2",
"sinon": "^4.0",
"sinon-test": "^2.1",
"sloc": "^0.2",
"ts-node": "^3.3",
"tslint": "^5.7",
"tslint-jsdoc-rules": "^0.1",
"tuling123-client": "^0.0",
"typescript": "^2.5"
},
"files_comment__whitelist_npm_publish": "http://stackoverflow.com/a/8617868/1123955",
"files": [
......
......@@ -31,10 +31,10 @@ test('important variables', async t => {
t.true('profile' in config, 'should exist `profile` in Config')
t.true('token' in config, 'should exist `token` in Config')
t.ok(config.DEFAULT_PUPPET , 'should export DEFAULT_PUPPET')
t.ok(config.DEFAULT_PROFILE , 'should export DEFAULT_PROFILE')
t.ok(config.DEFAULT_PROTOCOL , 'should export DEFAULT_PROTOCOL')
t.ok(config.DEFAULT_APIHOST , 'should export DEFAULT_APIHOST')
t.ok(config.default.DEFAULT_PUPPET , 'should export DEFAULT_PUPPET')
t.ok(config.default.DEFAULT_PROFILE , 'should export DEFAULT_PROFILE')
t.ok(config.default.DEFAULT_PROTOCOL , 'should export DEFAULT_PROTOCOL')
t.ok(config.default.DEFAULT_APIHOST , 'should export DEFAULT_APIHOST')
})
test('validApiHost()', async t => {
......
......@@ -20,10 +20,18 @@ import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import * as readPkgUp from 'read-pkg-up'
import * as Raven from 'raven'
import Brolog from 'brolog'
import Puppet from './puppet'
const pkg = readPkgUp.sync({ cwd: __dirname }).pkg
export const VERSION = pkg.version
/**
* Raven.io
*/
import * as Raven from 'raven'
Raven.disableConsoleAlerts()
Raven
......@@ -31,9 +39,9 @@ Raven
process.env.NODE_ENV === 'production'
&& 'https://f6770399ee65459a82af82650231b22c:d8d11b283deb441e807079b8bb2c45cd@sentry.io/179672',
{
release: require('../package.json').version,
release: VERSION,
tags: {
git_commit: 'c0deb10c4',
git_commit: '',
platform: !!process.env['WECHATY_DOCKER']
? 'docker'
: os.platform(),
......@@ -54,11 +62,7 @@ Raven.context(function () {
})
*/
import Brolog from 'brolog'
export const log = new Brolog()
import { Puppet } from './puppet'
const logLevel = process.env['WECHATY_LOG'] || 'info'
if (logLevel) {
log.level(logLevel.toLowerCase() as any)
......@@ -68,14 +72,14 @@ if (logLevel) {
/**
* to handle unhandled exceptions
*/
if (/verbose|silly/i.test(logLevel)) {
if (/verbose|silly/i.test(log.level())) {
log.info('Config', 'registering process.on("unhandledRejection") for development/debug')
process.on('unhandledRejection', (reason, promise) => {
log.error('Config', '###########################')
log.error('Config', 'unhandledRejection: %s %s', reason, promise)
log.error('Config', '###########################')
promise.catch(err => {
log.error('Config', 'unhandledRejection::catch(%s)', err.message)
log.error('Config', 'process.on(unhandledRejection) promise.catch(%s)', err.message)
console.error('Config', err) // I don't know if log.error has similar full trace print support like console.error
})
})
......@@ -85,173 +89,109 @@ export type PuppetName = 'web'
| 'android'
| 'ios'
export interface ConfigSetting {
export interface DefaultSetting {
DEFAULT_HEAD : number,
DEFAULT_PORT : number,
DEFAULT_PUPPET : PuppetName
DEFAULT_APIHOST : string
DEFAULT_PROFILE : string
DEFAULT_TOKEN : string
DEFAULT_PROTOCOL : string
profile : string
token : string
debug : boolean
head: boolean
puppet: PuppetName
apihost: string
validApiHost: (host: string) => boolean
httpPort: number
_puppetInstance: Puppet | null
puppetInstance(): Puppet
puppetInstance(empty: null): void
puppetInstance(instance: Puppet): void
puppetInstance(instance?: Puppet | null): Puppet | void,
gitVersion(): string | null,
npmVersion(): string,
docker: boolean,
DEFAULT_PUPPET : PuppetName,
DEFAULT_APIHOST : string,
DEFAULT_PROFILE : string,
DEFAULT_TOKEN : string,
DEFAULT_PROTOCOL : string,
}
/* tslint:disable:variable-name */
/* tslint:disable:no-var-requires */
export const config: ConfigSetting = require('../package.json').wechaty
export const DEFAULT_SETTING = pkg.wechaty as DefaultSetting
/**
* 1. ENVIRONMENT VARIABLES + PACKAGES.JSON (default)
*/
Object.assign(config, {
apihost: process.env['WECHATY_APIHOST'] || config.DEFAULT_APIHOST,
head: ('WECHATY_HEAD' in process.env) ? (!!process.env['WECHATY_HEAD']) : (!!(config.DEFAULT_HEAD)),
puppet: process.env['WECHATY_PUPPET'] || config.DEFAULT_PUPPET,
validApiHost,
})
export class Config {
public default = DEFAULT_SETTING
function validApiHost(apihost: string): boolean {
if (/^[a-zA-Z0-9\.\-\_]+:?[0-9]*$/.test(apihost)) {
return true
}
throw new Error('validApiHost() fail for ' + apihost)
}
validApiHost(config.apihost)
public apihost = process.env['WECHATY_APIHOST'] || DEFAULT_SETTING.DEFAULT_APIHOST
public head = ('WECHATY_HEAD' in process.env) ? (!!process.env['WECHATY_HEAD']) : (!!(DEFAULT_SETTING.DEFAULT_HEAD))
public puppet = (process.env['WECHATY_PUPPET'] || DEFAULT_SETTING.DEFAULT_PUPPET) as PuppetName
/**
* 2. ENVIRONMENT VARIABLES (only)
*/
Object.assign(config, {
// port: process.env['WECHATY_PORT'] || null, // 0 for disable port
profile: process.env['WECHATY_PROFILE'] || null, // DO NOT set DEFAULT_PROFILE, because sometimes user do not want to save session
token: process.env['WECHATY_TOKEN'] || null, // DO NOT set DEFAULT, because sometimes user do not want to connect to io cloud service
debug: !!(process.env['WECHATY_DEBUG']) || false,
})
public profile = process.env['WECHATY_PROFILE'] || null // DO NOT set DEFAULT_PROFILE, because sometimes user do not want to save session
public token = process.env['WECHATY_TOKEN'] || null // DO NOT set DEFAULT, because sometimes user do not want to connect to io cloud service
public debug = !!(process.env['WECHATY_DEBUG'])
/**
* 3. Service Settings
*/
Object.assign(config, {
// get PORT form cloud service env, ie: heroku
httpPort: process.env['PORT'] || process.env['WECHATY_PORT'] || config.DEFAULT_PORT,
})
public httpPort = process.env['PORT'] || process.env['WECHATY_PORT'] || DEFAULT_SETTING.DEFAULT_PORT
public docker = !!(process.env['WECHATY_DOCKER'])
/**
* 4. Envioronment Identify
*/
Object.assign(config, {
docker: !!process.env['WECHATY_DOCKER'],
isGlobal: isWechatyInstalledGlobal(),
})
private _puppetInstance: Puppet | null = null
constructor() {
log.verbose('Config', 'constructor()')
this.validApiHost(this.apihost)
}
function isWechatyInstalledGlobal() {
/**
* TODO:
* 1. check /node_modules/wechaty
* 2. return true if exists
* 3. otherwise return false
* 5. live setting
*/
return false
}
/**
* 5. live setting
*/
function puppetInstance(): Puppet
function puppetInstance(empty: null): void
function puppetInstance(instance: Puppet): void
function puppetInstance(instance?: Puppet | null): Puppet | void {
if (typeof instance === 'undefined') {
if (!this._puppetInstance) {
throw new Error('no puppet instance')
public puppetInstance(): Puppet
public puppetInstance(empty: null): void
public puppetInstance(instance: Puppet): void
public puppetInstance(instance?: Puppet | null): Puppet | void {
if (typeof instance === 'undefined') {
if (!this._puppetInstance) {
throw new Error('no puppet instance')
}
return this._puppetInstance
} else if (instance === null) {
log.verbose('Config', 'puppetInstance(null)')
this._puppetInstance = null
return
}
return this._puppetInstance
} else if (instance === null) {
log.verbose('Config', 'puppetInstance(null)')
this._puppetInstance = null
log.verbose('Config', 'puppetInstance(%s)', instance.constructor.name)
this._puppetInstance = instance
return
}
log.verbose('Config', 'puppetInstance(%s)', instance.constructor.name)
this._puppetInstance = instance
return
}
function gitVersion(): string | null {
const dotGitPath = path.join(__dirname, '..', '.git') // only for ts-node, not for dist
// const gitLogArgs = ['log', '--oneline', '-1']
// TODO: use git rev-parse HEAD ?
const gitArgs = ['rev-parse', 'HEAD']
try {
// Make sure this is a Wechaty repository
fs.statSync(dotGitPath).isDirectory()
const ss = require('child_process')
.spawnSync('git', gitArgs, { cwd: __dirname })
}
if (ss.status !== 0) {
throw new Error(ss.error)
public gitRevision(): string | null {
const dotGitPath = path.join(__dirname, '..', '.git') // only for ts-node, not for dist
// const gitLogArgs = ['log', '--oneline', '-1']
// TODO: use git rev-parse HEAD ?
const gitArgs = ['rev-parse', 'HEAD']
try {
// Make sure this is a Wechaty repository
fs.statSync(dotGitPath).isDirectory()
const ss = require('child_process')
.spawnSync('git', gitArgs, { cwd: __dirname })
if (ss.status !== 0) {
throw new Error(ss.error)
}
const revision = ss.stdout
.toString()
.trim()
.slice(0, 7)
return revision
} catch (e) { /* fall safe */
/**
* 1. .git not exist
* 2. git log fail
*/
log.silly('Wechaty', 'version() form development environment is not availble: %s', e.message)
return null
}
const revision = ss.stdout
.toString()
.trim()
.slice(0, 7)
return revision
} catch (e) { /* fall safe */
/**
* 1. .git not exist
* 2. git log fail
*/
log.silly('Wechaty', 'version() form development environment is not availble: %s', e.message)
return null
}
}
function npmVersion(): string {
try {
return require('../package.json').version
} catch (e) {
log.error('Wechaty', 'npmVersion() exception %s', e.message)
Raven.captureException(e)
return '0.0.0'
public validApiHost(apihost: string): boolean {
if (/^[a-zA-Z0-9\.\-\_]+:?[0-9]*$/.test(apihost)) {
return true
}
throw new Error('validApiHost() fail for ' + apihost)
}
}
Object.assign(config, {
gitVersion,
npmVersion,
puppetInstance,
})
export interface Sayable {
say(content: string, replyTo?: any|any[]): Promise<boolean>
}
......@@ -264,4 +204,5 @@ export {
Raven,
}
export const config = new Config()
export default config
......@@ -47,7 +47,7 @@ export class IoClient {
) {
log.verbose('IoClient', 'constructor({ token = %s})', options.token)
options.token = options.token || config.token || config.DEFAULT_TOKEN,
options.token = options.token || config.token || config.default.DEFAULT_TOKEN
options.wechaty = options.wechaty || Wechaty.instance({ profile: this.options.token })
this.io = new Io({
......
......@@ -70,7 +70,7 @@ export class Io {
private options: IoOptions,
) {
options.apihost = options.apihost || config.apihost
options.protocol = options.protocol || config.DEFAULT_PROTOCOL
options.protocol = options.protocol || config.default.DEFAULT_PROTOCOL
this.uuid = options.wechaty.uuid
......@@ -269,7 +269,7 @@ export class Io {
break
case 'shutdown':
log.warn('Io', 'on(shutdown): %s', ioEvent.payload)
log.info('Io', 'on(shutdown): %s', ioEvent.payload)
process.exit(0)
break
......@@ -304,7 +304,7 @@ export class Io {
break
case 'logout':
log.warn('Io', 'on(logout): %s', ioEvent.payload)
log.info('Io', 'on(logout): %s', ioEvent.payload)
this.options.wechaty.logout()
break
......
......@@ -17,7 +17,7 @@ export class Profile {
private file : string | null
constructor(
public name: string = config.profile,
public name = config.profile,
) {
log.verbose('Profile', 'constructor(%s)', name)
......@@ -28,8 +28,11 @@ export class Profile {
? name
: path.join(
process.cwd(),
name + '.wechaty.json',
name,
)
if (!/\.wechaty\.json$/.test(this.file)) {
this.file += '.wechaty.json'
}
}
}
......
......@@ -43,7 +43,40 @@ test('PuppetWebBridge', async t => {
}
})
test('preHtmlToXml()', async t => {
const BLOCKED_HTML_ZH = [
'<pre style="word-wrap: break-word; white-space: pre-wrap;">',
'&lt;error&gt;',
'&lt;ret&gt;1203&lt;/ret&gt;',
'&lt;message&gt;当前登录环境异常。为了你的帐号安全,暂时不能登录web微信。你可以通过Windows微信、Mac微信或者手机客户端微信登录。&lt;/message&gt;',
'&lt;/error&gt;',
'</pre>',
].join('')
const BLOCKED_XML_ZH = [
'<error>',
'<ret>1203</ret>',
'<message>当前登录环境异常。为了你的帐号安全,暂时不能登录web微信。你可以通过Windows微信、Mac微信或者手机客户端微信登录。</message>',
'</error>',
].join('')
const profile = new Profile()
const bridge = new Bridge({ profile })
const xml = bridge.preHtmlToXml(BLOCKED_HTML_ZH)
t.equal(xml, BLOCKED_XML_ZH, 'should parse html to xml')
})
test('testBlockedMessage()', async t => {
const BLOCKED_HTML_ZH = [
'<pre style="word-wrap: break-word; white-space: pre-wrap;">',
'&lt;error&gt;',
'&lt;ret&gt;1203&lt;/ret&gt;',
'&lt;message&gt;当前登录环境异常。为了你的帐号安全,暂时不能登录web微信。你可以通过手机客户端或者windows微信登录。&lt;/message&gt;',
'&lt;/error&gt;',
'</pre>',
].join('')
const BLOCKED_XML_ZH = `
<error>
<ret>1203</ret>
......@@ -70,36 +103,32 @@ test('testBlockedMessage()', async t => {
const profile = new Profile()
const bridge = new Bridge({ profile })
try {
await bridge.testBlockedMessage('this is not xml')
t.pass('should not throw when no block message')
} catch (e) {
t.fail('should throw when no block message')
}
const msg = await bridge.testBlockedMessage('this is not xml')
t.equal(msg, false, 'should return false when no block message')
})
test('html', async t => {
const profile = new Profile()
const bridge = new Bridge({ profile })
const msg = await bridge.testBlockedMessage(BLOCKED_HTML_ZH)
t.equal(msg, BLOCKED_TEXT_ZH, 'should get zh blocked message')
})
test('zh', async t => {
const profile = new Profile()
const bridge = new Bridge({ profile })
try {
await bridge.testBlockedMessage(BLOCKED_XML_ZH)
t.fail('should throw exception')
} catch (e) {
t.equal(e.message, BLOCKED_TEXT_ZH, 'should get zh blocked message')
}
const msg = await bridge.testBlockedMessage(BLOCKED_XML_ZH)
t.equal(msg, BLOCKED_TEXT_ZH, 'should get zh blocked message')
})
test('en', async t => {
const profile = new Profile()
const bridge = new Bridge({ profile })
try {
await bridge.testBlockedMessage(BLOCKED_XML_EN)
t.fail('should throw exception')
} catch (e) {
t.equal(e.message, BLOCKED_TEXT_EN, 'should get en blocked message')
}
const msg = await bridge.testBlockedMessage(BLOCKED_XML_EN)
t.equal(msg, BLOCKED_TEXT_EN, 'should get en blocked message')
})
})
......@@ -117,7 +146,7 @@ test('clickSwitchAccount()', async t => {
test('switch account needed', async t => {
const browser = await launch()
const page = await browser.newPage()
const page = await browser.newPage()
await page.setContent(SWITCH_ACCOUNT_HTML)
const clicked = await bridge.clickSwitchAccount(page)
......@@ -130,7 +159,7 @@ test('clickSwitchAccount()', async t => {
test('switch account not needed', async t => {
const browser = await launch()
const page = await browser.newPage()
const page = await browser.newPage()
await page.setContent('<h1>ok</h1>')
const clicked = await bridge.clickSwitchAccount(page)
......
......@@ -36,7 +36,7 @@ const retryPromise = require('retry-promise').default
import { log } from '../config'
import Profile from '../profile'
import Misc from '../misc'
import {
MediaData,
MsgRawObj,
......@@ -76,13 +76,30 @@ export class Bridge extends EventEmitter {
this.browser = await this.initBrowser()
log.verbose('PuppetWebBridge', 'init() initBrowser() done')
this.on('load', this.onLoad.bind(this))
const ready = new Promise(resolve => this.once('ready', resolve))
this.page = await this.initPage(this.browser)
await ready
this.state.on(true)
log.verbose('PuppetWebBridge', 'init() initPage() done')
} catch (e) {
this.state.off(true)
log.error('PuppetWebBridge', 'init() exception: %s', e)
this.state.off(true)
try {
if (this.page) {
await this.page.close()
}
if (this.browser) {
await this.browser.close()
}
} catch (e2) {
log.error('PuppetWebBridge', 'init() exception %s, close page/browser exception %s', e, e2)
}
this.emit('error', e)
throw e
}
}
......@@ -94,8 +111,15 @@ export class Bridge extends EventEmitter {
const browser = await launch({
headless,
args: [
'--audio-output-channels=0',
'--disable-default-apps',
'--disable-extensions',
'--disable-translate',
'--disable-gpu',
'--disable-setuid-sandbox',
'--disable-sync',
'--hide-scrollbars',
'--mute-audio',
'--no-sandbox',
],
})
......@@ -106,79 +130,58 @@ export class Bridge extends EventEmitter {
return browser
}
public async initPage(browser: Browser): Promise<Page> {
log.verbose('PuppetWebBridge', 'initPage()')
const page = await browser.newPage()
// set this in time because the following callbacks
// might be called before initPage() return.
this.page = page
const onDialog = async (dialog: Dialog) => {
log.warn('PuppetWebBridge', 'init() page.on(dialog) type:%s message:%s',
dialog.type, dialog.message())
try {
// XXX: Which ONE is better?
await dialog.accept()
// await dialog.dismiss()
} catch (e) {
log.error('PuppetWebBridge', 'init() dialog.dismiss() reject: %s', e)
}
this.emit('error', new Error(`${dialog.type}(${dialog.message()})`))
public async onDialog(dialog: Dialog) {
log.warn('PuppetWebBridge', 'init() page.on(dialog) type:%s message:%s',
dialog.type, dialog.message())
try {
// XXX: Which ONE is better?
await dialog.accept()
// await dialog.dismiss()
} catch (e) {
log.error('PuppetWebBridge', 'init() dialog.dismiss() reject: %s', e)
}
this.emit('error', new Error(`${dialog.type}(${dialog.message()})`))
}
const onLoad = async (done: () => void) => {
log.verbose('PuppetWebBridge', 'initPage() on(load) %s', page.url())
public async onLoad(page: Page): Promise<void> {
log.verbose('PuppetWebBridge', 'initPage() on(load) %s', page.url())
if (this.state.off()) {
log.verbose('PuppetWebBridge', 'initPage() onLoad() OFF state detected. NOP')
return
}
if (this.state.off()) {
log.verbose('PuppetWebBridge', 'initPage() onLoad() OFF state detected. NOP')
return // reject(new Error('onLoad() OFF state detected'))
}
try {
try {
const emitExist = await page.evaluate(() => {
return typeof window['emit'] === 'function'
})
if (!emitExist) {
await page.exposeFunction('emit', this.emit.bind(this))
} catch (e) {
if (this.state.off()) {
log.verbose('PuppetWebBridge', 'initPage() onLoad() OFF state detected. NOP')
return
}
// exposed function will stay in the browser after reload the page
log.verbose('PuppetWebBridge', 'initPage() onLoad() page.exposeFunction(emit) already exist')
log.silly('PuppetWebBridge', 'initPage() onLoad() page.exposeFunction(emit) exception: %s', e)
}
try {
await this.readyAngular(page)
await this.inject(page)
const clicked = await this.clickSwitchAccount(page)
if (clicked) {
log.verbose('PuppetWebBridge', 'initPage() onLoad() clickSwitchAccount() clicked')
} else {
log.silly('PuppetWebBridge', 'initPage() onLoad() clickSwitchAccount() NOP')
}
await this.readyAngular(page)
await this.inject(page)
await this.clickSwitchAccount(page)
} catch (e) {
if (this.state.off()) {
log.verbose('PuppetWebBridge', 'initPage() onLoad() OFF state detected. NOP')
return
}
this.emit('ready')
log.error('PuppetWebBridge', 'init() initPage() onLoad() exception: %s', e)
this.emit('error', e)
} finally {
done()
}
} catch (e) {
log.error('PuppetWebBridge', 'init() initPage() onLoad() exception: %s', e)
await page.close()
this.emit('error', e)
}
}
page.on('dialog', onDialog)
page.on('error', e => this.emit('error', e))
public async initPage(browser: Browser): Promise<Page> {
log.verbose('PuppetWebBridge', 'initPage()')
const loaded = new Promise(resolve => page.on('load', () => onLoad(resolve)))
// set this in time because the following callbacks
// might be called before initPage() return.
const page = this.page = await browser.newPage()
page.on('error', e => this.emit('error', e))
///////////////////
page.on('dialog', this.onDialog.bind(this))
const cookieList = this.options.profile.get('cookies') as Cookie[]
const url = this.entryUrl(cookieList)
......@@ -190,25 +193,29 @@ export class Bridge extends EventEmitter {
if (cookieList && cookieList.length) {
await page.setCookie(...cookieList)
log.silly('PuppetWebBridge', 'initPage() page.setCookie() %s cookies set back', cookieList.length)
await page.reload() // reload page to make effect of the new cookie.
}
await loaded // wait the page on load finish
page.on('load', () => this.emit('load', page))
await page.reload() // reload page to make effect of the new cookie.
return page
}
public async readyAngular(page: Page): Promise<void> {
log.verbose('PuppetWebBridge', 'readyAngular()')
const TIMEOUT = 30 * 1000
await new Promise<void>(async (resolve, reject) => {
const timer = setTimeout(reject, TIMEOUT)
try {
await page.waitForFunction(`typeof window.angular !== 'undefined'`)
} catch (e) {
log.verbose('PuppetWebBridge', 'readyAngular() exception: %s', e)
clearTimeout(timer)
log.silly('PuppetWebBridge', 'readyAngular() resolve-ed')
resolve()
})
const blockedMessage = await this.testBlockedMessage()
if (blockedMessage) { // Wechat Account Blocked
throw new Error(blockedMessage)
} else {
throw e
}
}
}
public async inject(page: Page): Promise<void> {
......@@ -272,14 +279,18 @@ export class Bridge extends EventEmitter {
try {
await this.page.close()
log.silly('PuppetWebBridge', 'quit() page.close()-ed')
} catch (e) {
log.warn('PuppetWebBridge', 'quit() page.close() exception: %s', e)
}
try {
await this.browser.close()
log.silly('PuppetWebBridge', 'quit() browser.close()-ed')
} catch (e) {
log.warn('PuppetWebBridge', 'quit() exception: %s', e)
this.emit('error', e)
} finally {
this.state.off(true)
log.warn('PuppetWebBridge', 'quit() browser.close() exception: %s', e)
}
this.state.off(true)
}
public async getUserName(): Promise<string> {
......@@ -659,12 +670,36 @@ export class Bridge extends EventEmitter {
}
}
public preHtmlToXml(text: string): string {
log.verbose('PuppetWebBridge', 'preHtmlToXml()')
const preRegex = /^<pre[^>]*>([^<]+)<\/pre>$/i
const matches = text.match(preRegex)
if (!matches) {
return text
}
return Misc.unescapeHtml(matches[1])
}
/**
* Throw if there's a blocked message
*/
public async testBlockedMessage(text: string): Promise<void> {
log.silly('PuppetWebBridge', 'testBlockedMessage(%s)',
text.substr(0, 50).replace(/\n/, ''))
public async testBlockedMessage(text?: string): Promise<string | false> {
if (!text) {
text = await this.evaluate(() => {
return document.body.innerHTML
})
}
if (!text) {
throw new Error('testBlockedMessage() no text found!')
}
const textSnip = text.substr(0, 50).replace(/\n/, '')
log.verbose('PuppetWebBridge', 'testBlockedMessage(%s)',
textSnip)
// see unit test for detail
const tryXmlText = this.preHtmlToXml(text)
interface BlockedMessage {
error?: {
......@@ -673,28 +708,32 @@ export class Bridge extends EventEmitter {
}
}
return new Promise<void>((resolve, reject) => {
parseString(text, { explicitArray: false }, (err, obj: BlockedMessage) => {
if (err) {
return resolve()
return new Promise<string | false>((resolve, reject) => {
parseString(tryXmlText, { explicitArray: false }, (err, obj: BlockedMessage) => {
if (err) { // HTML can not be parsed to JSON
return resolve(false)
}
if (!obj) {
// FIXME: when will this happen?
log.warn('PuppetWebBridge', 'testBlockedMessage() parseString(%s) return empty obj', textSnip)
return resolve(false)
}
if (!obj.error) {
return resolve()
return resolve(false)
}
const ret = +obj.error.ret
const message = obj.error.message
const e = new Error(message)
log.warn('PuppetWebBridge', 'testBlockedMessage() error.ret=%s', ret)
if (ret === 1203) {
// <error>
// <ret>1203</ret>
// <message>当前登录环境异常。为了你的帐号安全,暂时不能登录web微信。你可以通过手机客户端或者windows微信登录。</message>
// </error>
return reject(e)
return resolve(message)
}
log.warn('PuppetWebBridge', 'testBlockedMessage() code: %s type: %s', ret, typeof ret)
return reject(e) // other error message
return resolve(message) // other error message
})
})
}
......@@ -704,28 +743,35 @@ export class Bridge extends EventEmitter {
// https://github.com/GoogleChrome/puppeteer/issues/537#issuecomment-334918553
async function listXpath(thePage: Page, xpath: string): Promise<ElementHandle[]> {
const nodeHandleList = await (thePage as any).evaluateHandle(xpathInner => {
const nodeList: Node[] = []
const query = document.evaluate(xpathInner, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
for (let i = 0, length = query.snapshotLength; i < length; ++i) {
nodeList.push(query.snapshotItem(i))
log.verbose('PuppetWebBridge', 'clickSwitchAccount() listXpath()')
try {
const nodeHandleList = await (thePage as any).evaluateHandle(xpathInner => {
const nodeList: Node[] = []
const query = document.evaluate(xpathInner, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
for (let i = 0, length = query.snapshotLength; i < length; ++i) {
nodeList.push(query.snapshotItem(i))
}
return nodeList
}, xpath)
const properties = await nodeHandleList.getProperties()
const elementHandleList: ElementHandle[] = []
const releasePromises: Promise<void>[] = []
for (const property of properties.values()) {
const element = property.asElement()
if (element)
elementHandleList.push(element)
else
releasePromises.push(property.dispose())
}
return nodeList
}, xpath)
const properties = await nodeHandleList.getProperties()
const elementHandleList: ElementHandle[] = []
const releasePromises: Promise<void>[] = []
for (const property of properties.values()) {
const element = property.asElement()
if (element)
elementHandleList.push(element)
else
releasePromises.push(property.dispose())
await Promise.all(releasePromises)
return elementHandleList
} catch (e) {
log.verbose('PuppetWebBridge', 'clickSwitchAccount() listXpath() exception: %s', e)
return []
}
await Promise.all(releasePromises)
return elementHandleList
}
const XPATH_SELECTOR = `//div[contains(@class,'association') and contains(@class,'show')]/a[@ng-click='qrcodeLogin()']`
......@@ -735,10 +781,12 @@ export class Bridge extends EventEmitter {
await button.click()
log.silly('PuppetWebBridge', 'clickSwitchAccount() clicked!')
return true
} else {
// log.silly('PuppetWebBridge', 'clickSwitchAccount() button not found')
return false
}
} catch (e) {
log.silly('PuppetWebBridge', 'clickSwitchAccount() exception: %s', e)
throw e
......@@ -747,9 +795,15 @@ export class Bridge extends EventEmitter {
public async hostname(): Promise<string | null> {
log.verbose('PuppetWebBridge', 'hostname()')
const hostname = await this.page.evaluate(() => location.hostname) as any as string
log.silly('PuppetWebBridge', 'hostname() got %s', hostname)
return hostname
try {
const hostname = await this.page.evaluate(() => location.hostname) as any as string
log.silly('PuppetWebBridge', 'hostname() got %s', hostname)
return hostname
} catch (e) {
log.error('PuppetWebBridge', 'hostname() exception: %s', e)
this.emit('error', e)
return null
}
}
public async cookies(cookieList: Cookie[]): Promise<void>
......@@ -818,9 +872,15 @@ export class Bridge extends EventEmitter {
return
}
public async evaluate(fn: () => any, ...args: any[]): Promise<() => any> {
public async evaluate(fn: () => any, ...args: any[]): Promise<any> {
log.silly('PuppetWebBridge', 'evaluate()')
return await this.page.evaluate(fn, ...args)
try {
return await this.page.evaluate(fn, ...args)
} catch (e) {
log.error('PuppetWebBridge', 'evaluate() exception: %s', e)
this.emit('error', e)
return null
}
}
}
......
......@@ -20,6 +20,9 @@ import {
Watchdog,
WatchdogFood,
} from 'watchdog'
import {
ThrottleQueue,
} from 'rx-queue'
import {
config,
......@@ -38,7 +41,6 @@ import {
ScanInfo,
} from '../puppet'
import Room from '../room'
import RxQueue from '../rx-queue'
import Misc from '../misc'
import {
......@@ -74,14 +76,16 @@ export class PuppetWeb extends Puppet {
super(options)
this.fileId = 0
const PUPPET_TIMEOUT = 60 * 1000 // 1 minute
const PUPPET_TIMEOUT = 1 * 60 * 1000 // 1 minute
this.puppetWatchdog = new Watchdog<PuppetFoodType>(PUPPET_TIMEOUT, 'PuppetWeb')
const SCAN_TIMEOUT = 4 * 60 * 1000 // 4 minutes
const SCAN_TIMEOUT = 2 * 60 * 1000 // 2 minutes
this.scanWatchdog = new Watchdog<ScanFoodType>(SCAN_TIMEOUT, 'Scan')
}
public toString() { return `PuppetWeb<${this.options.profile.name}>` }
public toString() {
return `PuppetWeb<${this.options.profile.name}>`
}
public async init(): Promise<void> {
log.verbose('PuppetWeb', `init() with ${this.options.profile}`)
......@@ -107,10 +111,10 @@ export class PuppetWeb extends Puppet {
}
this.emit('watchdog', food)
const throttleQueue = new RxQueue('throttle', 5 * 60 * 1000)
this.on('heartbeat', data => throttleQueue.emit('i', data))
throttleQueue.on('o', async () => {
log.verbose('Wechaty', 'init() throttleQueue.on(o)')
const throttleQueue = new ThrottleQueue(5 * 60 * 1000)
this.on('heartbeat', data => throttleQueue.next(data))
throttleQueue.subscribe(async data => {
log.verbose('Wechaty', 'init() throttleQueue.subscribe() new item: %s', data)
await this.saveCookie()
})
......@@ -140,14 +144,14 @@ export class PuppetWeb extends Puppet {
puppet.on('watchdog', food => dog.feed(food))
dog.on('feed', food => {
log.silly('PuppetWeb', 'initWatchdogForPuppet() dog.on(feed)')
log.silly('PuppetWeb', 'initWatchdogForPuppet() dog.on(feed, food={type=%s, data=%s})', food.type, food.data)
// feed the dog, heartbeat the puppet.
puppet.emit('heartbeat', food.data)
})
dog.on('reset', async (food, left) => {
log.warn('PuppetWeb', 'initWatchdogForPuppet() dog.on(reset) last food: %s',
food.data)
dog.on('reset', async (food, timeout) => {
log.warn('PuppetWeb', 'initWatchdogForPuppet() dog.on(reset) last food:%s, timeout:%s',
food.data, timeout)
try {
await this.quit()
await this.init()
......@@ -263,6 +267,7 @@ export class PuppetWeb extends Puppet {
})
this.bridge.on('ding' , Event.onDing.bind(this))
this.bridge.on('error' , e => this.emit('error', e))
this.bridge.on('log' , Event.onLog.bind(this))
this.bridge.on('login' , Event.onLogin.bind(this))
this.bridge.on('logout' , Event.onLogout.bind(this))
......@@ -274,28 +279,9 @@ export class PuppetWeb extends Puppet {
await this.bridge.init()
} catch (e) {
log.error('PuppetWeb', 'initBridge() exception: %s', e.message)
await this.bridge.quit().catch(console.error)
this.emit('error', e)
// TypeError: Cannot read property 'evaluate' of undefined
if (!this.bridge) {
throw e
}
const text = await this.bridge.evaluate(() => {
return document.body.innerHTML
}) as any as string
try {
// Test if Wechat account is blocked
// will throw exception if blocked
await this.bridge.testBlockedMessage(text)
} catch (blockedError) {
// Wechat Account Blocked
log.error('PuppetWeb', 'initBridge() Wechat Account Blocked for using Web: %s', blockedError.message)
this.emit('error', blockedError)
throw blockedError
}
Raven.captureException(e)
throw e
}
......@@ -303,24 +289,24 @@ export class PuppetWeb extends Puppet {
return this.bridge
}
public reset(reason?: string): void {
public async reset(reason?: string): Promise<void> {
log.verbose('PuppetWeb', 'reset(%s)', reason)
this.bridge.quit().then(async () => {
try {
await this.bridge.init()
log.silly('PuppetWeb', 'reset() done')
} catch (e) {
log.error('PuppetWeb', 'reset(%s) bridge.init() reject: %s', reason, e)
this.emit('error', e)
}
}).catch(err => {
log.error('PuppetWeb', 'reset(%s) bridge.quit() reject: %s', reason, err)
try {
await this.bridge.quit()
await this.bridge.init()
log.silly('PuppetWeb', 'reset() done')
} catch (err) {
log.error('PuppetWeb', 'reset(%s) bridge.{quit,init}() exception: %s', reason, err)
this.emit('error', err)
})
}
}
public logined(): boolean {
log.warn('PuppetWeb', 'logined() DEPRECATED. use logonoff() instead.')
return this.logonoff()
}
public logonoff(): boolean {
return !!(this.user)
}
......@@ -742,7 +728,7 @@ export class PuppetWeb extends Puppet {
* send to `filehelper` for notice / log
*/
public async say(content: string): Promise<boolean> {
if (!this.logined()) {
if (!this.logonoff()) {
throw new Error('can not say before login')
}
......
......@@ -63,7 +63,7 @@
hookRecalledMsgProcess()
log('init() scanCode: ' + WechatyBro.vars.scanCode)
checkScan()
setTimeout(() => checkScan(), 1000)
heartBeat(true)
......@@ -165,25 +165,25 @@
// get all we need from wx in browser(angularjs)
WechatyBro.glue = {
injector: injector
, http: http
, mmHttp: mmHttp
, state: state
, accountFactory: accountFactory
, chatroomFactory: chatroomFactory
, chatFactory: chatFactory
, confFactory: confFactory
, contactFactory: contactFactory
, emojiFactory: emojiFactory
, loginFactory: loginFactory
, utilFactory: utilFactory
, rootScope: rootScope
, appScope: appScope
, loginScope: loginScope
, contentChatScope: contentChatScope
injector,
http,
mmHttp,
state,
accountFactory,
chatroomFactory,
chatFactory,
confFactory,
contactFactory,
emojiFactory,
loginFactory,
utilFactory,
rootScope,
appScope,
loginScope,
contentChatScope,
}
return true
......@@ -196,10 +196,13 @@
// login('checkScan found already login')
return
}
if (!WechatyBro.glue.loginScope) {
const loginScope = WechatyBro.glue.loginScope
if (!loginScope) {
log('checkScan() - loginScope disappeared, TODO: find out the reason why this happen')
// login('loginScope disappeared')
// return
return setTimeout(checkScan, 1000)
}
// loginScope.code:
......@@ -207,8 +210,8 @@
// 408: 未确认(显示二维码后30秒触发)
// 201: 扫描,未确认
// 200: 登录成功
var code = +WechatyBro.glue.loginScope.code
var url = WechatyBro.glue.loginScope.qrcodeUrl
var code = +loginScope.code
var url = loginScope.qrcodeUrl
log('checkScan() code:' + code + ' url:' + url + ' scanCode:' + WechatyBro.vars.scanCode)
if (url && code !== WechatyBro.vars.scanCode) {
......@@ -229,7 +232,7 @@
}
WechatyBro.vars.scanCode = null
WechatyBro.glue.loginScope.code = null
loginScope.code = null
return login('scan code 200')
}
......@@ -255,12 +258,12 @@
// WechatyBro.emit('logout', data)
if (WechatyBro.glue.loginFactory) {
WechatyBro.glue.loginFactory.loginout()
WechatyBro.glue.loginFactory.loginout(0)
} else {
log('logout() WechatyBro.glue.loginFactory NOT found')
}
checkScan()
setTimeout(() => checkScan(), 1000)
}
function ding(data) {
......@@ -537,7 +540,7 @@
}
function getUserName() {
if (!WechatyBro.isLogin()) {
if (!WechatyBro.loginState()) {
return null
}
var accountFactory = WechatyBro.glue.accountFactory
......@@ -603,12 +606,12 @@
data: angular.extend({
UserName: UserName,
CmdId: confFactory.oplogCmdId.MODREMARKNAME,
RemarkName: emojiFactory.formatHTMLToSend(remark)
RemarkName: emojiFactory.formatHTMLToSend(remark),
}, accountFactory.getBaseRequest()),
MMRetry: {
count: 3,
timeout: 1e4,
serial: !0
serial: !0,
}
})
.success(() => {
......@@ -717,11 +720,11 @@
return new Promise(resolve => {
contactFactory.verifyUser({
UserName: UserName
, Opcode: confFactory.VERIFYUSER_OPCODE_SENDREQUEST
, Scene: confFactory.ADDSCENE_PF_WEB
, Ticket: Ticket
, VerifyContent: VerifyContent
Opcode: confFactory.VERIFYUSER_OPCODE_SENDREQUEST,
Scene: confFactory.ADDSCENE_PF_WEB,
UserName,
Ticket,
VerifyContent,
})
.then(() => { // succ
// alert('ok')
......@@ -741,10 +744,10 @@
return new Promise(resolve => {
contactFactory.verifyUser({
UserName: UserName
, Opcode: confFactory.VERIFYUSER_OPCODE_VERIFYOK
, Scene: confFactory.ADDSCENE_PF_WEB
, Ticket: Ticket
UserName: UserName,
Opcode: confFactory.VERIFYUSER_OPCODE_VERIFYOK,
Scene: confFactory.ADDSCENE_PF_WEB,
Ticket: Ticket,
}).then(() => { // succ
// alert('ok')
log('friendVerify(' + UserName + ', ' + Ticket + ') done')
......@@ -786,65 +789,65 @@
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
angularIsReady,
// variable
, vars: {
vars: {
loginState : false,
initState : false,
scanCode : null,
heartBeatTimmer : null,
}
},
// funcs
, ding: ding // simple return 'dong'
, emit: window.emit // send event to Node.js
, init: init // initialize WechatyBro @ Browser
, log: log // log to Node.js
, logout: logout // logout current logined user
, send: send // send message to wechat user
, getContact: getContact
, getUserName: getUserName
, getMsgImg: getMsgImg
, getMsgEmoticon: getMsgEmoticon
, getMsgVideo: getMsgVideo
, getMsgVoice: getMsgVoice
, getMsgPublicLinkImg: getMsgPublicLinkImg
, getBaseRequest: getBaseRequest
, getPassticket: getPassticket
, getUploadMediaUrl: getUploadMediaUrl
, sendMedia: sendMedia
, forward: forward
, getCheckUploadUrl: getCheckUploadUrl
ding, // simple return 'dong'
emit: window.emit, // send event to Node.js
init, // initialize WechatyBro @ Browser
log, // log to Node.js
logout, // logout current logined user
send, // send message to wechat user
getContact,
getUserName,
getMsgImg,
getMsgEmoticon,
getMsgVideo,
getMsgVoice,
getMsgPublicLinkImg,
getBaseRequest,
getPassticket,
getUploadMediaUrl,
sendMedia,
forward,
getCheckUploadUrl,
// for Wechaty Contact Class
, contactFind: contactFind
, contactRemark: contactRemark
contactFind,
contactRemark,
// for Wechaty Room Class
, roomCreate: roomCreate
, roomAddMember: roomAddMember
, roomFind: roomFind
, roomDelMember: roomDelMember
, roomModTopic: roomModTopic
roomCreate,
roomAddMember,
roomFind,
roomDelMember,
roomModTopic,
// for Friend Request
, verifyUserRequest: verifyUserRequest
, verifyUserOk: verifyUserOk
verifyUserRequest,
verifyUserOk,
// test purpose
, isLogin: () => {
log('DEPRECATED. use loginState() instead');
isLogin: () => {
log('isLogin() DEPRECATED. use loginState() instead');
return loginState()
}
, loginState: loginState
},
loginState,
}
this.WechatyBro = WechatyBro
......
......@@ -110,14 +110,24 @@ export abstract class Puppet extends EventEmitter implements Sayable {
public abstract self() : Contact
/**
* Message
*/
public abstract forward(message: MediaMessage, contact: Contact | Room) : Promise<boolean>
public abstract say(content: string) : Promise<boolean>
public abstract send(message: Message | MediaMessage) : Promise<boolean>
/**
* Login / Logout
*/
public abstract logonoff() : boolean
public abstract reset(reason?: string) : void
public abstract logout() : Promise<void>
public abstract quit() : Promise<void>
/**
* Misc
*/
public abstract ding() : Promise<string>
/**
......
#!/usr/bin/env ts-node
// tslint:disable:no-shadowed-variable
import * as test from 'blue-tape'
import * as sinon from 'sinon'
// const sinonTest = require('sinon-test')(sinon)
import {
Observable,
// TestScheduler,
} from 'rxjs/Rx'
// import { log } from './config'
// log.level('silly')
import RxQueue from './rx-queue'
test('delay', async function (t) {
const spy = sinon.spy()
const DELAY_TIME = 100
const delayQueue = new RxQueue('delay', DELAY_TIME)
delayQueue.init()
delayQueue.on('o', spy)
const startTime = Date.now()
for (let i = 0; i < 2; i++) {
delayQueue.emit('i', i)
}
const END_CHIPER = 'chiper123'
const wait = new Promise(r => delayQueue.on('o', val => val === END_CHIPER ? r() : ''))
delayQueue.emit('i', END_CHIPER)
await wait
// TODO: use fake timer / TestScheduler to do this
const duration = Math.round((Date.now() - startTime) / 100) * 100
const EXPECT_DURATION = DELAY_TIME * 2
t.equal(duration, EXPECT_DURATION, 'should delayed all message')
t.ok(spy.calledThrice, 'should received 3 calls')
t.deepEqual(spy.args[0][0], 0, 'should received 0')
t.deepEqual(spy.args[1][0], 1, 'should received 1')
t.deepEqual(spy.args[2][0], END_CHIPER, 'should received CHIPER')
})
test('throttle', async function (t) {
const spy = sinon.spy()
const THROTTLE_TIME = 50
const GENERATED_NUM = 7
const GENERATED_INTERVAL = 30
const throttleQueue = new RxQueue('throttle', THROTTLE_TIME)
throttleQueue.init()
throttleQueue.on('o', spy)
const startTime = Date.now()
const wait = new Promise(resolve => {
Observable
.interval(GENERATED_INTERVAL)
.take(GENERATED_NUM)
.subscribe(x => {
// console.log('x', x, Date.now() - startTime)
throttleQueue.emit('i', x)
if (x === GENERATED_NUM - 1) {
resolve()
}
})
})
await wait
// TODO: use fake timer / TestScheduler to do this
const duration = Math.round((Date.now() - startTime) / 100) * 100
const EXPECT_DURATION = Math.round(GENERATED_NUM * GENERATED_INTERVAL / 100) * 100
t.equal(duration, EXPECT_DURATION, 'should cost time as expectation')
const EXPECTED_O_NUM = Math.floor((GENERATED_NUM * GENERATED_INTERVAL) / THROTTLE_TIME)
t.equal(spy.callCount, EXPECTED_O_NUM, 'should received EXPECTED_O_NUM calls')
t.equal(spy.args[0][0], 0, 'should received 0')
t.equal(spy.args[1][0], 2, 'should received 2')
t.equal(spy.args[2][0], 4, 'should received 4')
})
test('debounce', async function (t) {
const spy = sinon.spy()
const DEBOUNCE_TIME = 30
const GENERATED_NUM = 5
const GENERATED_INTERVAL = 10
const debounceQueue = new RxQueue('debounce', DEBOUNCE_TIME)
debounceQueue.init()
debounceQueue.on('o', spy)
const startTime = Date.now()
const wait = new Promise(resolve => {
Observable
.interval(GENERATED_INTERVAL)
.take(GENERATED_NUM)
.subscribe(x => {
// console.log('x', x, Date.now() - startTime)
debounceQueue.emit('i', x)
if (x === GENERATED_NUM - 1) {
setTimeout(resolve, DEBOUNCE_TIME + 1)
}
})
})
await wait
// TODO: use fake timer / TestScheduler to do this
const duration = Math.round((Date.now() - startTime) / 100) * 100
const EXPECT_DURATION = Math.round((GENERATED_NUM * GENERATED_INTERVAL + DEBOUNCE_TIME) / 100) * 100
t.equal(duration, EXPECT_DURATION, 'should cost time as expectation')
const EXPECTED_O_NUM = 1
t.equal(spy.callCount, EXPECTED_O_NUM, 'should received EXPECTED_O_NUM calls')
t.deepEqual(spy.args[0][0], GENERATED_NUM - 1, 'should received GENERATED_NUM - 1')
})
import { EventEmitter } from 'events'
import { Observable } from 'rxjs/Rx'
import { log } from './config'
export type QueueType = 'debounce' | 'delay' | 'throttle'
/**
* [What is a Scheduler?](https://github.com/ReactiveX/rxjs/blob/master/doc/scheduler.md)
* [Writing Marble Tests](https://github.com/ReactiveX/rxjs/blob/master/doc/writing-marble-tests.md)
* [Debounce](http://reactivex.io/documentation/operators/debounce.html)
*/
export class RxQueue extends EventEmitter {
constructor(
public type: QueueType,
public time: number,
) {
super()
log.verbose('RxQueue', 'constructor(type=%s, time=%s)', type, time)
}
public init(): void {
log.verbose('RxQueue', 'init()')
switch (this.type) {
case 'debounce':
this.initDebounce()
break
case 'delay':
this.initDelay()
break
case 'throttle':
this.initThrottle()
break
default:
throw new Error('not supported type: ' + this.type)
}
}
public on(event: 'i', listener: ((...args: any[]) => void)) : this
public on(event: 'o', listener: ((...args: any[]) => void)) : this
public on(event: never, listener: never) : never
public on(event: 'i' | 'o', listener: ((...args: any[]) => void)): this {
super.on(event, listener)
return this
}
public emit(event: 'i', ...args: any[]) : boolean
public emit(event: 'o', ...args: any[]) : boolean
public emit(event: never, listener: never) : never
public emit(event: 'i' | 'o', ...args: any[]): boolean {
return super.emit(event, ...args)
}
// https://stackoverflow.com/a/40088306/1123955
// http://jsbin.com/mulocegalu/1/edit?html,js,output
private initDelay() {
log.verbose('RxQueue', 'initDelay()')
Observable
.fromEvent(this, 'i', (...args: any[]) => args)
// .interval(5 /* ms */)
// .take(30)
.concatMap(args => {
return Observable.of(args) // emit first item right away
.concat(Observable.empty().delay(this.time)) // delay next item
})
.subscribe((args: any[]) => this.emit('o', ...args))
}
private initThrottle() {
log.verbose('RxQueue', 'initThrottle()')
Observable
.fromEvent(this, 'i', (...args: any[]) => args)
.throttle(val => Observable.interval(this.time))
.subscribe((args: any[]) => this.emit('o', ...args))
}
private initDebounce() {
log.verbose('RxQueue', 'initDebounce()')
Observable
.fromEvent(this, 'i', (...args: any[]) => args)
.debounce(val => Observable.interval(this.time))
.subscribe((args: any[]) => this.emit('o', ...args))
}
}
export default RxQueue
......@@ -32,6 +32,7 @@ import {
Raven,
Sayable,
log,
VERSION,
} from './config'
import Contact from './contact'
......@@ -136,7 +137,7 @@ export class Wechaty extends EventEmitter implements Sayable {
super()
log.verbose('Wechaty', 'contructor()')
options.puppet = options.puppet || config.puppet
options.puppet = options.puppet || config.puppet
this.profile = new Profile(options.profile)
......@@ -149,28 +150,28 @@ export class Wechaty extends EventEmitter implements Sayable {
public toString() { return `Wechaty<${this.options.puppet}, ${this.profile.name}>`}
/**
* Return version of Wechaty
*
* @param {boolean} [forceNpm=false] - if set to true, will only return the version in package.json.
* otherwise will return git commit hash if .git exists.
* @returns {string} - the version number
* @example
* console.log(Wechaty.instance().version()) // return '#git[af39df]'
* console.log(Wechaty.instance().version(true)) // return '0.7.9'
* @private
*/
public static version(forceNpm = false): string {
if (!forceNpm) {
const revision = config.gitVersion()
const revision = config.gitRevision()
if (revision) {
return `#git[${revision}]`
}
}
return config.npmVersion()
return VERSION
}
/**
* @private
*/
/**
* Return version of Wechaty
*
* @param {boolean} [forceNpm=false] - if set to true, will only return the version in package.json.
* otherwise will return git commit hash if .git exists.
* @returns {string} - the version number
* @example
* console.log(Wechaty.instance().version()) // return '#git[af39df]'
* console.log(Wechaty.instance().version(true)) // return '0.7.9'
*/
public version(forceNpm?) {
return Wechaty.version(forceNpm)
}
......@@ -523,6 +524,24 @@ export class Wechaty extends EventEmitter implements Sayable {
return
}
/**
* Get the logon / logoff state
*
* @returns {boolean}
* @example
* if (bot.logonoff()) {
* console.log('Bot logined')
* } else {
* console.log('Bot not logined')
* }
*/
public logonoff(): Boolean {
if (!this.puppet) {
return false
}
return this.puppet.logonoff()
}
/**
* Get current user
*
......
......@@ -5,14 +5,14 @@ const { Wechaty } = require('wechaty')
async function main() {
const bot = Wechaty.instance()
try {
await bot.init()
await bot.start()
console.log(`Wechaty v${bot.version()} smoking test passed.`)
} catch (e) {
console.error(e)
// Error!
return 1
} finally {
await bot.quit()
await bot.stop()
}
return 0
}
......
......@@ -88,14 +88,14 @@ test('WechatyBro.ding()', async t => {
t.is(retDing, 'dong', 'should got dong after execute WechatyBro.ding()')
const retCode = await bridge.proxyWechaty('isLogin')
t.is(typeof retCode, 'boolean', 'should got a boolean after call proxyWechaty(isLogin)')
const retCode = await bridge.proxyWechaty('loginState')
t.is(typeof retCode, 'boolean', 'should got a boolean after call proxyWechaty(loginState)')
await bridge.quit()
t.pass('b.quit()')
profile.destroy()
} catch (err) {
t.fail('exception: ' + err.message)
} finally {
profile.destroy()
}
})
......@@ -64,13 +64,13 @@ test('login/logout events', sinonTest(async function (t: test.Test) {
await pw.init()
t.pass('should be inited')
t.is(pw.logined() , false , 'should be not logined')
t.is(pw.logonoff() , false , 'should be not logined')
const EXPECTED_CHIPER = 'loginFired'
const loginPromise = new Promise(r => pw.once('login', _ => r(EXPECTED_CHIPER)))
pw.bridge.emit('login', 'TestPuppetWeb')
t.is(await loginPromise, EXPECTED_CHIPER, 'should fired login event')
t.is(pw.logined(), true , 'should be logined')
t.is(pw.logonoff(), true , 'should be logined')
t.ok((pw.bridge.getUserName as any).called, 'bridge.getUserName should be called')
t.ok((pw.getContact as any).called, 'pw.getContact should be called')
......@@ -81,7 +81,7 @@ test('login/logout events', sinonTest(async function (t: test.Test) {
const logoutPromise = new Promise((res, rej) => pw.once('logout', _ => res('logoutFired')))
pw.bridge.emit('logout')
t.is(await logoutPromise, 'logoutFired', 'should fire logout event')
t.is(pw.logined(), false, 'should be logouted')
t.is(pw.logonoff(), false, 'should be logouted')
await pw.quit()
profile.destroy()
......
......@@ -58,8 +58,8 @@ test('Export of the Framework', async t => {
})
test('Config setting', async t => {
t.ok(config , 'should export Config')
t.ok(config.DEFAULT_PUPPET , 'should has DEFAULT_PUPPET')
t.ok(config , 'should export Config')
t.ok(config.default.DEFAULT_PUPPET , 'should has DEFAULT_PUPPET')
})
test('event:start/stop', async t => {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册