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

Replace WebDriver by Puppeteer (#790) (#860)

REFACTOR
----------------

### 1. Use Chrome Puppeteer to control WebWechat.

All codes related with WebDriver/Selenium had been removed.

1. This makes the logic simple: no server, no https endpoint, no WebSocket anymore;
1. Removed lots of Classes, like `PuppetWebServer`, `PuppetWebBrowser`,  `PuppetWebBrowserDriver`, `ssl-pem` and `PUppetWebBrowserCookie` ;
1. Removed all the `Xvfb` packages and settings;

### 2. Upgrade CircleCI from v1 to v2.

Nothing changed except all the YAML syntax of the CircleCI configuration file.

Workflows look very good, maybe set it up later.

### 3. Dockerfile switch from `alpine` to `node:7` image

The size increased hugely. However, this will let us feel better if we want to add more and more packages into our Docker Image: Debian has better support to the packaging ecosystem.

### 4. Drop Node.js v6 support

No special reasons, it's just tooold.

### 5. Use `blue-tape` to do unit testing, with Typing support.

Do not use AVA anymore, do not wait for TAP anymore. They all support TypeScript not well.

### 6. Separate source code related with `watchdog` to a new NPM module.

It's modulized very well and can be used as needed everywhere: https://github.com/zixia/watchdog

### 7. Created a new module `RxQueue` for `throttle` / `debounce` / `delay` tasks.

Consider publishing a solo NPM module in the future.

### 8. Arrange all the WEB JSON data structure

Save them into a single file: `schema.ts`

### 9. A new Class for manage Wechaty Profile

Use it to `new Profile('botName')` and `save()`/`load()`/`get()`/`set` etc.
上级 336d4b7f
version: 2
jobs:
build:
machine: true
steps:
- checkout
# - setup_remote_docker
- run:
name: Environment Information
command: |
docker --version
docker info
whoami
pwd
ls -lr
- run:
name: Prepare Build
command: |
# apk update && apk add curl curl-dev bash
curl -sSL -o /tmp/bats_v0.4.0.tar.gz https://github.com/sstephenson/bats/archive/v0.4.0.tar.gz
tar -xf /tmp/bats_v0.4.0.tar.gz
sudo bats-0.4.0/install.sh /usr/local
- run:
name: Build Wechaty
command: |
docker run -ti -v "$(pwd)":/mnt nlknguyen/alpine-shellcheck bin/*.sh
./script/docker.sh build
- run:
name: Testing
command: |
./script/docker.sh test
- deploy:
name: Deploy Wechaty Image to Docker Hub
command: |
if [ "${CIRCLE_BRANCH}" == "master" ]; then
curl -X POST -d '{"from":"circleci"}' "$DOCKER_REBUILD_URL"
else
echo "Skipped for not master branch."
fi
...@@ -66,3 +66,4 @@ tags ...@@ -66,3 +66,4 @@ tags
/wechaty-*.*.*.tgz /wechaty-*.*.*.tgz
*.bak *.bak
package-lock.json package-lock.json
.babel.json
...@@ -4,52 +4,40 @@ language: node_js ...@@ -4,52 +4,40 @@ language: node_js
node_js: node_js:
- "7" - "7"
- "8"
os: os:
- linux - linux
- osx - osx
addons: addons:
chrome: stable
apt: apt:
packages: packages:
- jq - jq
- shellcheck - shellcheck
- xvfb - moreutils
- dbus-x11
cache: cache:
directories: directories:
- node_modules - node_modules
before_install: before_install:
- npm config set progress=false
install: install:
- if [ "$TRAVIS_OS_NAME" == 'osx' ]; then brew update; brew cleanup; brew cask cleanup; fi - 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 cask install --force google-chrome; 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 shellcheck; fi
- if [ "$TRAVIS_OS_NAME" == 'osx' ]; then brew install jq; fi - if [ "$TRAVIS_OS_NAME" == 'osx' ]; then brew install jq; fi
- npm install
- if [ "$TRAVIS_OS_NAME" == 'linux' ]; then export DISPLAY=':99.0'; fi
- if [ "$TRAVIS_OS_NAME" == 'linux' ]; then sh -e /etc/init.d/xvfb start; fi
- npm --progress false --loglevel warn install
script: script:
- echo $TRAVIS_OS_NAME - echo $TRAVIS_OS_NAME
- node --version - node --version
- npm --version - npm --version
- if [ "$TRAVIS_OS_NAME" == 'linux' ]; then google-chrome --version; fi
- if [ "$TRAVIS_OS_NAME" == 'osx' ]; then /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version; fi
- shellcheck bin/*.sh - shellcheck bin/*.sh
- npm run lint - npm run test:linux
- npm run dist - npm run test:npm && echo 'Npm packing test is passed'
# https://github.com/SeleniumHQ/docker-selenium/issues/87#issuecomment-187580115
- if [ "$TRAVIS_OS_NAME" == 'linux' ]; then DBUS_SESSION_BUS_ADDRESS=/dev/null WECHATY_LOG=silly npm run test:chrome; fi
- if [ "$TRAVIS_OS_NAME" == 'osx' ]; then WECHATY_LOG=verbose npm run nycava; fi
- WECHATY_HEAD=chrome-headless npm run test:npm && echo 'Npm packing test is passed'
notifications: notifications:
webhooks: webhooks:
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
"operatorPadding": "right" "operatorPadding": "right"
, "indentBase": "firstline" , "indentBase": "firstline"
, "surroundSpace": { , "surroundSpace": {
"colon": [-1, 1], // The first number specify how much space to add to the left, can be negative. The second number is how much space to the right, can be negative. "colon": [1, 1], // The first number specify how much space to add to the left, can be negative. The second number is how much space to the right, can be negative.
"assignment": [1, 1], // The same as above. "assignment": [1, 1], // The same as above.
"arrow": [1, 1], // The same as above. "arrow": [1, 1], // The same as above.
"comment": 2 // Special how much space to add between the trailing comment and the code. "comment": 2 // Special how much space to add between the trailing comment and the code.
......
# FROM node:7
# Wechaty Docker LABEL maintainer="Huan LI <zixia@zixia.net>"
# https://github.com/chatie/wechaty
# ENV NPM_CONFIG_LOGLEVEL warn
# FROM alpine ENV DEBIAN_FRONTEND noninteractive
#
# Docker image for Alpine Linux with latest ShellCheck, a static analysis tool for shell scripts. # Installing the 'apt-utils' package gets rid of the 'debconf: delaying package configuration, since apt-utils is not installed'
# https://hub.docker.com/r/nlknguyen/alpine-shellcheck/ # error message when installing any other package with the apt-get package manager.
# FROM nlknguyen/alpine-shellcheck # https://peteris.rocks/blog/quiet-and-unattended-installation-with-apt-get/
FROM mhart/alpine-node:7 RUN apt-get update && apt-get install -y --no-install-recommends \
MAINTAINER Huan LI <zixia@zixia.net> apt-utils \
bash \
ca-certificates \
curl \
coreutils \
figlet \
jq \
libav-tools \
moreutils \
sudo \
ttf-freefont \
vim \
&& rm -rf /tmp/* /var/lib/apt/lists/*
# https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md
# https://github.com/ebidel/try-puppeteer/blob/master/backend/Dockerfile
# Install latest chrome dev package.
# Note: this also installs the necessary libs so we don't need the previous RUN command.
RUN apt-get update && apt-get install -y wget --no-install-recommends \
&& 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 purge --auto-remove \
&& rm -rf /src/*.deb
# Add chatie user.
RUN groupadd -r bot && useradd -r -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
RUN apk update && apk upgrade \ RUN mkdir /wechaty \
&& apk add \ && chown -R bot:bot /wechaty \
bash \ && mkdir /node_modules
ca-certificates \
chromium-chromedriver \
chromium \
coreutils \
curl \
ffmpeg \
figlet \
jq \
ttf-freefont \
udev \
vim \
xauth \
xvfb \
&& rm -rf /tmp/* /var/cache/apk/*
RUN mkdir /wechaty
WORKDIR /wechaty WORKDIR /wechaty
VOLUME [ "/bot" ]
# Run user as non privileged.
USER bot
# npm `chromedriver` not support alpine linux
# https://github.com/giggio/node-chromedriver/issues/70
COPY package.json . COPY package.json .
RUN sed -i '/chromedriver/d' package.json \ RUN npm install \
&& npm --silent --progress=false install > /dev/null \ && sudo rm -fr /tmp/* ~/.npm
&& rm -fr /tmp/* ~/.npm
# Loading from node_modules Folders: https://nodejs.org/api/modules.html
# If it is not found there, then it moves to the parent directory, and so on, until the root of the file system is reached.
COPY . . COPY . .
RUN sed -i '/chromedriver/d' package.json \ RUN npm run dist
&& npm run dist \
&& npm --silent --progress=false link \
\
&& mkdir /bot \
\
&& ( mkdir /node_modules && cd /node_modules \
&& ln -s /wechaty . \
&& ln -s /wechaty/node_modules/* . \
) \
&& ln -s /wechaty/tsconfig.json / \
&& echo "export * from 'wechaty'" > /index.ts \
\
&& echo 'Linked wechaty to global'
VOLUME [ "/bot" ] # Loading from node_modules Folders: https://nodejs.org/api/modules.html
# If it is not found there, then it moves to the parent directory, and so on, until the root of the file system is reached.
RUN sudo npm link \
&& sudo ln -s /wechaty /node_modules/wechaty \
&& sudo ln -s /wechaty/node_modules/* /node_modules/ \
&& sudo ln -s /wechaty/tsconfig.json / \
&& echo "export * from 'wechaty'" | sudo tee /index.ts \
&& echo 'Linked wechaty to global'
ENTRYPOINT [ "/wechaty/bin/entrypoint.sh" ] ENTRYPOINT [ "/wechaty/bin/entrypoint.sh" ]
CMD [ "" ] CMD [ "" ]
...@@ -65,17 +75,17 @@ CMD [ "" ] ...@@ -65,17 +75,17 @@ CMD [ "" ]
# https://docs.docker.com/docker-cloud/builds/advanced/ # https://docs.docker.com/docker-cloud/builds/advanced/
# http://label-schema.org/rc1/ # http://label-schema.org/rc1/
# #
LABEL org.label-schema.license="ISC" \ LABEL org.label-schema.license="Apache-2.0" \
org.label-schema.build-date="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ org.label-schema.build-date="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
org.label-schema.version="$DOCKER_TAG" \ org.label-schema.version="$DOCKER_TAG" \
org.label-schema.schema-version="$(wechaty-version)" \ org.label-schema.schema-version="$(wechaty-version)" \
org.label-schema.name="Wechaty" \ org.label-schema.name="Wechaty" \
org.label-schema.description="Wechat for Bot" \ org.label-schema.description="Wechat for Bot" \
org.label-schema.usage="https://github.com/wechaty/wechaty/wiki/Docker" \ org.label-schema.usage="https://github.com/chatie/wechaty/wiki/Docker" \
org.label-schema.url="https://www.chatie.io" \ org.label-schema.url="https://www.chatie.io" \
org.label-schema.vendor="AKA Mobi" \ org.label-schema.vendor="AKA Mobi" \
org.label-schema.vcs-ref="$SOURCE_COMMIT" \ org.label-schema.vcs-ref="$SOURCE_COMMIT" \
org.label-schema.vcs-url="https://github.com/wechaty/wechaty" \ org.label-schema.vcs-url="https://github.com/chatie/wechaty" \
org.label-schema.docker.cmd="docker run -ti --rm zixia/wechaty <code.js>" \ org.label-schema.docker.cmd="docker run -ti --rm zixia/wechaty <code.js>" \
org.label-schema.docker.cmd.test="docker run -ti --rm zixia/wechaty test" \ org.label-schema.docker.cmd.test="docker run -ti --rm zixia/wechaty test" \
org.label-schema.docker.cmd.help="docker run -ti --rm zixia/wechaty help" \ org.label-schema.docker.cmd.help="docker run -ti --rm zixia/wechaty help" \
......
#
# Wechaty Docker
# https://github.com/chatie/wechaty
#
# FROM alpine
#
# Docker image for Alpine Linux with latest ShellCheck, a static analysis tool for shell scripts.
# https://hub.docker.com/r/nlknguyen/alpine-shellcheck/
# FROM nlknguyen/alpine-shellcheck
FROM mhart/alpine-node:7
LABEL maintainer="Huan LI <zixia@zixia.net>"
RUN apk update && apk upgrade \
&& apk add \
bash \
ca-certificates \
chromium-chromedriver \
chromium \
coreutils \
curl \
ffmpeg \
figlet \
jq \
moreutils \
ttf-freefont \
udev \
vim \
xauth \
xvfb \
&& rm -rf /tmp/* /var/cache/apk/*
RUN mkdir /wechaty
WORKDIR /wechaty
# npm `chromedriver` not support alpine linux
# https://github.com/giggio/node-chromedriver/issues/70
COPY package.json .
RUN sed -i '/chromedriver/d' package.json \
&& npm --silent --progress=false install > /dev/null \
&& rm -fr /tmp/* ~/.npm
# Loading from node_modules Folders: https://nodejs.org/api/modules.html
# If it is not found there, then it moves to the parent directory, and so on, until the root of the file system is reached.
COPY . .
RUN sed -i '/chromedriver/d' package.json \
&& npm run dist \
&& npm --silent --progress=false link \
\
&& mkdir /bot \
\
&& ( mkdir /node_modules && cd /node_modules \
&& ln -s /wechaty . \
&& ln -s /wechaty/node_modules/* . \
) \
&& ln -s /wechaty/tsconfig.json / \
&& echo "export * from 'wechaty'" > /index.ts \
\
&& echo 'Linked wechaty to global'
VOLUME [ "/bot" ]
ENTRYPOINT [ "/wechaty/bin/entrypoint.sh" ]
CMD [ "" ]
#
# https://docs.docker.com/docker-cloud/builds/advanced/
# http://label-schema.org/rc1/
#
LABEL org.label-schema.license="ISC" \
org.label-schema.build-date="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
org.label-schema.version="$DOCKER_TAG" \
org.label-schema.schema-version="$(wechaty-version)" \
org.label-schema.name="Wechaty" \
org.label-schema.description="Wechat for Bot" \
org.label-schema.usage="https://github.com/wechaty/wechaty/wiki/Docker" \
org.label-schema.url="https://www.chatie.io" \
org.label-schema.vendor="AKA Mobi" \
org.label-schema.vcs-ref="$SOURCE_COMMIT" \
org.label-schema.vcs-url="https://github.com/wechaty/wechaty" \
org.label-schema.docker.cmd="docker run -ti --rm zixia/wechaty <code.js>" \
org.label-schema.docker.cmd.test="docker run -ti --rm zixia/wechaty test" \
org.label-schema.docker.cmd.help="docker run -ti --rm zixia/wechaty help" \
org.label-schema.docker.params="WECHATY_TOKEN=token token from https://www.chatie.io"
#RUN npm test
FROM node:7
ENV NPM_CONFIG_LOGLEVEL warn
# Installing the 'apt-utils' package gets rid of the 'debconf: delaying package configuration, since apt-utils is not installed'
# error message when installing any other package with the apt-get package manager.
# https://peteris.rocks/blog/quiet-and-unattended-installation-with-apt-get/
RUN apt-get update > /dev/null 2>&1 && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
apt-utils \
> /dev/null 2>&1 \
&& rm -rf /var/lib/apt/lists/*
# 1. xvfb depend on xauth
# 2. chromium(with webdriver) depend on libgconf-2-4
RUN apt-get update > /dev/null \
&& DEBIAN_FRONTEND=noninteractive apt-get -qqy --no-install-recommends -o Dpkg::Use-Pty=0 install \
chromium \
figlet \
libgconf-2-4 \
vim \
xauth \
xvfb \
> /dev/null \
&& rm -rf /tmp/* /var/lib/apt/lists/*
RUN mkdir /wechaty
WORKDIR /wechaty
COPY package.json .
RUN npm --progress=false install > /dev/null \
&& npm --progress=false install -g yarn > /dev/null \
&& rm -fr /tmp/*
# && npm install ts-node typescript -g \
COPY . .
RUN npm --progress false link
# Loading from node_modules Folders: https://nodejs.org/api/modules.html
# If it is not found there, then it moves to the parent directory, and so on, until the root of the file system is reached.
RUN mkdir /bot \
&& ln -s /usr/local/lib/node_modules / \
&& ln -s /wechaty/tsconfig.json /
VOLUME [ "/bot" ]
ENTRYPOINT [ "/wechaty/bin/entrypoint.sh" ]
CMD [ "start" ]
#RUN npm test
...@@ -2,9 +2,8 @@ ...@@ -2,9 +2,8 @@
# Test against these versions of Io.js and Node.js. # Test against these versions of Io.js and Node.js.
environment: environment:
WECHATY_LOG: silent
matrix: matrix:
# - nodejs_version: "7" - nodejs_version: "7"
- nodejs_version: "8" - nodejs_version: "8"
# Install scripts. (runs after repo cloning) # Install scripts. (runs after repo cloning)
...@@ -14,7 +13,7 @@ install: ...@@ -14,7 +13,7 @@ install:
# chocolatey install jq - https://chocolatey.org/packages/jq # chocolatey install jq - https://chocolatey.org/packages/jq
- choco install jq - choco install jq
# install modules # install modules
- npm --progress=false install > nul 2>&1 - npm install
# Post-install test scripts. # Post-install test scripts.
test_script: test_script:
...@@ -23,8 +22,7 @@ test_script: ...@@ -23,8 +22,7 @@ test_script:
# https://bugs.chromium.org/p/chromium/issues/detail?id=158372#c6 # 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 - wmic datafile where name="C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe" get Version /value
- npm run lint - npm run lint
- npm run dist - npm run test
- npm run test:chrome:fast
# Don't actually build. # Don't actually build.
build: off build: off
......
...@@ -43,7 +43,7 @@ async function main() { ...@@ -43,7 +43,7 @@ async function main() {
1. Wechaty version: ${wechaty.version()} 1. Wechaty version: ${wechaty.version()}
2. ${os.type()} ${os.arch()} version ${os.release()} memory ${Math.floor(os.freemem() / 1024 / 1024)}/${Math.floor(os.totalmem() / 1024 / 1024)} MB 2. ${os.type()} ${os.arch()} version ${os.release()} memory ${Math.floor(os.freemem() / 1024 / 1024)}/${Math.floor(os.totalmem() / 1024 / 1024)} MB
3. Docker: ${config.dockerMode} 3. Docker: ${config.docker}
4. Node version: ${process.version} 4. Node version: ${process.version}
5. Tcp IPC TEST: ${ipcTestResult} 5. Tcp IPC TEST: ${ipcTestResult}
6. Chromedriver: ${chromedriverVersion} 6. Chromedriver: ${chromedriverVersion}
...@@ -52,8 +52,5 @@ async function main() { ...@@ -52,8 +52,5 @@ async function main() {
} }
try { main()
main() .catch(err => console.error('main() exception: %s', err))
} catch (err) {
console.error('main() exception: %s', err.message || err)
}
...@@ -134,8 +134,9 @@ function wechaty::runBot() { ...@@ -134,8 +134,9 @@ function wechaty::runBot() {
case "$botFile" in case "$botFile" in
*.js) *.js)
if [ "$NODE_ENV" != "production" ]; then if [ "$NODE_ENV" != "production" ]; then
echo "Executing babel-node --presets es2015 $*" echo "Executing babel-node --presets env $*"
babel-node --presets es2015 "$@" & # https://stackoverflow.com/a/34025957/1123955
BABEL_DISABLE_CACHE=1 babel-node --presets env "$@" &
else else
echo "Executing node $*" echo "Executing node $*"
node "$@" & node "$@" &
...@@ -206,7 +207,7 @@ HELP ...@@ -206,7 +207,7 @@ HELP
function main() { function main() {
# issue #84 # issue #84
echo -e 'nameserver 114.114.114.114\nnameserver 114.114.115.115' >> /etc/resolv.conf echo -e 'nameserver 114.114.114.114\nnameserver 114.114.115.115' | sudo tee -a /etc/resolv.conf > /dev/null
wechaty::banner wechaty::banner
figlet Connecting figlet Connecting
...@@ -260,7 +261,7 @@ function main() { ...@@ -260,7 +261,7 @@ function main() {
;; ;;
test) test)
WECHATY_LOG=silent npm run test WECHATY_LOG=silent npm run test:linux
;; ;;
# #
......
#!/bin/bash
#
# Wechaty Xvfb Browser Loader
#
# https://github.com/wechaty/wechaty
#
# Original Code Credit:
# https://github.com/mark-adams/docker-chromium-xvfb/blob/master/images/base/xvfb-chromium
# http://stackoverflow.com/a/30336424/1123955
#
set -e
_kill_procs() {
kill -TERM $pidXvfbChromium
wait $pidXvfbChromium
}
# Setup a trap to catch SIGTERM and relay it to child processes
trap _kill_procs SIGTERM
XVFB_WHD=${XVFB_WHD:-640x480x8}
chromiumCmd=$(which chromium || which chromium-browser) || echo 'ERROR: can not found chromium binary'
xvfb-run -a "$chromiumCmd" --no-sandbox $@ &
pidXvfbChromium=$!
wait $pidXvfbChromium
#!/bin/sh
# $Id: xvfb-run 2027 2004-11-16 14:54:16Z branden $
# This script starts an instance of Xvfb, the "fake" X server, runs a command
# with that server available, and kills the X server when done. The return
# value of the command becomes the return value of this script, except in cases
# where this script encounters an error.
#
# If anyone is using this to build a Debian package, make sure the package
# Build-Depends on xvfb, xbase-clients, and xfonts-base.
set -e
PROGNAME=xvfb-run
SERVERNUM=99
AUTHFILE=
ERRORFILE=/dev/null
STARTWAIT=3
XVFBARGS="-screen 0 640x480x8"
LISTENTCP="-nolisten tcp"
XAUTHPROTO=.
# Query the terminal to establish a default number of columns to use for
# displaying messages to the user. This is used only as a fallback in the event
# the COLUMNS variable is not set. ($COLUMNS can react to SIGWINCH while the
# script is running, and this cannot, only being calculated once.)
DEFCOLUMNS=$(stty size 2>/dev/null | awk '{print $2}') || true
if ! expr "$DEFCOLUMNS" : "[[:digit:]]\+$" >/dev/null 2>&1; then
DEFCOLUMNS=80
fi
# Display a message, wrapping lines at the terminal width.
message () {
echo "$PROGNAME: $*" | fmt -t -w ${COLUMNS:-$DEFCOLUMNS}
}
# Display an error message.
error () {
message "error: $*" >&2
}
# Display a usage message.
usage () {
if [ -n "$*" ]; then
message "usage error: $*"
fi
cat <<EOF
Usage: $PROGNAME [OPTION ...] COMMAND
Run COMMAND (usually an X client) in a virtual X server environment.
Options:
-a --auto-servernum try to get a free server number, starting at
--server-num
-e FILE --error-file=FILE file used to store xauth errors and Xvfb
output (default: $ERRORFILE)
-f FILE --auth-file=FILE file used to store auth cookie
(default: ./.Xauthority)
-h --help display this usage message and exit
-n NUM --server-num=NUM server number to use (default: $SERVERNUM)
-l --listen-tcp enable TCP port listening in the X server
-p PROTO --xauth-protocol=PROTO X authority protocol name to use
(default: xauth command's default)
-s ARGS --server-args=ARGS arguments (other than server number and
"-nolisten tcp") to pass to the Xvfb server
(default: "$XVFBARGS")
-w DELAY --wait=DELAY delay in seconds to wait for Xvfb to start
before running COMMAND (default: $STARTWAIT)
EOF
}
# Find a free server number by looking at .X*-lock files in /tmp.
find_free_servernum() {
# Sadly, the "local" keyword is not POSIX. Leave the next line commented in
# the hope Debian Policy eventually changes to allow it in /bin/sh scripts
# anyway.
#local i
i=$SERVERNUM
while [ -f /tmp/.X$i-lock ]; do
i=$(($i + 1))
done
echo $i
}
# Clean up files
clean_up() {
if [ -e "$AUTHFILE" ]; then
XAUTHORITY=$AUTHFILE xauth remove ":$SERVERNUM" >>"$ERRORFILE" 2>&1
fi
if [ -n "$XVFB_RUN_TMPDIR" ]; then
if ! rm -r "$XVFB_RUN_TMPDIR"; then
error "problem while cleaning up temporary directory"
exit 5
fi
fi
}
# Parse the command line.
ARGS=$(getopt --options +ae:f:hn:lp:s:w: \
--long auto-servernum,error-file:,auth-file:,help,server-num:,listen-tcp,xauth-protocol:,server-args:,wait: \
--name "$PROGNAME" -- "$@")
GETOPT_STATUS=$?
if [ $GETOPT_STATUS -ne 0 ]; then
error "internal error; getopt exited with status $GETOPT_STATUS"
exit 6
fi
eval set -- "$ARGS"
while :; do
case "$1" in
-a|--auto-servernum) SERVERNUM=$(find_free_servernum); AUTONUM="yes" ;;
-e|--error-file) ERRORFILE="$2"; shift ;;
-f|--auth-file) AUTHFILE="$2"; shift ;;
-h|--help) SHOWHELP="yes" ;;
-n|--server-num) SERVERNUM="$2"; shift ;;
-l|--listen-tcp) LISTENTCP="" ;;
-p|--xauth-protocol) XAUTHPROTO="$2"; shift ;;
-s|--server-args) XVFBARGS="$2"; shift ;;
-w|--wait) STARTWAIT="$2"; shift ;;
--) shift; break ;;
*) error "internal error; getopt permitted \"$1\" unexpectedly"
exit 6
;;
esac
shift
done
if [ "$SHOWHELP" ]; then
usage
exit 0
fi
if [ -z "$*" ]; then
usage "need a command to run" >&2
exit 2
fi
if ! which xauth >/dev/null; then
error "xauth command not found"
exit 3
fi
# tidy up after ourselves
trap clean_up EXIT
# If the user did not specify an X authorization file to use, set up a temporary
# directory to house one.
if [ -z "$AUTHFILE" ]; then
XVFB_RUN_TMPDIR="$(mktemp -d -t $PROGNAME.XXXXXX)"
# Create empty file to avoid xauth warning
#AUTHFILE=$(tempfile -n "$XVFB_RUN_TMPDIR/Xauthority")
AUTHFILE=$(mktemp -p "$XVFB_RUN_TMPDIR" Xauthority.XXXXXXXX)
fi
# Start Xvfb.
MCOOKIE=$(mcookie)
tries=10
while [ $tries -gt 0 ]; do
tries=$(( $tries - 1 ))
XAUTHORITY=$AUTHFILE xauth source - << EOF >>"$ERRORFILE" 2>&1
add :$SERVERNUM $XAUTHPROTO $MCOOKIE
EOF
XAUTHORITY=$AUTHFILE Xvfb ":$SERVERNUM" $XVFBARGS $LISTENTCP >>"$ERRORFILE" 2>&1 &
XVFBPID=$!
sleep "$STARTWAIT"
if kill -0 $XVFBPID 2>/dev/null; then
break
elif [ -n "$AUTONUM" ]; then
# The display is in use so try another one (if '-a' was specified).
SERVERNUM=$((SERVERNUM + 1))
SERVERNUM=$(find_free_servernum)
continue
fi
error "Xvfb failed to start" >&2
exit 1
done
# Start the command and save its exit status.
set +e
DISPLAY=:$SERVERNUM XAUTHORITY=$AUTHFILE "$@" 2>&1
RETVAL=$?
set -e
# Kill Xvfb now that the command has exited.
kill $XVFBPID
# Return the executed command's exit status.
exit $RETVAL
# vim:set ai et sts=4 sw=4 tw=80:
machine:
services:
- docker
dependencies:
pre:
- sudo add-apt-repository ppa:duggan/bats --yes
- sudo apt-get update -qq
- sudo apt-get install -qq bats
override:
- echo 'Ignore CircleCI defaults'
test:
override:
- docker --version
- docker info
- docker run -ti -v "$(pwd)":/mnt nlknguyen/alpine-shellcheck bin/*.sh
- ./script/docker.sh build
- ./script/docker.sh test
deployment:
master:
branch: master
commands:
- curl -X POST -d '{"from":"circleci"}' "$DOCKER_REBUILD_URL"
...@@ -64,7 +64,7 @@ bot ...@@ -64,7 +64,7 @@ bot
/** /**
* Main Contact Bot start from here * Main Contact Bot start from here
*/ */
main() onLogin()
}) })
.on('logout' , user => log.info('Bot', `${user.name()} logouted`)) .on('logout' , user => log.info('Bot', `${user.name()} logouted`))
...@@ -87,7 +87,7 @@ bot.init() ...@@ -87,7 +87,7 @@ bot.init()
/** /**
* Main Contact Bot * Main Contact Bot
*/ */
async function main() { async function onLogin() {
const contactList = await Contact.findAll() const contactList = await Contact.findAll()
log.info('Bot', '#######################') log.info('Bot', '#######################')
...@@ -139,7 +139,10 @@ async function main() { ...@@ -139,7 +139,10 @@ async function main() {
const avatarFileName = `${i}-${contact.name()}.jpg` const avatarFileName = `${i}-${contact.name()}.jpg`
const avatarReadStream = await contact.avatar() const avatarReadStream = await contact.avatar()
const avatarWriteStream = createWriteStream(avatarFileName) const avatarWriteStream = createWriteStream(avatarFileName)
const wait = new Promise(r => avatarWriteStream.once('close', r))
avatarReadStream.pipe(avatarWriteStream) avatarReadStream.pipe(avatarWriteStream)
await wait
log.info('Bot', 'Contact: %s: %s with avatar file: %s', contact.weixin(), contact.name(), avatarFileName) log.info('Bot', 'Contact: %s: %s with avatar file: %s', contact.weixin(), contact.name(), avatarFileName)
...@@ -149,7 +152,10 @@ async function main() { ...@@ -149,7 +152,10 @@ async function main() {
} }
} }
const SLEEP = 7 // const SLEEP = 7
log.info('Bot', 'I will re-dump contact weixin id & names after %d second... ', SLEEP) // log.info('Bot', 'I will re-dump contact weixin id & names after %d second... ', SLEEP)
setTimeout(main, SLEEP * 1000) // setTimeout(main, SLEEP * 1000)
await bot.logout()
await bot.quit()
} }
...@@ -35,7 +35,7 @@ import * as qrcodeTerminal from 'qrcode-terminal' ...@@ -35,7 +35,7 @@ import * as qrcodeTerminal from 'qrcode-terminal'
import { import {
config, config,
MediaMessage, MediaMessage,
MsgType, // MsgType,
Wechaty, Wechaty,
} from '../' } from '../'
const bot = Wechaty.instance({ profile: config.DEFAULT_PROFILE }) const bot = Wechaty.instance({ profile: config.DEFAULT_PROFILE })
...@@ -55,18 +55,18 @@ bot ...@@ -55,18 +55,18 @@ bot
// console.log(inspect(m)) // console.log(inspect(m))
// saveRawObj(m.rawObj) // saveRawObj(m.rawObj)
if ( m.type() === MsgType.IMAGE // if ( m.type() === MsgType.IMAGE
|| m.type() === MsgType.EMOTICON // || m.type() === MsgType.EMOTICON
|| m.type() === MsgType.VIDEO // || m.type() === MsgType.VIDEO
|| m.type() === MsgType.VOICE // || m.type() === MsgType.VOICE
|| m.type() === MsgType.MICROVIDEO // || m.type() === MsgType.MICROVIDEO
|| m.type() === MsgType.APP // || m.type() === MsgType.APP
|| (m.type() === MsgType.TEXT && m.typeSub() === MsgType.LOCATION) // LOCATION // || (m.type() === MsgType.TEXT && m.typeSub() === MsgType.LOCATION) // LOCATION
) { // ) {
if (m instanceof MediaMessage) { if (m instanceof MediaMessage) {
saveMediaFile(m) saveMediaFile(m)
}
} }
// }
}) })
.init() .init()
.catch(e => console.error('bot.init() error: ' + e)) .catch(e => console.error('bot.init() error: ' + e))
......
import { export {
config, config,
log, log,
Sayable, Sayable,
} from './src/config' } from './src/config'
import Contact from './src/contact' export { Contact } from './src/contact'
// ISSUE #70 import { FriendRequest } from './src/friend-request' // ISSUE #70 import { FriendRequest } from './src/friend-request'
import FriendRequest from './src/puppet-web/friend-request' export {
PuppetWebFriendRequest as FriendRequest,
import IoClient from './src/io-client' } from './src/puppet-web/friend-request'
import { export {
Message,
MediaMessage,
MsgType, MsgType,
} from './src/message' } from './src/puppet-web/schema'
import Puppet from './src/puppet'
import PuppetWeb from './src/puppet-web/'
import Room from './src/room'
import UtilLib from './src/util-lib'
import Wechaty from './src/wechaty'
const VERSION = require('./package.json').version
export { IoClient } from './src/io-client'
export { export {
config,
Contact,
FriendRequest,
IoClient,
Message, Message,
MediaMessage, MediaMessage,
MsgType, } from './src/message'
Puppet, export { Profile } from './src/profile'
PuppetWeb, export { Puppet } from './src/puppet'
Room, export { PuppetWeb } from './src/puppet-web/'
Sayable, export { Room } from './src/room'
UtilLib, export { Misc } from './src/misc'
VERSION,
Wechaty,
log, // for convenionce use npmlog with environment variable LEVEL
}
export default Wechaty export const VERSION = require('./package.json').version
import Wechaty from './src/wechaty'
export { Wechaty }
export default Wechaty
{ {
"name": "wechaty", "name": "wechaty",
"version": "0.9.5", "version": "0.10.1",
"description": "Wechat for Bot(Personal Account)", "description": "Wechat for Bot(Personal Account)",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"wechaty": { "wechaty": {
"DEFAULT_HEAD": "chrome",
"DEFAULT_PUPPET": "web", "DEFAULT_PUPPET": "web",
"DEFAULT_PROFILE": "demo", "DEFAULT_PROFILE": "demo",
"DEFAULT_PORT": 8788,
"DEFAULT_PROTOCOL": "io|0.0.1", "DEFAULT_PROTOCOL": "io|0.0.1",
"DEFAULT_TOKEN": "WECHATY_IO_TOKEN", "DEFAULT_TOKEN": "WECHATY_IO_TOKEN",
"DEFAULT_APIHOST": "api.chatie.io", "DEFAULT_APIHOST": "api.chatie.io"
"CMD_CHROMIUM": "/wechaty/bin/xvfb-chromium"
}, },
"scripts": { "scripts": {
"ava": "ava --verbose --extension ts",
"ts-node": "ts-node", "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 && jq \"del (.files)\" < package.json > dist/package.json && 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", "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",
...@@ -23,25 +19,18 @@ ...@@ -23,25 +19,18 @@
"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 && 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 && ts-node script/sort-contributiveness.ts < CHANGELOG.md > CHANGELOG.new.md && cat CHANGELOG.md >> CHANGELOG.new.md && mv CHANGELOG.new.md CHANGELOG.md",
"doctor": "npm run check-node-version && ts-node bin/doctor", "doctor": "npm run check-node-version && ts-node bin/doctor",
"clean": "shx rm -fr dist/*", "clean": "shx rm -fr dist/*",
"check-node-version": "check-node-version --node \">= 6.9\"", "check-node-version": "check-node-version --node \">= 7\"",
"lint": "npm run clean && npm run check-node-version && npm run lint:es && npm run lint:ts", "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:es": "eslint \"{bin,example,src,test}/**/*.js\" --ignore-pattern=\"test/fixture/**\"",
"lint:ts": "tslint --version && tslint --project tsconfig.json --type-check \"{bin,example,src,test}/**/*.ts\" --exclude \"test/fixture/**\" --exclude \"dist/\" && npm run clean && tsc --noEmit", "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:sh": "bash -n bin/*.sh", "lint:sh": "bash -n bin/*.sh",
"nycava": "WECHATY_HEAD=chrome nyc ava --serial --fail-fast --verbose --timeout=5m \"dist/{src,test}/**/*.spec.js\"",
"pretest": "npm run clean && npm run lint && npm run dist",
"sloc": "sloc bin example src test index.ts --details --format cli-table --keys total,source,comment && sloc bin example src test index.ts", "sloc": "sloc bin example src test index.ts --details --format cli-table --keys total,source,comment && sloc bin example src test index.ts",
"test": "npm run test:chrome", "pretest": "npm run clean && npm run lint",
"posttest": "npm run clean && npm run sloc", "test": "blue-tape -r ts-node/register -r source-map-support/register \"src/**/*.spec.ts\" \"tests/**/*.spec.ts\"",
"test:chrome": "cross-env LC_ALL=C WECHATY_HEAD=chrome ava --serial --fail-fast --verbose --timeout=5m \"dist/{src,test}/**/*.spec.js\"", "posttest": "npm run sloc",
"test:bug": "cross-env LC_ALL=C WECHATY_HEAD=chrome WECHATY_LOG=silly ava --serial --fail-fast --verbose --timeout=5m \"dist/src/message.spec.js\"", "test:linux": "npm run pretest && parallel ts-node -- ./src/**/*.spec.ts ./test/**/*.spec.ts && npm run posttest",
"test:chrome:fast": "cross-env LC_ALL=C WECHATY_HEAD=chrome ava --concurrency 5 --fail-fast --timeout=5m \"dist/{src,test}/**/*.spec.js\"", "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",
"testdev": "cross-env LC_ALL=C WECHATY_LOG=silly ava --ext ts --serial --verbose --fail-fast --timeout=2m",
"testdist": "cross-env WECHATY_LOG=SILLY WECHATY_HEAD=chrome ava --ext ts --verbose --fail-fast --timeout=2m",
"test:pack": "npm run dist && rm -fr package wechaty-*.*.*.tgz && npm pack --verbose && tar zxvf wechaty-*.*.*.tgz",
"test:npm": "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",
"io-client": "ts-node bin/io-client", "io-client": "ts-node bin/io-client",
"dev": "ts-node dev.ts",
"demo": "ts-node example/ding-dong-bot.ts", "demo": "ts-node example/ding-dong-bot.ts",
"start": "npm run demo" "start": "npm run demo"
}, },
...@@ -83,67 +72,58 @@ ...@@ -83,67 +72,58 @@
"node": true, "node": true,
"es6": true "es6": true
}, },
"plugins": [
"ava"
],
"parser": "babel-eslint", "parser": "babel-eslint",
"parserOptions": { "parserOptions": {
"ecmaVersion": 7, "ecmaVersion": 7,
"sourceType": "module", "sourceType": "module",
"ecmaFeatures": { "ecmaFeatures": {
"impliedStrict": true "impliedStrict": true
}, }
"extends": "plugin:ava/recommended"
} }
}, },
"ava": {
"require": [
"ts-node/register"
]
},
"engines": { "engines": {
"node": ">= 6.9.0" "node": ">= 7"
}, },
"dependencies": { "dependencies": {
"@types/express": "4.0.37",
"@types/node": "8.0.33", "@types/node": "8.0.33",
"@types/ws": "3.2.0", "@types/ws": "3.2.0",
"bl": "1.2.1", "bl": "1.2.1",
"body-parser": "1.18.2", "body-parser": "1.18.2",
"brolog": "1.2.8", "brolog": "1.2.8",
"chromedriver": "2.33.0",
"express": "4.15.2",
"hot-import": "0.1.21", "hot-import": "0.1.21",
"ps-tree": "1.1.0", "puppeteer": "0.11.0",
"raven": "2.2.1", "raven": "2.2.1",
"request": "2.83.0", "request": "2.83.0",
"retry-promise": "1.0.0", "retry-promise": "1.0.0",
"selenium-webdriver": "3.6.0", "rxjs": "^5.4.3",
"state-switch": "0.1.13", "state-switch": "0.1.13",
"ws": "3.2.0", "ws": "3.2.0",
"xml2js": "0.4.19" "xml2js": "0.4.19"
}, },
"devDependencies": { "devDependencies": {
"@types/blue-tape": "^0.1.31",
"@types/body-parser": "1.16.5", "@types/body-parser": "1.16.5",
"@types/express": "4.0.37",
"@types/fluent-ffmpeg": "2.1.4", "@types/fluent-ffmpeg": "2.1.4",
"@types/glob": "5.0.33", "@types/glob": "5.0.33",
"@types/mime": "2.0.0", "@types/mime": "2.0.0",
"@types/puppeteer": "0.10.1",
"@types/raven": "2.1.2", "@types/raven": "2.1.2",
"@types/request": "2.0.4", "@types/request": "2.0.4",
"@types/selenium-webdriver": "3.0.7",
"@types/sinon": "2.3.5", "@types/sinon": "2.3.5",
"@types/xml2js": "0.4.0", "@types/xml2js": "0.4.0",
"apiai": "4.0.3", "apiai": "4.0.3",
"ava": "0.22.0",
"babel-cli": "6.26.0", "babel-cli": "6.26.0",
"babel-eslint": "8.0.1", "babel-eslint": "8.0.1",
"babel-preset-es2015": "6.24.1", "babel-preset-env": "^1.6.0",
"blue-tape": "^1.0.0",
"body-parser": "1.18.2",
"check-node-version": "2.1.0", "check-node-version": "2.1.0",
"cookie-parser": "1.4.3", "cookie-parser": "1.4.3",
"coveralls": "3.0.0", "coveralls": "3.0.0",
"cross-env": "5.0.5", "cross-env": "5.0.5",
"eslint": "4.8.0", "eslint": "4.8.0",
"eslint-plugin-ava": "4.2.2", "express": "4.15.2",
"finis": "0.0.2", "finis": "0.0.2",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"glob": "7.1.2", "glob": "7.1.2",
......
#!/usr/bin/env ts-node
/** /**
* Wechaty - https://github.com/chatie/wechaty * Wechaty - https://github.com/chatie/wechaty
* *
...@@ -16,29 +17,27 @@ ...@@ -16,29 +17,27 @@
* limitations under the License. * limitations under the License.
* *
*/ */
import { test } from 'ava' // tslint:disable:no-shadowed-variable
import * as test from 'blue-tape'
// import * as sinon from 'sinon'
// const sinonTest = require('sinon-test')(sinon)
import { config } from './config' import { config } from './config'
import { Puppet } from './puppet' import { Puppet } from './puppet'
test('important variables', t => { test('important variables', async t => {
t.true('head' in config, 'should exist `head` in Config')
t.true('puppet' in config, 'should exist `puppet` in Config') t.true('puppet' in config, 'should exist `puppet` in Config')
t.true('apihost' in config, 'should exist `apihost` in Config') t.true('apihost' in config, 'should exist `apihost` in Config')
t.true('port' in config, 'should exist `port` in Config')
t.true('profile' in config, 'should exist `profile` in Config') t.true('profile' in config, 'should exist `profile` in Config')
t.true('token' in config, 'should exist `token` in Config') t.true('token' in config, 'should exist `token` in Config')
t.truthy(config.DEFAULT_PUPPET , 'should export DEFAULT_PUPPET') t.ok(config.DEFAULT_PUPPET , 'should export DEFAULT_PUPPET')
t.truthy(config.DEFAULT_PORT , 'should export DEFAULT_PORT') t.ok(config.DEFAULT_PROFILE , 'should export DEFAULT_PROFILE')
t.truthy(config.DEFAULT_PROFILE , 'should export DEFAULT_PROFILE') t.ok(config.DEFAULT_PROTOCOL , 'should export DEFAULT_PROTOCOL')
t.truthy(config.DEFAULT_HEAD , 'should export DEFAULT_HEAD') t.ok(config.DEFAULT_APIHOST , 'should export DEFAULT_APIHOST')
t.truthy(config.DEFAULT_PROTOCOL , 'should export DEFAULT_PROTOCOL')
t.truthy(config.DEFAULT_APIHOST , 'should export DEFAULT_APIHOST')
t.truthy(config.CMD_CHROMIUM , 'should export CMD_CHROMIUM')
}) })
test('validApiHost()', t => { test('validApiHost()', async t => {
const OK_APIHOSTS = [ const OK_APIHOSTS = [
'api.wechaty.io', 'api.wechaty.io',
'wechaty.io:8080', 'wechaty.io:8080',
...@@ -48,7 +47,7 @@ test('validApiHost()', t => { ...@@ -48,7 +47,7 @@ test('validApiHost()', t => {
'wechaty.io/', 'wechaty.io/',
] ]
OK_APIHOSTS.forEach(apihost => { OK_APIHOSTS.forEach(apihost => {
t.notThrows(() => { t.doesNotThrow(() => {
config.validApiHost(apihost) config.validApiHost(apihost)
}) })
}, 'should not row for right apihost') }, 'should not row for right apihost')
...@@ -60,10 +59,16 @@ test('validApiHost()', t => { ...@@ -60,10 +59,16 @@ test('validApiHost()', t => {
}) })
test('puppetInstance()', t => { test('puppetInstance()', async t => {
// BUG Compitable with Win32 CI
// global instance infected across unit tests... :(
const bak = config.puppetInstance()
config.puppetInstance(null)
t.throws(() => { t.throws(() => {
config.puppetInstance() config.puppetInstance()
}, Error, 'should throw when not initialized') }, Error, 'should throw when not initialized')
config.puppetInstance(bak)
const EXPECTED = <Puppet>{userId: 'test'} const EXPECTED = <Puppet>{userId: 'test'}
const mockPuppet = EXPECTED const mockPuppet = EXPECTED
...@@ -77,17 +82,5 @@ test('puppetInstance()', t => { ...@@ -77,17 +82,5 @@ test('puppetInstance()', t => {
config.puppetInstance() config.puppetInstance()
}, Error, 'should throw after set to null') }, Error, 'should throw after set to null')
}) config.puppetInstance(bak)
test('dockerMode', t => {
t.true('dockerMode' in config, 'should identify docker env by `dockerMode`')
if ('C9_PORT' in process.env) {
t.is(config.dockerMode, false, 'should not in docker mode in Cloud9 IDE')
} else if (require('is-ci')) {
t.is(config.dockerMode, false, 'should not in docker mode in Continuous Integeration System')
} else {
// a custom running envioronment, maybe docker, maybe not
}
}) })
...@@ -85,29 +85,19 @@ export type PuppetName = 'web' ...@@ -85,29 +85,19 @@ export type PuppetName = 'web'
| 'android' | 'android'
| 'ios' | 'ios'
export type HeadName = 'chrome'
| 'chrome-headless'
| 'phantomjs'
| 'firefox'
export interface ConfigSetting { export interface ConfigSetting {
DEFAULT_HEAD: HeadName
DEFAULT_PUPPET: PuppetName DEFAULT_PUPPET: PuppetName
DEFAULT_APIHOST: string DEFAULT_APIHOST: string
DEFAULT_PROFILE: string DEFAULT_PROFILE: string
DEFAULT_TOKEN: string DEFAULT_TOKEN: string
DEFAULT_PROTOCOL: string DEFAULT_PROTOCOL: string
CMD_CHROMIUM: string
DEFAULT_PORT: number
port: number
profile: string profile: string
token: string token: string
debug: boolean debug: boolean
puppet: PuppetName puppet: PuppetName
head: HeadName
apihost: string apihost: string
validApiHost: (host: string) => boolean validApiHost: (host: string) => boolean
...@@ -123,7 +113,7 @@ export interface ConfigSetting { ...@@ -123,7 +113,7 @@ export interface ConfigSetting {
gitVersion(): string | null, gitVersion(): string | null,
npmVersion(): string, npmVersion(): string,
dockerMode: boolean, docker: boolean,
} }
/* tslint:disable:variable-name */ /* tslint:disable:variable-name */
/* tslint:disable:no-var-requires */ /* tslint:disable:no-var-requires */
...@@ -134,7 +124,7 @@ export const config: ConfigSetting = require('../package.json').wechaty ...@@ -134,7 +124,7 @@ export const config: ConfigSetting = require('../package.json').wechaty
*/ */
Object.assign(config, { Object.assign(config, {
apihost: process.env['WECHATY_APIHOST'] || config.DEFAULT_APIHOST, apihost: process.env['WECHATY_APIHOST'] || config.DEFAULT_APIHOST,
head: process.env['WECHATY_HEAD'] || config.DEFAULT_HEAD, // head: process.env['WECHATY_HEAD'] || config.DEFAULT_HEAD,
puppet: process.env['WECHATY_PUPPET'] || config.DEFAULT_PUPPET, puppet: process.env['WECHATY_PUPPET'] || config.DEFAULT_PUPPET,
validApiHost, validApiHost,
}) })
...@@ -151,7 +141,7 @@ validApiHost(config.apihost) ...@@ -151,7 +141,7 @@ validApiHost(config.apihost)
* 2. ENVIRONMENT VARIABLES (only) * 2. ENVIRONMENT VARIABLES (only)
*/ */
Object.assign(config, { Object.assign(config, {
port: process.env['WECHATY_PORT'] || null, // 0 for disable port // 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 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 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, debug: !!(process.env['WECHATY_DEBUG']) || false,
...@@ -160,16 +150,16 @@ Object.assign(config, { ...@@ -160,16 +150,16 @@ Object.assign(config, {
/** /**
* 3. Service Settings * 3. Service Settings
*/ */
Object.assign(config, { // Object.assign(config, {
// get PORT form cloud service env, ie: heroku // get PORT form cloud service env, ie: heroku
httpPort: process.env['PORT'] || process.env['WECHATY_PORT'] || config.DEFAULT_PORT, // httpPort: process.env['PORT'] || process.env['WECHATY_PORT'] || config.DEFAULT_PORT,
}) // })
/** /**
* 4. Envioronment Identify * 4. Envioronment Identify
*/ */
Object.assign(config, { Object.assign(config, {
dockerMode: !!process.env['WECHATY_DOCKER'], docker: !!process.env['WECHATY_DOCKER'],
isGlobal: isWechatyInstalledGlobal(), isGlobal: isWechatyInstalledGlobal(),
}) })
...@@ -192,7 +182,7 @@ function puppetInstance(instance: Puppet): void ...@@ -192,7 +182,7 @@ function puppetInstance(instance: Puppet): void
function puppetInstance(instance?: Puppet | null): Puppet | void { function puppetInstance(instance?: Puppet | null): Puppet | void {
if (instance === undefined) { if (typeof instance === 'undefined') {
if (!this._puppetInstance) { if (!this._puppetInstance) {
throw new Error('no puppet instance') throw new Error('no puppet instance')
} }
...@@ -259,35 +249,6 @@ Object.assign(config, { ...@@ -259,35 +249,6 @@ Object.assign(config, {
puppetInstance, puppetInstance,
}) })
export type WatchdogFoodName = 'HEARTBEAT'
| 'POISON'
| 'SCAN'
export interface WatchdogFood {
data: any,
timeout?: number, // millisecond
type?: WatchdogFoodName,
}
export interface ScanInfo {
url: string,
code: number,
}
/**
* from Message
*/
export interface RecommendInfo {
UserName: string,
NickName: string, // display_name
Content: string, // request message
HeadImgUrl: string, // message.RecommendInfo.HeadImgUrl
Ticket: string, // a pass token
VerifyFlag: number,
}
export interface Sayable { export interface Sayable {
say(content: string, replyTo?: any|any[]): Promise<boolean> say(content: string, replyTo?: any|any[]): Promise<boolean>
} }
......
...@@ -23,14 +23,14 @@ import { ...@@ -23,14 +23,14 @@ import {
Raven, Raven,
Sayable, Sayable,
log, log,
} from './config' } from './config'
import { import {
Message, Message,
MediaMessage, MediaMessage,
} from './message' } from './message'
import { PuppetWeb } from './puppet-web' import Misc from './misc'
import { UtilLib } from './util-lib' import PuppetWeb from './puppet-web'
import { Wechaty } from './wechaty' import Wechaty from './wechaty'
export interface ContactObj { export interface ContactObj {
address: string, address: string,
...@@ -358,7 +358,7 @@ export class Contact implements Sayable { ...@@ -358,7 +358,7 @@ export class Contact implements Sayable {
* @example * @example
* const name = contact.name() * const name = contact.name()
*/ */
public name() { return UtilLib.plainText(this.obj && this.obj.name || '') } public name() { return Misc.plainText(this.obj && this.obj.name || '') }
public alias(): string | null public alias(): string | null
...@@ -537,12 +537,12 @@ export class Contact implements Sayable { ...@@ -537,12 +537,12 @@ export class Contact implements Sayable {
} }
try { try {
const hostname = await (config.puppetInstance() as PuppetWeb).browser.hostname() const hostname = await (config.puppetInstance() as PuppetWeb).hostname()
const avatarUrl = `http://${hostname}${this.obj.avatar}&type=big` // add '&type=big' to get big image const avatarUrl = `http://${hostname}${this.obj.avatar}&type=big` // add '&type=big' to get big image
const cookies = await (config.puppetInstance() as PuppetWeb).browser.readCookie() const cookies = await (config.puppetInstance() as PuppetWeb).cookies()
log.silly('Contact', 'avatar() url: %s', avatarUrl) log.silly('Contact', 'avatar() url: %s', avatarUrl)
return UtilLib.urlStream(avatarUrl, cookies) return Misc.urlStream(avatarUrl, cookies)
} catch (err) { } catch (err) {
log.warn('Contact', 'avatar() exception: %s', err.stack) log.warn('Contact', 'avatar() exception: %s', err.stack)
Raven.captureException(err) Raven.captureException(err)
...@@ -582,11 +582,6 @@ export class Contact implements Sayable { ...@@ -582,11 +582,6 @@ export class Contact implements Sayable {
return this.ready() return this.ready()
} }
// public ready() {
// log.warn('Contact', 'ready() DEPRECATED. use load() instead.')
// return this.load()
// }
/** /**
* @private * @private
*/ */
...@@ -708,9 +703,9 @@ export class Contact implements Sayable { ...@@ -708,9 +703,9 @@ export class Contact implements Sayable {
public weixin(): string | null { public weixin(): string | null {
const wxId = this.obj && this.obj.weixin || null const wxId = this.obj && this.obj.weixin || null
if (!wxId) { if (!wxId) {
log.info('Contact', `weixin() is not able to always work, it's limited by Tencent API`) log.verbose('Contact', `weixin() is not able to always work, it's limited by Tencent API`)
log.info('Contact', 'weixin() If you want to track a contact between sessions, see FAQ at') log.verbose('Contact', 'weixin() If you want to track a contact between sessions, see FAQ at')
log.info('Contact', 'https://github.com/Chatie/wechaty/wiki/FAQ#1-how-to-get-the-permanent-id-for-a-contact') log.verbose('Contact', 'https://github.com/Chatie/wechaty/wiki/FAQ#1-how-to-get-the-permanent-id-for-a-contact')
} }
return wxId return wxId
} }
......
#!/usr/bin/env ts-node
/** /**
* Wechaty - https://github.com/chatie/wechaty * Wechaty - https://github.com/chatie/wechaty
* *
...@@ -16,22 +17,28 @@ ...@@ -16,22 +17,28 @@
* limitations under the License. * limitations under the License.
* *
*/ */
import { test } from 'ava' // tslint:disable:no-shadowed-variable
import * as test from 'blue-tape'
// import * as sinon from 'sinon'
// const sinonTest = require('sinon-test')(sinon)
import { import {
config, config,
log, log,
} from './config' } from './config'
import Message from './message' import Message from './message'
import Profile from './profile'
import PuppetWeb from './puppet-web/' import PuppetWeb from './puppet-web/'
const MOCK_USER_ID = 'TEST-USER-ID' const MOCK_USER_ID = 'TEST-USER-ID'
const puppet = new PuppetWeb() const puppet = new PuppetWeb({
profile: new Profile(),
})
puppet.userId = MOCK_USER_ID puppet.userId = MOCK_USER_ID
config.puppetInstance(puppet) config.puppetInstance(puppet)
test('constructor()', t => { test('constructor()', async t => {
/* tslint:disable:max-line-length */ /* tslint:disable:max-line-length */
const rawData = JSON.parse('{"MsgId":"179242112323992762","FromUserName":"@0bb3e4dd746fdbd4a80546aef66f4085","ToUserName":"@16d20edf23a3bf3bc71bb4140e91619f3ff33b4e33f7fcd25e65c1b02c7861ab","MsgType":1,"Content":"test123","Status":3,"ImgStatus":1,"CreateTime":1461652670,"VoiceLength":0,"PlayLength":0,"FileName":"","FileSize":"","MediaId":"","Url":"","AppMsgType":0,"StatusNotifyCode":0,"StatusNotifyUserName":"","RecommendInfo":{"UserName":"","NickName":"","QQNum":0,"Province":"","City":"","Content":"","Signature":"","Alias":"","Scene":0,"VerifyFlag":0,"AttrStatus":0,"Sex":0,"Ticket":"","OpCode":0},"ForwardFlag":0,"AppInfo":{"AppID":"","Type":0},"HasProductId":0,"Ticket":"","ImgHeight":0,"ImgWidth":0,"SubMsgType":0,"NewMsgId":179242112323992770,"MMPeerUserName":"@0bb3e4dd746fdbd4a80546aef66f4085","MMDigest":"test123","MMIsSend":false,"MMIsChatRoom":false,"MMUnread":true,"LocalID":"179242112323992762","ClientMsgId":"179242112323992762","MMActualContent":"test123","MMActualSender":"@0bb3e4dd746fdbd4a80546aef66f4085","MMDigestTime":"14:37","MMDisplayTime":1461652670,"MMTime":"14:37"}') const rawData = JSON.parse('{"MsgId":"179242112323992762","FromUserName":"@0bb3e4dd746fdbd4a80546aef66f4085","ToUserName":"@16d20edf23a3bf3bc71bb4140e91619f3ff33b4e33f7fcd25e65c1b02c7861ab","MsgType":1,"Content":"test123","Status":3,"ImgStatus":1,"CreateTime":1461652670,"VoiceLength":0,"PlayLength":0,"FileName":"","FileSize":"","MediaId":"","Url":"","AppMsgType":0,"StatusNotifyCode":0,"StatusNotifyUserName":"","RecommendInfo":{"UserName":"","NickName":"","QQNum":0,"Province":"","City":"","Content":"","Signature":"","Alias":"","Scene":0,"VerifyFlag":0,"AttrStatus":0,"Sex":0,"Ticket":"","OpCode":0},"ForwardFlag":0,"AppInfo":{"AppID":"","Type":0},"HasProductId":0,"Ticket":"","ImgHeight":0,"ImgWidth":0,"SubMsgType":0,"NewMsgId":179242112323992770,"MMPeerUserName":"@0bb3e4dd746fdbd4a80546aef66f4085","MMDigest":"test123","MMIsSend":false,"MMIsChatRoom":false,"MMUnread":true,"LocalID":"179242112323992762","ClientMsgId":"179242112323992762","MMActualContent":"test123","MMActualSender":"@0bb3e4dd746fdbd4a80546aef66f4085","MMDigestTime":"14:37","MMDisplayTime":1461652670,"MMTime":"14:37"}')
...@@ -50,7 +57,7 @@ test('constructor()', t => { ...@@ -50,7 +57,7 @@ test('constructor()', t => {
// Issue #445 // Issue #445
// XXX have to use test.serial() because mockGetContact can not be parallel // XXX have to use test.serial() because mockGetContact can not be parallel
test.serial('ready()', async t => { test('ready()', async t => {
// must different with other rawData, because Contact class with load() will cache the result. or use Contact.resetPool() // must different with other rawData, because Contact class with load() will cache the result. or use Contact.resetPool()
/* tslint:disable:max-line-length */ /* tslint:disable:max-line-length */
...@@ -119,7 +126,7 @@ test('find()', async t => { ...@@ -119,7 +126,7 @@ test('find()', async t => {
id: 'xxx', id: 'xxx',
}) })
t.truthy(m.id, 'Message.find') t.ok(m.id, 'Message.find')
}) })
test('findAll()', async t => { test('findAll()', async t => {
...@@ -130,8 +137,8 @@ test('findAll()', async t => { ...@@ -130,8 +137,8 @@ test('findAll()', async t => {
t.is(msgList.length, 2, 'Message.findAll with limit 2') t.is(msgList.length, 2, 'Message.findAll with limit 2')
}) })
test('self()', t => { test('self()', async t => {
config.puppetInstance() config.puppetInstance(puppet)
const m = new Message() const m = new Message()
m.from(MOCK_USER_ID) m.from(MOCK_USER_ID)
...@@ -142,9 +149,7 @@ test('self()', t => { ...@@ -142,9 +149,7 @@ test('self()', t => {
t.false(m.self(), 'should identify self message false when from a different fromId') t.false(m.self(), 'should identify self message false when from a different fromId')
}) })
// Issue #445 test('mentioned()', async t => {
// XXX have to use test.serial() because mockGetContact can not be parallel
test.serial('mentioned()', async t => {
/* tslint:disable:max-line-length */ /* tslint:disable:max-line-length */
const rawObj11 = JSON.parse(`{"MsgId":"6475340302153501409","FromUserName":"@@9cdc696e490bd76c57e7dd54792dc1408e27d65e312178b1943e88579b7939f4","ToUserName":"@cd7d467d7464e8ff6b0acd29364654f3666df5d04551f6082bfc875f90a6afd2","MsgType":1,"Content":"@4c32c97337cbb325442c304d6a44e374:<br/>@_@","Status":3,"ImgStatus":1,"CreateTime":1489823176,"VoiceLength":0,"PlayLength":0,"FileName":"","FileSize":"","MediaId":"","Url":"","AppMsgType":0,"StatusNotifyCode":0,"StatusNotifyUserName":"","RecommendInfo":{"UserName":"","NickName":"","QQNum":0,"Province":"","City":"","Content":"","Signature":"","Alias":"","Scene":0,"VerifyFlag":0,"AttrStatus":0,"Sex":0,"Ticket":"","OpCode":0},"ForwardFlag":0,"AppInfo":{"AppID":"","Type":0},"HasProductId":0,"Ticket":"","ImgHeight":0,"ImgWidth":0,"SubMsgType":0,"NewMsgId":6475340302153502000,"OriContent":"","MMPeerUserName":"@@9cdc696e490bd76c57e7dd54792dc1408e27d65e312178b1943e88579b7939f4","MMDigest":"22acb030-ff09-11e6-8a73-cff62d9268c5:@_@","MMIsSend":false,"MMIsChatRoom":true,"MMUnread":true,"LocalID":"6475340302153501409","ClientMsgId":"6475340302153501409","MMActualContent":"@_@","MMActualSender":"@4c32c97337cbb325442c304d6a44e374","MMDigestTime":"15:46","MMDisplayTime":1489823176,"MMTime":"15:46"}`) const rawObj11 = JSON.parse(`{"MsgId":"6475340302153501409","FromUserName":"@@9cdc696e490bd76c57e7dd54792dc1408e27d65e312178b1943e88579b7939f4","ToUserName":"@cd7d467d7464e8ff6b0acd29364654f3666df5d04551f6082bfc875f90a6afd2","MsgType":1,"Content":"@4c32c97337cbb325442c304d6a44e374:<br/>@_@","Status":3,"ImgStatus":1,"CreateTime":1489823176,"VoiceLength":0,"PlayLength":0,"FileName":"","FileSize":"","MediaId":"","Url":"","AppMsgType":0,"StatusNotifyCode":0,"StatusNotifyUserName":"","RecommendInfo":{"UserName":"","NickName":"","QQNum":0,"Province":"","City":"","Content":"","Signature":"","Alias":"","Scene":0,"VerifyFlag":0,"AttrStatus":0,"Sex":0,"Ticket":"","OpCode":0},"ForwardFlag":0,"AppInfo":{"AppID":"","Type":0},"HasProductId":0,"Ticket":"","ImgHeight":0,"ImgWidth":0,"SubMsgType":0,"NewMsgId":6475340302153502000,"OriContent":"","MMPeerUserName":"@@9cdc696e490bd76c57e7dd54792dc1408e27d65e312178b1943e88579b7939f4","MMDigest":"22acb030-ff09-11e6-8a73-cff62d9268c5:@_@","MMIsSend":false,"MMIsChatRoom":true,"MMUnread":true,"LocalID":"6475340302153501409","ClientMsgId":"6475340302153501409","MMActualContent":"@_@","MMActualSender":"@4c32c97337cbb325442c304d6a44e374","MMDigestTime":"15:46","MMDisplayTime":1489823176,"MMTime":"15:46"}`)
......
...@@ -25,242 +25,27 @@ import { ...@@ -25,242 +25,27 @@ import {
import { import {
config, config,
Raven, Raven,
RecommendInfo,
Sayable, Sayable,
log, log,
} from './config' } from './config'
import Contact from './contact' import Contact from './contact'
import Room from './room' import Room from './room'
import UtilLib from './util-lib' import Misc from './misc'
import PuppetWeb from './puppet-web/puppet-web' import PuppetWeb from './puppet-web/puppet-web'
import Bridge from './puppet-web/bridge' import Bridge from './puppet-web/bridge'
export interface MsgRawObj { import {
MsgId: string, AppMsgType,
MsgObj,
MMActualSender: string, // getUserContact(message.MMActualSender,message.MMPeerUserName).isContact() MsgRawObj,
MMPeerUserName: string, // message.MsgType == CONF.MSGTYPE_TEXT && message.MMPeerUserName == 'newsapp' MsgType,
ToUserName: string, } from './puppet-web/schema'
FromUserName: string,
MMActualContent: string, // Content has @id prefix added by wx
Content: string,
MMDigest: string,
MMDisplayTime: number, // Javascript timestamp of milliseconds
/**
* MsgType == MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_URL
* class="cover" mm-src="{{getMsgImg(message.MsgId,'slave')}}"
*/
Url: string,
MMAppMsgDesc: string, // class="desc" ng-bind="message.MMAppMsgDesc"
/**
* Attachment
*
* MsgType == MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_ATTACH
*/
FileName: string, // FileName: '钢甲互联项目BP1108.pdf',
FileSize: number, // FileSize: '2845701',
MediaId: string, // MediaId: '@crypt_b1a45e3f_c21dceb3ac01349...
MMFileExt: string, // doc, docx ... 'undefined'?
Signature: string, // checkUpload return the signature used to upload large files
MMAppMsgFileExt: string, // doc, docx ... 'undefined'?
MMAppMsgFileSize: string, // '2.7MB',
MMAppMsgDownloadUrl: string, // 'https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmedia?sender=@4f549c2dafd5ad731afa4d857bf03c10&mediaid=@crypt_b1a45e3f
// <a download ng-if="message.MMFileStatus == CONF.MM_SEND_FILE_STATUS_SUCCESS
// && (massage.MMStatus == CONF.MSG_SEND_STATUS_SUCC || massage.MMStatus === undefined)
// " href="{{message.MMAppMsgDownloadUrl}}">下载</a>
MMUploadProgress: number, // < 100
/**
* 模板消息
* MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_READER_TYPE
* item.url
* item.title
* item.pub_time
* item.cover
* item.digest
*/
MMCategory: any[], // item in message.MMCategory
/**
* Type
*
* MsgType == CONF.MSGTYPE_VOICE : ng-style="{'width':40 + 7*message.VoiceLength/1000}
*/
MsgType: number,
AppMsgType: AppMsgType, // message.MsgType == CONF.MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_URL
// message.MsgType == CONF.MSGTYPE_TEXT && message.SubMsgType != CONF.MSGTYPE_LOCATION
SubMsgType: MsgType, // "msgType":"{{message.MsgType}}","subType":{{message.SubMsgType||0}},"msgId":"{{message.MsgId}}"
/**
* Status-es
*/
Status: string,
MMStatus: number, // img ng-show="message.MMStatus == 1" class="ico_loading"
// ng-click="resendMsg(message)" ng-show="message.MMStatus == 5" title="重新发送"
MMFileStatus: number, // <p class="loading" ng-show="message.MMStatus == 1 || message.MMFileStatus == CONF.MM_SEND_FILE_STATUS_FAIL">
// CONF.MM_SEND_FILE_STATUS_QUEUED, MM_SEND_FILE_STATUS_SENDING
/**
* Location
*/
MMLocationUrl: string, // ng-if="message.MsgType == CONF.MSGTYPE_TEXT && message.SubMsgType == CONF.MSGTYPE_LOCATION"
// <a href="{{message.MMLocationUrl}}" target="_blank">
// 'http://apis.map.qq.com/uri/v1/geocoder?coord=40.075041,116.338994'
MMLocationDesc: string, // MMLocationDesc: '北京市昌平区回龙观龙腾苑(五区)内(龙腾街南)',
/**
* MsgType == CONF.MSGTYPE_EMOTICON
*
* getMsgImg(message.MsgId,'big',message)
*/
/**
* Image
*
* getMsgImg(message.MsgId,'slave')
*/
MMImgStyle: string, // ng-style="message.MMImgStyle"
MMPreviewSrc: string, // message.MMPreviewSrc || message.MMThumbSrc || getMsgImg(message.MsgId,'slave')
MMThumbSrc: string,
/**
* Friend Request & ShareCard ?
*
* MsgType == CONF.MSGTYPE_SHARECARD" ng-click="showProfile($event,message.RecommendInfo.UserName)
* MsgType == CONF.MSGTYPE_VERIFYMSG
*/
RecommendInfo?: RecommendInfo,
/**
* Transpond Message
*/
MsgIdBeforeTranspond?: string, // oldMsg.MsgIdBeforeTranspond || oldMsg.MsgId,
isTranspond?: boolean,
MMSourceMsgId?: string,
MMSendContent?: string,
MMIsChatRoom?: boolean,
}
export interface MsgObj {
id: string,
type: MsgType,
from: string,
to?: string, // if to is not set, then room must be set
room?: string,
content: string,
status: string,
digest: string,
date: string,
url?: string, // for MessageMedia class
}
// export type MessageTypeName = 'TEXT' | 'IMAGE' | 'VOICE' | 'VERIFYMSG' | 'POSSIBLEFRIEND_MSG'
// | 'SHARECARD' | 'VIDEO' | 'EMOTICON' | 'LOCATION' | 'APP' | 'VOIPMSG' | 'STATUSNOTIFY'
// | 'VOIPNOTIFY' | 'VOIPINVITE' | 'MICROVIDEO' | 'SYSNOTICE' | 'SYS' | 'RECALLED'
// export type MessageTypeValue = 1 | 3 | 34 | 37 | 40 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 62 | 9999 | 10000 | 10002
export interface MsgTypeMap {
[index: string]: string|number,
// MessageTypeName: MessageTypeValue
// , MessageTypeValue: MessageTypeName
}
/**
*
* Enum for AppMsgType values.
*
* @enum {number}
* @property {number} TEXT - AppMsgType.TEXT (1) for TEXT
* @property {number} IMG - AppMsgType.IMG (2) for IMG
* @property {number} AUDIO - AppMsgType.AUDIO (3) for AUDIO
* @property {number} VIDEO - AppMsgType.VIDEO (4) for VIDEO
* @property {number} URL - AppMsgType.URL (5) for URL
* @property {number} ATTACH - AppMsgType.ATTACH (6) for ATTACH
* @property {number} OPEN - AppMsgType.OPEN (7) for OPEN
* @property {number} EMOJI - AppMsgType.EMOJI (8) for EMOJI
* @property {number} VOICE_REMIND - AppMsgType.VOICE_REMIND (9) for VOICE_REMIND
* @property {number} SCAN_GOOD - AppMsgType.SCAN_GOOD (10) for SCAN_GOOD
* @property {number} GOOD - AppMsgType.GOOD (13) for GOOD
* @property {number} EMOTION - AppMsgType.EMOTION (15) for EMOTION
* @property {number} CARD_TICKET - AppMsgType.CARD_TICKET (16) for CARD_TICKET
* @property {number} REALTIME_SHARE_LOCATION - AppMsgType.REALTIME_SHARE_LOCATION (17) for REALTIME_SHARE_LOCATION
* @property {number} TRANSFERS - AppMsgType.TRANSFERS (2e3) for TRANSFERS
* @property {number} RED_ENVELOPES - AppMsgType.RED_ENVELOPES (2001) for RED_ENVELOPES
* @property {number} READER_TYPE - AppMsgType.READER_TYPE (100001) for READER_TYPE
*/
export enum AppMsgType {
TEXT = 1,
IMG = 2,
AUDIO = 3,
VIDEO = 4,
URL = 5,
ATTACH = 6,
OPEN = 7,
EMOJI = 8,
VOICE_REMIND = 9,
SCAN_GOOD = 10,
GOOD = 13,
EMOTION = 15,
CARD_TICKET = 16,
REALTIME_SHARE_LOCATION = 17,
TRANSFERS = 2e3,
RED_ENVELOPES = 2001,
READER_TYPE = 100001,
}
/** export type TypeName = 'attachment'
* | 'audio'
* Enum for MsgType values. | 'image'
* @enum {number} | 'video'
* @property {number} TEXT - MsgType.TEXT (1) for TEXT
* @property {number} IMAGE - MsgType.IMAGE (3) for IMAGE
* @property {number} VOICE - MsgType.VOICE (34) for VOICE
* @property {number} VERIFYMSG - MsgType.VERIFYMSG (37) for VERIFYMSG
* @property {number} POSSIBLEFRIEND_MSG - MsgType.POSSIBLEFRIEND_MSG (40) for POSSIBLEFRIEND_MSG
* @property {number} SHARECARD - MsgType.SHARECARD (42) for SHARECARD
* @property {number} VIDEO - MsgType.VIDEO (43) for VIDEO
* @property {number} EMOTICON - MsgType.EMOTICON (47) for EMOTICON
* @property {number} LOCATION - MsgType.LOCATION (48) for LOCATION
* @property {number} APP - MsgType.APP (49) for APP
* @property {number} VOIPMSG - MsgType.VOIPMSG (50) for VOIPMSG
* @property {number} STATUSNOTIFY - MsgType.STATUSNOTIFY (51) for STATUSNOTIFY
* @property {number} VOIPNOTIFY - MsgType.VOIPNOTIFY (52) for VOIPNOTIFY
* @property {number} VOIPINVITE - MsgType.VOIPINVITE (53) for VOIPINVITE
* @property {number} MICROVIDEO - MsgType.MICROVIDEO (62) for MICROVIDEO
* @property {number} SYSNOTICE - MsgType.SYSNOTICE (9999) for SYSNOTICE
* @property {number} SYS - MsgType.SYS (10000) for SYS
* @property {number} RECALLED - MsgType.RECALLED (10002) for RECALLED
*/
export enum MsgType {
TEXT = 1,
IMAGE = 3,
VOICE = 34,
VERIFYMSG = 37,
POSSIBLEFRIEND_MSG = 40,
SHARECARD = 42,
VIDEO = 43,
EMOTICON = 47,
LOCATION = 48,
APP = 49,
VOIPMSG = 50,
STATUSNOTIFY = 51,
VOIPNOTIFY = 52,
VOIPINVITE = 53,
MICROVIDEO = 62,
SYSNOTICE = 9999,
SYS = 10000,
RECALLED = 10002,
}
/** /**
* All wechat messages will be encapsulated as a Message. * All wechat messages will be encapsulated as a Message.
...@@ -378,14 +163,14 @@ export class Message implements Sayable { ...@@ -378,14 +163,14 @@ export class Message implements Sayable {
* @private * @private
*/ */
public toString() { public toString() {
return UtilLib.plainText(this.obj.content) return Misc.plainText(this.obj.content)
} }
/** /**
* @private * @private
*/ */
public toStringDigest() { public toStringDigest() {
const text = UtilLib.digestEmoji(this.obj.digest) const text = Misc.digestEmoji(this.obj.digest)
return '{' + this.typeEx() + '}' + text return '{' + this.typeEx() + '}' + text
} }
...@@ -414,7 +199,7 @@ export class Message implements Sayable { ...@@ -414,7 +199,7 @@ export class Message implements Sayable {
* @private * @private
*/ */
public getContentString() { public getContentString() {
let content = UtilLib.plainText(this.obj.content) let content = Misc.plainText(this.obj.content)
if (content.length > 20) { content = content.substring(0, 17) + '...' } if (content.length > 20) { content = content.substring(0, 17) + '...' }
return '{' + this.type() + '}' + content return '{' + this.type() + '}' + content
} }
...@@ -1108,12 +893,12 @@ export class MediaMessage extends Message { ...@@ -1108,12 +893,12 @@ export class MediaMessage extends Message {
try { try {
await this.ready() await this.ready()
// FIXME: decoupling needed // FIXME: decoupling needed
const cookies = await (config.puppetInstance() as PuppetWeb).browser.readCookie() const cookies = await (config.puppetInstance() as PuppetWeb).cookies()
if (!this.obj.url) { if (!this.obj.url) {
throw new Error('no url') throw new Error('no url')
} }
log.verbose('MediaMessage', 'readyStream() url: %s', this.obj.url) log.verbose('MediaMessage', 'readyStream() url: %s', this.obj.url)
return UtilLib.urlStream(this.obj.url, cookies) return Misc.urlStream(this.obj.url, cookies)
} catch (e) { } catch (e) {
log.warn('MediaMessage', 'readyStream() exception: %s', e.stack) log.warn('MediaMessage', 'readyStream() exception: %s', e.stack)
Raven.captureException(e) Raven.captureException(e)
...@@ -1154,8 +939,6 @@ export class MediaMessage extends Message { ...@@ -1154,8 +939,6 @@ export class MediaMessage extends Message {
}) })
} }
public forward(room: Room|Room[]): Promise<boolean>
public forward(contact: Contact|Contact[]): Promise<boolean>
/** /**
* Forward the received message. * Forward the received message.
* *
...@@ -1200,53 +983,19 @@ export class MediaMessage extends Message { ...@@ -1200,53 +983,19 @@ export class MediaMessage extends Message {
* }) * })
* ``` * ```
* *
* @param {(Sayable | Sayable[])} sendTo Room or Contact, or array * @param {(Sayable | Sayable[])} to Room or Contact
* The recipient of the message, the room, or the contact * The recipient of the message, the room, or the contact
* @returns {Promise<boolean>} * @returns {Promise<boolean>}
* @memberof MediaMessage * @memberof MediaMessage
*/ */
public async forward(sendTo: Room|Room[]|Contact|Contact[]): Promise<boolean> { public async forward(to: Room|Contact): Promise<boolean> {
if (!this.rawObj) { try {
throw new Error('no rawObj!') const ret = await config.puppetInstance().forward(this, to)
} return ret
let m = Object.assign({}, this.rawObj) } catch (e) {
const newMsg = <MsgRawObj>{} log.error('Message', 'forward(%s) exception: %s', to, e)
const largeFileSize = 25 * 1024 * 1024 throw e
let ret = false
// if you know roomId or userId, you can use `Room.load(roomId)` or `Contact.load(userId)`
let sendToList: Contact[] = [].concat(sendTo as any || [])
sendToList = sendToList.filter(s => {
if ((s instanceof Room || s instanceof Contact) && s.id) {
return true
}
return false
}) as Contact[]
if (sendToList.length < 1) {
throw new Error('param must be Room or Contact and array')
}
if (m.FileSize >= largeFileSize && !m.Signature) {
// if has RawObj.Signature, can forward the 25Mb+ file
log.warn('MediaMessage', 'forward() Due to webWx restrictions, more than 25MB of files can not be downloaded and can not be forwarded.')
return false
}
newMsg.FromUserName = config.puppetInstance().userId || ''
newMsg.isTranspond = true
newMsg.MsgIdBeforeTranspond = m.MsgIdBeforeTranspond || m.MsgId
newMsg.MMSourceMsgId = m.MsgId
// In room msg, the content prefix sender:, need to be removed, otherwise the forwarded sender will display the source message sender, causing self () to determine the error
newMsg.Content = UtilLib.unescapeHtml(m.Content.replace(/^@\w+:<br\/>/, '')).replace(/^[\w\-]+:<br\/>/, '')
newMsg.MMIsChatRoom = sendTo instanceof Room ? true : false
// The following parameters need to be overridden after calling createMessage()
m = Object.assign(m, newMsg)
for (let i = 0; i < sendToList.length; i++) {
newMsg.ToUserName = sendToList[i].id
// all call success return true
ret = (i === 0 ? true : ret) && await config.puppetInstance().forward(m, newMsg)
} }
return ret
} }
} }
......
#!/usr/bin/env ts-node
/** /**
* Wechaty - https://github.com/chatie/wechaty * Wechaty - https://github.com/chatie/wechaty
* *
...@@ -16,39 +17,41 @@ ...@@ -16,39 +17,41 @@
* limitations under the License. * limitations under the License.
* *
*/ */
import { test } from 'ava' // tslint:disable:no-shadowed-variable
import * as express from 'express' import * as test from 'blue-tape'
// import * as sinon from 'sinon'
// const sinonTest = require('sinon-test')(sinon)
import UtilLib from './util-lib' import * as express from 'express'
// import * as http from 'http' import Misc from './misc'
test('stripHtml()', t => { test('stripHtml()', async t => {
const HTML_BEFORE_STRIP = 'Outer<html>Inner</html>' const HTML_BEFORE_STRIP = 'Outer<html>Inner</html>'
const HTML_AFTER_STRIP = 'OuterInner' const HTML_AFTER_STRIP = 'OuterInner'
const strippedHtml = UtilLib.stripHtml(HTML_BEFORE_STRIP) const strippedHtml = Misc.stripHtml(HTML_BEFORE_STRIP)
t.is(strippedHtml, HTML_AFTER_STRIP, 'should strip html as expected') t.is(strippedHtml, HTML_AFTER_STRIP, 'should strip html as expected')
}) })
test('unescapeHtml()', t => { test('unescapeHtml()', async t => {
const HTML_BEFORE_UNESCAPE = '&apos;|&quot;|&gt;|&lt;|&amp;' const HTML_BEFORE_UNESCAPE = '&apos;|&quot;|&gt;|&lt;|&amp;'
const HTML_AFTER_UNESCAPE = `'|"|>|<|&` const HTML_AFTER_UNESCAPE = `'|"|>|<|&`
const unescapedHtml = UtilLib.unescapeHtml(HTML_BEFORE_UNESCAPE) const unescapedHtml = Misc.unescapeHtml(HTML_BEFORE_UNESCAPE)
t.is(unescapedHtml, HTML_AFTER_UNESCAPE, 'should unescape html as expected') t.is(unescapedHtml, HTML_AFTER_UNESCAPE, 'should unescape html as expected')
}) })
test('plainText()', t => { test('plainText()', async t => {
const PLAIN_BEFORE = '&amp;<html>&amp;</html>&amp;<img class="emoji emoji1f4a4" text="[流汗]_web" src="/zh_CN/htmledition/v2/images/spacer.gif" />' const PLAIN_BEFORE = '&amp;<html>&amp;</html>&amp;<img class="emoji emoji1f4a4" text="[流汗]_web" src="/zh_CN/htmledition/v2/images/spacer.gif" />'
const PLAIN_AFTER = '&&&[流汗]' const PLAIN_AFTER = '&&&[流汗]'
const plainText = UtilLib.plainText(PLAIN_BEFORE) const plainText = Misc.plainText(PLAIN_BEFORE)
t.is(plainText, PLAIN_AFTER, 'should convert plain text as expected') t.is(plainText, PLAIN_AFTER, 'should convert plain text as expected')
}) })
test('digestEmoji()', t => { test('digestEmoji()', async t => {
const EMOJI_XML = [ const EMOJI_XML = [
'<img class="emoji emoji1f4a4" text="[流汗]_web" src="/zh_CN/htmledition/v2/images/spacer.gif" />', '<img class="emoji emoji1f4a4" text="[流汗]_web" src="/zh_CN/htmledition/v2/images/spacer.gif" />',
'<span class="emoji emoji1f334"></span>', '<span class="emoji emoji1f334"></span>',
...@@ -59,12 +62,12 @@ test('digestEmoji()', t => { ...@@ -59,12 +62,12 @@ test('digestEmoji()', t => {
] ]
for (let i = 0; i < EMOJI_XML.length; i++) { for (let i = 0; i < EMOJI_XML.length; i++) {
const emojiDigest = UtilLib.digestEmoji(EMOJI_XML[i]) const emojiDigest = Misc.digestEmoji(EMOJI_XML[i])
t.is(emojiDigest, EMOJI_AFTER_DIGEST[i], 'should digest emoji string ' + i + ' as expected') t.is(emojiDigest, EMOJI_AFTER_DIGEST[i], 'should digest emoji string ' + i + ' as expected')
} }
}) })
test('unifyEmoji()', t => { test('unifyEmoji()', async t => {
const ORIGNAL_XML_LIST: [string[], string][] = [ const ORIGNAL_XML_LIST: [string[], string][] = [
[ [
[ [
...@@ -77,13 +80,13 @@ test('unifyEmoji()', t => { ...@@ -77,13 +80,13 @@ test('unifyEmoji()', t => {
ORIGNAL_XML_LIST.forEach(([xmlList, expectedEmojiXml]) => { ORIGNAL_XML_LIST.forEach(([xmlList, expectedEmojiXml]) => {
xmlList.forEach(xml => { xmlList.forEach(xml => {
const unifiedXml = UtilLib.unifyEmoji(xml) const unifiedXml = Misc.unifyEmoji(xml)
t.is(unifiedXml, expectedEmojiXml, 'should convert the emoji xml to the expected unified xml') t.is(unifiedXml, expectedEmojiXml, 'should convert the emoji xml to the expected unified xml')
}) })
}) })
}) })
test('stripEmoji()', t => { test('stripEmoji()', async t => {
const EMOJI_STR = [ const EMOJI_STR = [
[ [
'ABC<img class="emoji emoji1f4a4" text="[流汗]_web" src="/zh_CN/htmledition/v2/images/spacer.gif" />DEF', 'ABC<img class="emoji emoji1f4a4" text="[流汗]_web" src="/zh_CN/htmledition/v2/images/spacer.gif" />DEF',
...@@ -96,11 +99,11 @@ test('stripEmoji()', t => { ...@@ -96,11 +99,11 @@ test('stripEmoji()', t => {
] ]
EMOJI_STR.forEach(([emojiStr, expectResult]) => { EMOJI_STR.forEach(([emojiStr, expectResult]) => {
const result = UtilLib.stripEmoji(emojiStr) const result = Misc.stripEmoji(emojiStr)
t.is(result, expectResult, 'should strip to the expected str') t.is(result, expectResult, 'should strip to the expected str')
}) })
const empty = UtilLib.stripEmoji(undefined) const empty = Misc.stripEmoji(undefined)
t.is(empty, '', 'should return empty string for `undefined`') t.is(empty, '', 'should return empty string for `undefined`')
}) })
...@@ -109,7 +112,7 @@ test('downloadStream() for media', async t => { ...@@ -109,7 +112,7 @@ test('downloadStream() for media', async t => {
app.use(require('cookie-parser')()) app.use(require('cookie-parser')())
app.get('/ding', function(req, res) { app.get('/ding', function(req, res) {
// console.log(req.cookies) // console.log(req.cookies)
t.truthy(req.cookies, 'should has cookies in req') t.ok(req.cookies, 'should has cookies in req')
t.is(req.cookies.life, '42', 'should has a cookie named life value 42') t.is(req.cookies.life, '42', 'should has a cookie named life value 42')
res.end('dong') res.end('dong')
}) })
...@@ -122,7 +125,7 @@ test('downloadStream() for media', async t => { ...@@ -122,7 +125,7 @@ test('downloadStream() for media', async t => {
server.listen(65534) server.listen(65534)
try { try {
const s = await UtilLib.urlStream('http://127.0.0.1:65534/ding', [{name: 'life', value: 42}]) const s = await Misc.urlStream('http://127.0.0.1:65534/ding', [{name: 'life', value: 42}])
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
s.on('data', (chunk) => { s.on('data', (chunk) => {
// console.log(`BODY: ${chunk}`) // console.log(`BODY: ${chunk}`)
...@@ -140,7 +143,7 @@ test('downloadStream() for media', async t => { ...@@ -140,7 +143,7 @@ test('downloadStream() for media', async t => {
test('getPort() for an available socket port', async t => { test('getPort() for an available socket port', async t => {
const PORT = 8788 const PORT = 8788
let port = await UtilLib.getPort(PORT) let port = await Misc.getPort(PORT)
t.not(port, PORT, 'should not be same port even it is available(to provent conflict between concurrency tests in AVA)') t.not(port, PORT, 'should not be same port even it is available(to provent conflict between concurrency tests in AVA)')
let ttl = 17 let ttl = 17
...@@ -148,7 +151,7 @@ test('getPort() for an available socket port', async t => { ...@@ -148,7 +151,7 @@ test('getPort() for an available socket port', async t => {
try { try {
const app = express() const app = express()
const server = app.listen(PORT) const server = app.listen(PORT)
port = await UtilLib.getPort(PORT) port = await Misc.getPort(PORT)
server.close() server.close()
} catch (e) { } catch (e) {
t.fail('should not exception: ' + e.message + ', ' + e.stack) t.fail('should not exception: ' + e.message + ', ' + e.stack)
......
...@@ -24,10 +24,9 @@ import { ...@@ -24,10 +24,9 @@ import {
} from 'stream' } from 'stream'
import * as url from 'url' import * as url from 'url'
import { MsgType } from './message'
import { log } from './config' import { log } from './config'
export class UtilLib { export class Misc {
public static stripHtml(html?: string): string { public static stripHtml(html?: string): string {
if (!html) { if (!html) {
return '' return ''
...@@ -97,10 +96,10 @@ export class UtilLib { ...@@ -97,10 +96,10 @@ export class UtilLib {
if (!html) { if (!html) {
return '' return ''
} }
return UtilLib.stripHtml( return Misc.stripHtml(
UtilLib.unescapeHtml( Misc.unescapeHtml(
UtilLib.stripHtml( Misc.stripHtml(
UtilLib.digestEmoji( Misc.digestEmoji(
html, html,
), ),
), ),
...@@ -258,21 +257,21 @@ export class UtilLib { ...@@ -258,21 +257,21 @@ export class UtilLib {
return md5sum.digest('hex') return md5sum.digest('hex')
} }
public static msgType(ext): MsgType { // public static msgType(ext): MsgType {
switch (ext) { // switch (ext) {
case 'bmp': // case 'bmp':
case 'jpeg': // case 'jpeg':
case 'jpg': // case 'jpg':
case 'png': // case 'png':
return MsgType.IMAGE // return MsgType.IMAGE
case 'gif': // case 'gif':
return MsgType.EMOTICON // return MsgType.EMOTICON
case 'mp4': // case 'mp4':
return MsgType.VIDEO // return MsgType.VIDEO
default: // default:
return MsgType.APP // return MsgType.APP
} // }
} // }
public static mime(ext): string { public static mime(ext): string {
switch (ext) { switch (ext) {
...@@ -296,4 +295,4 @@ export class UtilLib { ...@@ -296,4 +295,4 @@ export class UtilLib {
} }
} }
export default UtilLib export default Misc
import * as fs from 'fs'
import * as path from 'path'
import {
config,
log,
} from './config'
export type ProfileSection = 'cookies'
export interface ProfileSchema {
cookies?: any[]
}
export class Profile {
private obj : ProfileSchema
private file : string | null
constructor(
public name: string = config.profile,
) {
log.verbose('Profile', 'constructor(%s)', name)
if (!name) {
this.file = null
} else {
this.file = path.isAbsolute(name)
? name
: path.join(
process.cwd(),
name + '.wechaty.json',
)
}
}
public load(): void {
log.verbose('Profile', 'load() file: %s', this.file)
if (!this.file) {
return
}
if (!fs.existsSync(this.file)) {
return
}
const text = fs.readFileSync(this.file).toString()
try {
this.obj = JSON.parse(text)
} catch (e) {
log.error('Profile', 'load() exception: %s', e)
this.obj = {}
}
}
public save(): void {
log.verbose('Profile', 'save() file: %s', this.file)
if (!this.file || !this.obj) {
return
}
try {
const text = JSON.stringify(this.obj)
fs.writeFileSync(this.file, text)
} catch (e) {
log.error('Profile', 'save() exception: %s', e)
throw e
}
}
public get(section: ProfileSection): null | any {
log.verbose('Profile', 'get(%s)', section)
if (!this.obj) {
return null
}
return this.obj[section]
}
public set(section: ProfileSection, data: any): void {
log.verbose('Profile', 'set(%s, %s)', section, data)
if (!this.obj) {
return
}
this.obj[section] = data
}
public destroy(): void {
log.verbose('Profile', 'destroy() file: %s', this.file)
if (this.file && fs.existsSync(this.file)) {
fs.unlinkSync(this.file)
}
}
}
export default Profile
#!/usr/bin/env ts-node
/** /**
* Wechaty - https://github.com/chatie/wechaty * Wechaty - https://github.com/chatie/wechaty
* *
...@@ -16,17 +17,19 @@ ...@@ -16,17 +17,19 @@
* limitations under the License. * limitations under the License.
* *
*/ */
import { test } from 'ava' // tslint:disable:no-shadowed-variable
import * as sinon from 'sinon' import * as test from 'blue-tape'
// import * as sinon from 'sinon'
// const sinonTest = require('sinon-test')(sinon) // const sinonTest = require('sinon-test')(sinon)
import PuppetWeb from './puppet-web' import Profile from '../profile'
import Bridge from './bridge'
test('PuppetWebBridge smoke testing', async t => { import Bridge from './bridge'
const browser = sinon.spy()
const mockPuppet = {browser} as any as PuppetWeb test('PuppetWebBridge', async t => {
const bridge = new Bridge(mockPuppet, 8788) const profile = new Profile()
t.truthy(bridge, 'Bridge instnace') const bridge = new Bridge({ profile })
await bridge.init()
t.ok(bridge, 'Bridge instnace')
await bridge.quit()
}) })
此差异已折叠。
/**
* Wechaty - https://github.com/chatie/wechaty
*
* @copyright 2016-2017 Huan LI <zixia@zixia.net>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { test } from 'ava'
import {
BrowserCookie,
} from './browser-cookie'
import { BrowserDriver } from './browser-driver'
test('hostname() for wx.qq.com', async t => {
const driver = <BrowserDriver>{}
const browserCookie = new BrowserCookie(driver, 'test/fixture/profile/qq.wechaty.json')
const hostname = await browserCookie.hostname()
t.is(hostname, 'wx.qq.com', 'should get wx.qq.com')
})
test('hostname() for wechat.com', async t => {
const driver = <BrowserDriver>{}
const browserCookie = new BrowserCookie(driver, 'test/fixture/profile/wechat.wechaty.json')
const hostname = await browserCookie.hostname()
t.is(hostname, 'web.wechat.com', 'should get web.wechat.com')
})
test('hostname() for default', async t => {
const driver = <BrowserDriver>{}
const browserCookie = new BrowserCookie(driver)
const hostname = await browserCookie.hostname()
t.is(hostname, 'wx.qq.com', 'should get wx.qq.com')
})
test('hostname() for file not exist', async t => {
const driver = <BrowserDriver>{}
const browserCookie = new BrowserCookie(driver, 'file-not-exist.wechaty.json')
const hostname = await browserCookie.hostname()
t.is(hostname, 'wx.qq.com', 'should get wx.qq.com for non exist file')
})
/**
* Wechaty - https://github.com/chatie/wechaty
*
* @copyright 2016-2017 Huan LI <zixia@zixia.net>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import * as fs from 'fs'
import { log } from '../config'
import {
BrowserDriver,
IWebDriverOptionsCookie,
} from './browser-driver'
/**
* The reason that driverCookie type defined here
* is because @types/selenium is not updated
* with the latest 3.0 version of selenium.
* 201610 zixia
*/
/**
* Updated 201708 zixia Use IWebDriverOptionsCookie from selenium instead
*/
// export interface CookieType {
// [index: string]: string | number | boolean,
// name: string,
// value: string,
// path: string,
// domain: string,
// secure: boolean,
// expiry: number,
// }
export class BrowserCookie {
constructor(
private driver: BrowserDriver,
private storeFile?: string,
) {
log.verbose('PuppetWebBrowserCookie', 'constructor(%s, %s)',
driver.constructor.name,
storeFile ? storeFile : '',
)
}
public async read(): Promise<IWebDriverOptionsCookie[]> {
// just check cookies, no file operation
log.verbose('PuppetWebBrowserCookie', 'read()')
// if (this.browser.dead()) {
// throw new Error('checkSession() - browser dead')
// }
try {
const cookies = await this.driver.manage().getCookies()
log.silly('PuppetWebBrowserCookie', 'read() %s', cookies.map(c => c.name).join(','))
return cookies
} catch (e) {
log.error('PuppetWebBrowserCookie', 'read() getCookies() exception: %s', e && e.message || e)
throw e
}
}
public async clean(): Promise<void> {
log.verbose('PuppetWebBrowserCookie', 'clean() file %s', this.storeFile)
if (!this.storeFile) {
return
}
// if (this.browser.dead()) { return Promise.reject(new Error('cleanSession() - browser dead'))}
const storeFile = this.storeFile
await new Promise((resolve, reject) => {
fs.unlink(storeFile, err => {
if (err && err.code !== 'ENOENT') {
log.silly('PuppetWebBrowserCookie', 'clean() unlink store file %s fail: %s', storeFile, err.message)
}
resolve()
})
})
return
}
public async save(): Promise<void> {
if (!this.storeFile) {
log.verbose('PuppetWebBrowserCookie', 'save() no store file')
return
}
log.silly('PuppetWebBrowserCookie', 'save() to file %s', this.storeFile)
const storeFile = this.storeFile
// if (this.browser.dead()) {
// throw new Error('saveSession() - browser dead')
// }
function cookieFilter(cookies: IWebDriverOptionsCookie[]) {
const skipNames = [
'ChromeDriver',
'MM_WX_SOUND_STATE',
'MM_WX_NOTIFY_STATE',
]
const skipNamesRegex = new RegExp(skipNames.join('|'), 'i')
return cookies.filter(c => {
if (skipNamesRegex.test(c.name)) { return false }
// else if (!/wx\.qq\.com/i.test(c.domain)) { return false }
else { return true }
})
}
try {
let cookies: IWebDriverOptionsCookie[] = await this.driver.manage().getCookies()
cookies = cookieFilter(cookies)
// log.silly('PuppetWebBrowserCookie', 'save() saving %d cookies for session: %s', cookies.length
// , require('util').inspect(cookies.map(c => { return {name: c.name, value: c.value, expiresType: typeof c.expiry, expiry: c.expiry} })))
log.silly('PuppetWebBrowserCookie', 'save() saving %d cookies: %s', cookies.length, cookies.map(c => c.name).join(','))
const jsonStr = JSON.stringify(cookies)
await new Promise((resolve, reject) => {
fs.writeFile(storeFile, jsonStr, err => {
if (err) {
log.error('PuppetWebBrowserCookie', 'save() fail to write file %s: %s', storeFile, err.errno)
reject(err)
}
log.silly('PuppetWebBrowserCookie', 'save() %d cookies to %s', cookies.length, storeFile)
resolve(cookies)
})
})
} catch (e) {
log.error('PuppetWebBrowserCookie', 'save() getCookies() exception: %s', e.message)
throw e
}
}
public async load(): Promise<void> {
log.verbose('PuppetWebBrowserCookie', 'load() from %s', this.storeFile || '"undefined"')
const cookies = this.getCookiesFromFile()
if (!cookies) {
log.silly('PuppetWebBrowserCookie', 'load() no cookies')
return
}
try {
await this.add(cookies)
log.verbose('PuppetWebBrowserCookie', 'loaded session(%d cookies) from %s', cookies.length, this.storeFile)
} catch (e) {
log.error('PuppetWebBrowserCookie', 'load() add() exception: %s', e.message)
throw e
}
return
}
public getCookiesFromFile(): IWebDriverOptionsCookie[] | null {
log.verbose('PuppetWebBrowserCookie', 'getCookiesFromFile() from %s', this.storeFile || '"undefined"')
try {
if (!this.storeFile) {
throw new Error('no store file')
}
fs.statSync(this.storeFile).isFile()
} catch (err) {
log.silly('PuppetWebBrowserCookie', 'getCookiesFromFile() no cookies: %s', err.message)
return null
}
const jsonStr = fs.readFileSync(this.storeFile)
const cookies = JSON.parse(jsonStr.toString())
return cookies
}
public hostname(): string {
log.verbose('PuppetWebBrowserCookie', 'hostname()')
const DEFAULT_HOSTNAME = 'wx.qq.com'
const cookieList = this.getCookiesFromFile()
if (!cookieList || cookieList.length === 0) {
log.silly('PuppetWebBrowserCookie', 'hostname() no cookie, return default hostname')
return DEFAULT_HOSTNAME
}
const wxCookieList = cookieList.filter(c => /^webwx_auth_ticket|webwxuvid$/.test(c.name))
if (!wxCookieList.length) {
log.silly('PuppetWebBrowserCookie', 'hostname() no valid cookie in files, return default hostname')
return DEFAULT_HOSTNAME
}
let domain = wxCookieList[0].domain
if (!domain) {
log.silly('PuppetWebBrowserCookie', 'hostname() no valid domain in cookies, return default hostname')
return DEFAULT_HOSTNAME
}
domain = domain.slice(1)
if (domain === 'wechat.com') {
domain = 'web.wechat.com'
}
log.silly('PuppetWebBrowserCookie', 'hostname() got %s', domain)
return domain
}
/**
* only wrap addCookies for convinience
*
* use this.driver().manage() to call other functions like:
* deleteCookie / getCookie / getCookies
*/
// TypeScript Overloading: http://stackoverflow.com/a/21385587/1123955
public async add(cookie: IWebDriverOptionsCookie | IWebDriverOptionsCookie[]): Promise<void> {
if (Array.isArray(cookie)) {
const cookieList = cookie
log.verbose('PuppetWebBrowserCookie', 'add(Array.length = %d)', cookieList.length)
for (const c of cookieList) {
await this.add(c)
}
return
}
/**
* convert expiry from seconds to milliseconds. https://github.com/SeleniumHQ/selenium/issues/2245
* with selenium-webdriver v2.53.2
* NOTICE: the lastest branch of selenium-webdriver for js has changed the interface of addCookie:
* https://github.com/SeleniumHQ/selenium/commit/02f407976ca1d516826990f11aca7de3c16ba576
*/
// if (cookie.expiry) { cookie.expiry = cookie.expiry * 1000 /* XXX: be aware of new version of webdriver */}
log.silly('PuppetWebBrowserCookie', 'add(%s)', JSON.stringify(cookie))
try {
await this.driver
.manage()
.addCookie(cookie)
} catch (e) {
log.warn('PuppetWebBrowserCookie', 'add() exception: %s', e.message)
throw e
}
}
}
export default BrowserCookie
/**
* Wechaty - https://github.com/chatie/wechaty
*
* @copyright 2016-2017 Huan LI <zixia@zixia.net>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { test } from 'ava'
import {
config,
log,
} from '../config'
import BrowserDriver from './browser-driver'
test('BrowserDriver smoke testing', async t => {
let err: Error | null = new Error('not run')
let ttl = 3
while (err && ttl--) {
try {
const browserDriver = new BrowserDriver(config.head)
t.truthy(browserDriver, 'BrowserDriver instnace')
await browserDriver.init()
const driver = browserDriver.getWebDriver()
t.truthy(driver, 'should get webdriver instance')
await driver.get('https://mp.weixin.qq.com/')
t.pass('should open mp.weixin.qq.com')
const retAdd = await driver.executeScript<number>('return 1 + 1')
t.is(retAdd, 2, 'should return 2 for execute 1+1 in browser')
await browserDriver.close().catch(() => { /* fail safe */ })
await browserDriver.quit().catch(() => { /* fail safe */ })
err = null
} catch (e) {
err = e
log.error('TestPuppetWebBrowserDriver', 'ttl %d, exception: %s',
ttl,
e && e.message || e,
)
}
}
if (err && ttl <= 0) {
t.fail('ttl timeout: ' + (err && err.message || err))
}
})
/**
* Wechaty - https://github.com/chatie/wechaty
*
* @copyright 2016-2017 Huan LI <zixia@zixia.net>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import {
Builder,
By,
Capabilities,
IWebDriverOptionsCookie,
logging,
Navigation,
Options,
promise as promiseManager,
Session,
TargetLocator,
WebDriver,
} from 'selenium-webdriver'
import {
config,
HeadName,
log,
} from '../config'
/**
* ISSUE #72
* Introduce the SELENIUM_PROMISE_MANAGER environment variable.
* When set to 1, selenium-webdriver will use the existing ControlFlow scheduler.
* When set to 0, the SimpleScheduler will be used.
*/
process.env['SELENIUM_PROMISE_MANAGER'] = '0'
promiseManager.USE_PROMISE_MANAGER = false
/**
* issue #756
* fix Chromedriver frequently hangs when attempting to start a new session.
* https://github.com/SeleniumHQ/docker-selenium/issues/87#issuecomment-187580115
*/
process.env['DBUS_SESSION_BUS_ADDRESS'] = '/dev/null'
export class BrowserDriver {
public driver: WebDriver
constructor(
private head: HeadName,
) {
log.verbose('PuppetWebBrowserDriver', 'constructor(%s)', head)
}
public async init(): Promise<void> {
log.verbose('PuppetWebBrowserDriver', 'init() for head: %s', this.head)
switch (this.head.toLowerCase()) {
case 'phantomjs':
this.driver = await this.getPhantomJsDriver()
break
case 'firefox':
this.driver = new Builder()
.setAlertBehavior('ignore')
.forBrowser('firefox')
.build()
break
case 'chrome':
this.driver = await this.getChromeDriver(false) // headless = false
break
case 'chrome-headless':
this.driver = await this.getChromeDriver(true) // headless = true
break
default: // unsupported browser head
throw new Error('unsupported head: ' + this.head)
}
const WEBDRIVER_TIMEOUT = 60 * 1000
await this.driver.manage()
.timeouts()
.setScriptTimeout(WEBDRIVER_TIMEOUT)
}
public getWebDriver(): WebDriver {
return this.driver
}
private async getChromeDriver(headless = false): Promise<WebDriver> {
log.verbose('PuppetWebBrowserDriver', 'getChromeDriver()')
const options = {
args: [
// fix 'No such session error'
// https://bugs.chromium.org/p/chromedriver/issues/detail?id=732#c19
'--disable-impl-side-painting',
'--homepage=about:blank',
// issue #26 for run inside docker
'--no-sandbox',
// '--remote-debugging-port=9222', // will conflict with webdriver
],
// binary: '/opt/google/chrome-unstable/chrome',
}
if (headless) {
const HEADLESS_ARGS = [
// --allow-insecure-localhost: Require Chrome v62
// https://bugs.chromium.org/p/chromium/issues/detail?id=721739#c26
'--allow-insecure-localhost',
'--disable-gpu',
// --headless: Require Chrome v60
// https://developers.google.com/web/updates/2017/04/headless-chrome
'--headless',
]
// ISSUE #739 Chrome v62 or above is required
// because when we are using --headless args, chrome version below 62 will not allow the
// self-signed certificate to be used when visiting https://localhost.
options.args.concat(HEADLESS_ARGS)
}
if (config.dockerMode) {
log.verbose('PuppetWebBrowserDriver', 'getChromeDriver() wechaty in docker confirmed(should not show this in CI)')
options['binary'] = config.CMD_CHROMIUM
} else {
/**
* https://github.com/Chatie/wechaty/pull/416
* In some circumstances, ChromeDriver could not be found on the current PATH when not in Docker.
* The chromedriver package always adds directory of chrome driver binary to PATH.
* So we requires chromedriver here to avoid PATH issue.
*/
require('chromedriver')
}
const customChrome = Capabilities
.chrome()
.set('chromeOptions', options)
{ // set logging
// TODO: chromedriver --silent
const prefs = new logging.Preferences()
let loggingLevel: logging.Level
switch (log.level()) {
case 'silly':
loggingLevel = logging.Level.ALL
break
case 'verbose':
loggingLevel = logging.Level.DEBUG
break
default:
loggingLevel = logging.Level.OFF
}
prefs.setLevel(logging.Type.BROWSER , loggingLevel)
prefs.setLevel(logging.Type.CLIENT , loggingLevel)
prefs.setLevel(logging.Type.DRIVER , loggingLevel)
prefs.setLevel(logging.Type.PERFORMANCE , loggingLevel)
prefs.setLevel(logging.Type.SERVER , loggingLevel)
customChrome.setLoggingPrefs(prefs)
}
/**
* XXX when will Builder().build() throw exception???
*/
let ttl = 3
let driverError = new Error('getChromeDriver() unknown invalid driver error')
let valid = false
let driver: WebDriver
while (ttl--) {
log.verbose('PuppetWebBrowserDriver', 'getChromeDriver() ttl: %d', ttl)
try {
log.verbose('PuppetWebBrowserDriver', 'getChromeDriver() new Builder()')
driver = new Builder()
.setAlertBehavior('ignore')
.forBrowser('chrome')
.withCapabilities(customChrome)
.build()
log.verbose('PuppetWebBrowserDriver', 'getChromeDriver() new Builder() done')
valid = await this.valid(driver)
log.verbose('PuppetWebBrowserDriver', 'getChromeDriver() valid() is %s at ttl %d', valid, ttl)
if (valid) {
log.silly('PuppetWebBrowserDriver', 'getChromeDriver() success')
return driver
} else {
const e = new Error('got invalid driver at ttl ' + ttl)
log.warn('PuppetWebBrowserDriver', 'getChromeDriver() %s', e.message)
driverError = e
log.verbose('PuppetWebBrowserDriver', 'getChromeDriver() driver.quit() at ttl %d', ttl)
driver.close()
.then(() => driver.quit()) // // do not await, because a invalid driver will always hang when quit()
.catch(err => {
log.warn('PuppetWebBrowserDriver', 'getChromeDriver() driver.{close,quit}() exception: %s', err.message)
driverError = err
})
} // END if
} catch (e) {
if (/could not be found/.test(e.message)) {
// The ChromeDriver could not be found on the current PATH
log.error('PuppetWebBrowserDriver', 'getChromeDriver() Wechaty require `chromedriver` to be installed.(try to run: "npm install chromedriver" to fix this issue)')
throw e
}
log.warn('PuppetWebBrowserDriver', 'getChromeDriver() ttl:%d exception: %s', ttl, e.message)
driverError = e
}
} // END while
log.warn('PuppetWebBrowserDriver', 'getChromeDriver() invalid after ttl expired: %s', driverError.stack)
throw driverError
}
private async getPhantomJsDriver(): Promise<WebDriver> {
// setup custom phantomJS capability https://github.com/SeleniumHQ/selenium/issues/2069
const phantomjsExe = require('phantomjs-prebuilt').path
if (!phantomjsExe) {
throw new Error('phantomjs binary path not found')
}
// const phantomjsExe = require('phantomjs2').path
const phantomjsArgs = [
'--load-images=false',
'--ignore-ssl-errors=true', // this help socket.io connect with localhost
'--web-security=false', // https://github.com/ariya/phantomjs/issues/12440#issuecomment-52155299
'--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
// , '--ssl-certificates-path=D:\\cygwin64\\home\\zixia\\git\\wechaty' // http://stackoverflow.com/a/32690349/1123955
// , '--ssl-client-certificate-file=cert.pem' //
]
if (config.debug) {
phantomjsArgs.push('--remote-debugger-port=8080') // XXX: be careful when in production env.
phantomjsArgs.push('--webdriver-loglevel=DEBUG')
// phantomjsArgs.push('--webdriver-logfile=webdriver.debug.log')
} else {
if (log && log.level() === 'silent') {
phantomjsArgs.push('--webdriver-loglevel=NONE')
} else {
phantomjsArgs.push('--webdriver-loglevel=ERROR')
}
}
const customPhantom = Capabilities.phantomjs()
.setAlertBehavior('ignore')
.set('phantomjs.binary.path', phantomjsExe)
.set('phantomjs.cli.args', phantomjsArgs)
log.silly('PuppetWebBrowserDriver', 'phantomjs binary: ' + phantomjsExe)
log.silly('PuppetWebBrowserDriver', 'phantomjs args: ' + phantomjsArgs.join(' '))
const driver = new Builder()
.withCapabilities(customPhantom)
.build()
// const valid = await this.valid(driver)
// if (!valid) {
// throw new Error('invalid driver founded')
// }
/**
* tslint:disable:jsdoc-format
*
* FIXME: ISSUE #21 - https://github.com/chatie/wechaty/issues/21
* http://phantomjs.org/api/webpage/handler/on-resource-requested.html
* http://stackoverflow.com/a/29544970/1123955
* https://github.com/geeeeeeeeek/electronic-wechat/pull/319
*/
// driver.executePhantomJS(`
// this.onResourceRequested = function(request, net) {
// console.log('REQUEST ' + request.url);
// blockRe = /wx\.qq\.com\/\?t=v2\/fake/i
// if (blockRe.test(request.url)) {
// console.log('Abort ' + request.url);
// net.abort();
// }
// }
// `)
// https://github.com/detro/ghostdriver/blob/f976007a431e634a3ca981eea743a2686ebed38e/src/session.js#L233
// driver.manage().timeouts().pageLoadTimeout(2000)
return driver
}
private async valid(driver: WebDriver): Promise<boolean> {
log.verbose('PuppetWebBrowserDriver', 'valid()')
if (!await this.validDriverSession(driver)) {
return false
}
if (!await this.validDriverExecute(driver)) {
return false
}
return true
}
private async validDriverExecute(driver: WebDriver): Promise<boolean> {
log.verbose('PuppetWebBrowserDriver', 'validDriverExecute()')
try {
const two = await driver.executeScript('return 1+1')
log.verbose('PuppetWebBrowserDriver', 'validDriverExecute() driver.executeScript() done: two = %s', two)
if (two === 2) {
log.silly('PuppetWebBrowserDriver', 'validDriverExecute() driver ok')
return true
} else {
log.warn('PuppetWebBrowserDriver', 'validDriverExecute() fail: two = %s ?', two)
return false
}
} catch (e) {
log.warn('BrowserDriver', 'validDriverExecute() fail: %s', e.message)
return false
}
}
private async validDriverSession(driver: WebDriver): Promise<boolean> {
log.verbose('PuppetWebBrowserDriver', 'validDriverSession()')
try {
const session = await new Promise(async (resolve, reject) => {
/**
* Be careful about this TIMEOUT, the total time(TIMEOUT x retry) should not trigger Watchdog Reset
* because we are in state(open, false) state, which will cause Watchdog Reset failure.
* https://travis-ci.org/wechaty/wechaty/jobs/179022657#L3246
*/
const TIMEOUT = 7 * 1000
let timer: NodeJS.Timer | null
timer = setTimeout(_ => {
const e = new Error('validDriverSession() driver.getSession() timeout(halt?)')
log.warn('PuppetWebBrowserDriver', e.message)
// record timeout by set timer to null
timer = null
// 1. Promise rejected
return reject(e)
}, TIMEOUT)
try {
log.verbose('PuppetWebBrowserDriver', 'validDriverSession() getSession()')
const driverSession = await driver.getSession()
log.verbose('PuppetWebBrowserDriver', 'validDriverSession() getSession() done')
// 3. Promise resolved
return resolve(driverSession)
} catch (e) {
log.warn('PuppetWebBrowserDriver', 'validDriverSession() getSession() catch() rejected: %s', e && e.message || e)
// 4. Promise rejected
return reject(e)
} finally {
if (timer) {
log.verbose('PuppetWebBrowserDriver', 'validDriverSession() getSession() clearing timer')
clearTimeout(timer)
timer = null
}
}
})
log.verbose('PuppetWebBrowserDriver', 'validDriverSession() driver.getSession() done()')
if (session) {
return true
} else {
log.verbose('PuppetWebBrowserDriver', 'validDriverSession() found an invalid driver')
return false
}
} catch (e) {
log.warn('PuppetWebBrowserDriver', 'validDriverSession() driver.getSession() exception: %s', e.message)
return false
}
}
public close() { return this.driver.close() }
public executeAsyncScript(script: string|Function, ...args: any[]) { return this.driver.executeAsyncScript.apply(this.driver, arguments) }
public executeScript (script: string|Function, ...args: any[]) { return this.driver.executeScript.apply(this.driver, arguments) }
public get(url: string) { return this.driver.get(url) }
public getSession() { return this.driver.getSession() }
public manage(): Options { return this.driver.manage() }
public navigate(): Navigation { return this.driver.navigate() }
public quit() { return this.driver.quit() }
public switchTo() { return this.driver.switchTo() }
}
// export default BrowserDriver
export {
By,
IWebDriverOptionsCookie,
Session,
TargetLocator,
}
export default BrowserDriver
/**
* Wechaty - https://github.com/chatie/wechaty
*
* @copyright 2016-2017 Huan LI <zixia@zixia.net>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { test } from 'ava'
import { Browser } from './browser'
test.serial('quit()', async t => {
const browser = new Browser()
await browser.driver.init() // init driver, not init browser
await t.throws(browser.quit(), Error, 'should throw on an un-inited browser')
browser.state.target('open')
browser.state.current('open', false)
await t.notThrows(browser.quit(), 'should not throw exception when call quit() on an `inprocess` `open` state browser')
browser.state.target('close')
browser.state.current('close')
await t.throws(browser.quit(), Error, 'should throw exception when call quit() twice on browser')
})
test.serial('init()', async t => {
const browser = new Browser()
browser.state.target('open')
browser.state.current('open')
await t.throws(browser.init(), Error, 'should throw exception when call init() on an `open` state browser')
browser.state.current('open', false)
await t.throws(browser.init(), Error, 'should throw exception when call init() on a `open`-`ing` state browser')
await browser.quit()
t.pass('should quited browser')
})
/**
* Wechaty - https://github.com/chatie/wechaty
*
* @copyright 2016-2017 Huan LI <zixia@zixia.net>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import { EventEmitter } from 'events'
// v8.x only import { promisify } from 'util'
const psTree = require('ps-tree')
const retryPromise = require('retry-promise').default // https://github.com/olalonde/retry-promise
import { StateSwitch } from 'state-switch'
import {
config,
HeadName,
log,
} from '../config'
import {
BrowserCookie,
} from './browser-cookie'
import {
BrowserDriver,
IWebDriverOptionsCookie,
By,
} from './browser-driver'
export interface BrowserSetting {
head: HeadName,
sessionFile?: string,
}
export class Browser extends EventEmitter {
private cookie: BrowserCookie
public driver: BrowserDriver
public state = new StateSwitch<'open', 'close'>('Browser', 'close', log)
constructor(
private setting: BrowserSetting = {
head: config.head,
sessionFile: '',
},
) {
super()
log.verbose('PuppetWebBrowser', 'constructor() with head(%s) sessionFile(%s)', setting.head, setting.sessionFile)
this.driver = new BrowserDriver(this.setting.head)
this.cookie = new BrowserCookie(this.driver, this.setting.sessionFile)
}
public toString() { return `Browser({head:${this.setting.head})` }
public async init(): Promise<void> {
log.verbose('PuppetWebBrowser', 'init()')
/**
* do not allow to init() twice without quit()
*/
if (this.state.current() === 'open') {
let e: Error
if (this.state.inprocess()) {
e = new Error('init() fail: current state is `open`-`ing`')
} else {
e = new Error('init() fail: current state is `open`')
}
log.error('PuppetWebBrowser', e.message)
throw e
}
this.state.target('open')
this.state.current('open', false)
const hostname = this.cookie.hostname()
// jumpUrl is used to open in browser for we can set cookies.
// backup: 'https://res.wx.qq.com/zh_CN/htmledition/v2/images/icon/ico_loading28a2f7.gif'
const jumpUrl = `https://${hostname}/zh_CN/htmledition/v2/images/webwxgeticon.jpg`
try {
await this.driver.init()
log.verbose('PuppetWebBrowser', 'init() driver.init() done')
await this.open(jumpUrl)
await this.loadCookie()
.catch(e => { // fail safe
log.verbose('PuppetWebBrowser', 'browser.loadSession(%s) exception: %s',
this.setting.sessionFile,
e && e.message || e,
)
})
await this.open()
/**
* when open url, there could happen a quit() call.
* should check here: if we are in `close` target state, we should clean up
*/
if (this.state.target() !== 'open') {
throw new Error('init() open() done, but state.target() is set to close after that. has to quit().')
}
this.state.current('open')
return
} catch (err) {
log.error('PuppetWebBrowser', 'init() exception: %s', err.message)
await this.quit()
throw err
}
}
public async hostname(): Promise<string | null> {
log.verbose('PuppetWebBrowser', 'hostname()')
const domain = await this.execute('return location.hostname')
log.silly('PuppetWebBrowser', 'hostname() got %s', domain)
return domain
}
public async open(url?: string): Promise<void> {
log.verbose('PuppetWebBrowser', 'open(%s)', url)
if (!url) {
const hostname = this.cookie.hostname()
if (!hostname) {
throw new Error('hostname unknown')
}
url = `https://${hostname}`
}
const openUrl = url
// Issue #175
// TODO: set a timer to guard driver.get timeout, then retry 3 times 201607
const TIMEOUT = 60 * 1000
let ttl = 3
while (ttl--) {
log.verbose('PuppetWebBrowser', 'open() begin for ttl:%d', ttl)
try {
await new Promise<void>(async (resolve, reject) => {
const timer = setTimeout(_ => {
const e = new Error('timeout after '
+ Math.round(TIMEOUT / 1000) + ' seconds'
+ 'at ttl:' + ttl,
)
return reject(e)
}, TIMEOUT)
try {
await this.driver.get(openUrl)
// open successful!
log.verbose('PuppetWebBrowser', 'open(%s) end at ttl:%d', openUrl, ttl)
return resolve()
} catch (e) {
return reject(e)
} finally {
clearTimeout(timer)
}
})
// open successful
return
} catch (e) {
log.error('PuppetWebBrowser', 'open() exception: %s', e.message)
await this.driver.close()
await this.driver.quit()
await this.driver.init()
log.verbose('PuppetWebBrowser', 'open() driver.{close,quit,init}() done')
}
}
await this.driver.close()
await this.driver.quit()
throw new Error('open fail because ttl expired')
}
public async refresh(): Promise<void> {
log.verbose('PuppetWebBrowser', 'refresh()')
await this.driver
.navigate()
.refresh()
return
}
public async restart(): Promise<void> {
log.verbose('PuppetWebBrowser', 'restart()')
await this.quit()
if (this.state.current() === 'open' && this.state.inprocess()) {
log.warn('PuppetWebBrowser', 'restart() found state.current() === open and inprocess() after quit()!')
return
}
await this.init()
}
public async quit(): Promise<void> {
log.verbose('PuppetWebBrowser', 'quit()')
if (this.state.current() === 'close') {
let e: Error
if (this.state.inprocess()) {
e = new Error('quit() fail: on a browser with state.current():`close` and inprocess():`true` ?')
} else { // stable
e = new Error('quit() fail: on a already quit-ed browser')
}
log.warn('PuppetWebBrowser', e.message)
throw e
}
this.state.target('close')
this.state.current('close', false)
try {
await this.driver.close()
.catch(e => { // http://stackoverflow.com/a/32341885/1123955
log.error('PuppetWebBriowser', 'quit() this.driver.close() exception %s', e.message)
})
log.silly('PuppetWebBrowser', 'quit() driver.close() done')
await this.driver.quit()
.catch( e => log.error('PuppetWebBrowser', 'quit() this.driver.quit() exception %s', e.message))
log.silly('PuppetWebBrowser', 'quit() driver.quit() done')
/**
*
* if we use AVA test runner, then this.clean might cause problems
* because there will be more than one instance of browser with the same nodejs process id
*
*/
try {
await this.clean()
} catch (e) {
await this.clean(true)
}
} catch (e) {
// log.warn('PuppetWebBrowser', 'err: %s %s %s %s', e.code, e.errno, e.syscall, e.message)
log.warn('PuppetWebBrowser', 'quit() exception: %s', e.message)
const crashMsgs = [
'ECONNREFUSED',
'WebDriverError: .* not reachable',
'NoSuchWindowError: no such window: target window already closed',
]
const crashRegex = new RegExp(crashMsgs.join('|'), 'i')
if (crashRegex.test(e.message)) { log.warn('PuppetWebBrowser', 'driver.quit() browser crashed') }
else { log.warn('PuppetWebBrowser', 'driver.quit() exception: %s', e.message) }
/* fail safe */
} finally {
this.state.current('close')
}
return
}
public async clean(kill = false): Promise<void> {
log.verbose('PuppetWebBrowser', 'clean(kill=%s)', kill)
const max = 15
const backoff = 100
/**
* issue #86 to kill orphan browser process
*/
if (kill) {
const pidList = await this.getBrowserPidList()
log.verbose('PuppetWebBrowser', 'clean() %d browsers will be killed', pidList.length)
pidList.forEach(pid => {
try {
process.kill(pid, 'SIGKILL')
} catch (e) {
log.warn('PuppetWebBrowser', 'clean(kill=true) process.kill(%d, SIGKILL) exception: %s', pid, e.message)
}
})
}
/**
* max = (2*totalTime/backoff) ^ (1/2)
* timeout = 45000 for {max: 30, backoff: 100}
* timeout = 11250 for {max: 15, backoff: 100}
*/
const timeout = max * (backoff * max) / 2
return retryPromise({max, backoff}, async attempt => {
log.silly('PuppetWebBrowser', 'clean() retryPromise: attempt %s time for timeout %s',
attempt, timeout,
)
const pidList = await this.getBrowserPidList()
if (pidList.length > 0) {
throw new Error('browser number: ' + pidList.length)
}
})
}
public async getBrowserPidList(): Promise<number[]> {
log.verbose('PuppetWebBrowser', 'getBrowserPidList()')
const head = this.setting.head
try {
const children = await new Promise<any[]>((resolve, reject) => {
psTree(process.pid, (err, c) => {
if (err) {
return reject(err)
} else {
return resolve(c)
}
})
})
let regexText: string
switch (head) {
case 'phantomjs':
regexText = 'phantomjs'
break
case 'chrome':
case 'chrome-headless':
regexText = 'chrome(?!driver)|chromium'
break
default:
const e = new Error('unsupported head: ' + head)
log.warn('PuppetWebBrowser', 'getBrowserPids() for %s', e.message)
throw e
}
const matchRegex = new RegExp(regexText, 'i')
const pids: number[] = children.filter(child => {
// https://github.com/indexzero/ps-tree/issues/18
if (matchRegex.test('' + child.COMMAND + child.COMM)) {
log.silly('PuppetWebBrowser', 'getBrowserPids() child: %s', JSON.stringify(child))
return true
}
return false
}).map(child => child.PID)
return pids
} catch (e) {
log.error('PuppetWebBrowser', 'getBrowserPidList() exception: %s', e.message || e)
throw e
}
}
public async execute(script, ...args): Promise<any> {
log.silly('PuppetWebBrowser', 'Browser.execute("%s")',
(
script.slice(0, 80)
.replace(/[\n\s]+/g, ' ')
+ (script.length > 80 ? ' ... ' : '')
),
)
// log.verbose('PuppetWebBrowser', `Browser.execute() driver.getSession: %s`, util.inspect(this.driver.getSession()))
if (this.dead()) {
const e = new Error('Browser.execute() browser dead')
log.warn('PuppetWebBrowser', 'execute() this.dead() %s', e.stack)
throw e
}
let ret
try {
ret = await this.driver.executeScript.apply(this.driver, arguments)
} catch (e) {
// this.dead(e)
log.warn('PuppetWebBrowser', 'execute() exception: %s, %s', e.message.substr(0, 99), e.stack)
log.silly('PuppetWebBrowser', 'execute() script: %s', script)
throw e
}
return ret
}
public async executeAsync(script, ...args): Promise<any> {
log.silly('PuppetWebBrowser', 'Browser.executeAsync(%s)', script.slice(0, 80))
if (this.dead()) { throw new Error('browser dead') }
try {
return await this.driver.executeAsyncScript.apply(this.driver, arguments)
} catch (e) {
// this.dead(e)
log.warn('PuppetWebBrowser', 'executeAsync() exception: %s', e.message.slice(0, 99))
throw e
}
}
/**
*
* check whether browser is full functional
*
*/
public async readyLive(): Promise<boolean> {
log.verbose('PuppetWebBrowser', 'readyLive()')
if (this.dead()) {
log.silly('PuppetWebBrowser', 'readyLive() dead() is true')
return false
}
let two
try {
two = await this.execute('return 1+1')
} catch (e) {
two = e && e.message
}
if (two === 2) {
return true // browser ok, living
}
const errMsg = 'found dead browser coz 1+1 = ' + two + ' (not 2)'
log.warn('PuppetWebBrowser', 'readyLive() %s', errMsg)
this.dead(errMsg)
return false // browser not ok, dead
}
public dead(forceReason?: any): boolean {
// too noisy!
// log.silly('PuppetWebBrowser', 'dead() checking ... ')
if ( this.state.target() === 'close'
|| this.state.current() === 'close'
// || this.state.inprocess()
) {
log.verbose('PuppetWebBrowser', 'dead() state target(%s) current(%s) stable(%s)',
this.state.target(),
this.state.current(),
this.state.stable(),
)
log.verbose('PuppetWebBrowser', 'dead() browser is in dead state')
return true
}
let msg
let dead = false
if (forceReason) {
dead = true
msg = forceReason
log.verbose('PuppetWebBrowser', 'dead(forceReason=%s) %s', forceReason, new Error().stack)
} else if (!this.driver) { // FIXME: this.driver is BrowserDriver, should add a method(sync) to check if availble 201610
dead = true
msg = 'no driver or session'
}
if (dead) {
log.warn('PuppetWebBrowser', 'dead(%s) because %s',
forceReason
? forceReason
: '',
msg,
)
if ( this.state.target() === 'open'
&& this.state.current() === 'open'
&& this.state.stable()
) {
log.verbose('PuppetWebBrowser', 'dead() emit a `dead` event because %s', msg)
this.emit('dead', msg)
} else {
log.warn('PuppetWebBrowser', 'dead() wil not emit `dead` event because states are: target(%s), current(%s), stable(%s)',
this.state.target(), this.state.current(), this.state.stable(),
)
}
}
return dead
}
public async clickSwitchAccount(): Promise<boolean> {
log.verbose('PuppetWebBrowser', 'clickSwitchAccount()')
try {
const button = await this.driver.driver.findElement(By.xpath(
"//div[contains(@class,'association') and contains(@class,'show')]/a[@ng-click='qrcodeLogin()']"))
button.click()
log.silly('PuppetWebBrowser', 'clickSwitchAccount() clicked!')
return true
} catch (e) {
log.silly('PuppetWebBrowser', 'clickSwitchAccount() button not found')
return false
}
}
public addCookie(cookies: IWebDriverOptionsCookie[]): Promise<void>
public addCookie(cookie: IWebDriverOptionsCookie): Promise<void>
public addCookie(cookie: IWebDriverOptionsCookie | IWebDriverOptionsCookie[]): Promise<void> {
return this.cookie.add(cookie)
}
public saveCookie() { return this.cookie.save() }
public loadCookie() { return this.cookie.load() }
public readCookie() { return this.cookie.read() }
public cleanCookie() { return this.cookie.clean() }
}
export {
IWebDriverOptionsCookie,
}
export default Browser
...@@ -17,245 +17,96 @@ ...@@ -17,245 +17,96 @@
* *
*/ */
import { import {
WatchdogFood,
ScanInfo,
log, log,
} from '../config' } from '../config'
import Contact from '../contact' import Contact from '../contact'
import { import {
Message, Message,
MediaMessage, MediaMessage,
MsgType,
MsgRawObj,
} from '../message' } from '../message'
import {
ScanInfo,
} from '../puppet'
import {
WatchratFood,
} from '../watchrat'
import Firer from './firer' import Firer from './firer'
import PuppetWeb from './puppet-web' import PuppetWeb from './puppet-web'
import {
MsgType,
MsgRawObj,
} from './schema'
/* tslint:disable:variable-name */ /* tslint:disable:variable-name */
export const Event = { export const Event = {
onBrowserDead, onLogin,
onLogout,
onServerLogin, onDing,
onServerLogout, onScan,
onLog,
onServerConnection, onMessage,
onServerDisconnect,
onServerDing,
onServerScan,
onServerLog,
onServerMessage,
} }
async function onBrowserDead(this: PuppetWeb, e: Error): Promise<void> { function onDing(this: PuppetWeb, data): void {
log.verbose('PuppetWebEvent', 'onBrowserDead(%s)', e && e.message || e) log.silly('PuppetWebEvent', 'onDing(%s)', data)
this.emit('watchdog', { data })
}
if (!this.browser || !this.bridge) { async function onScan(this: PuppetWeb, data: ScanInfo): Promise<void> {
throw new Error('onBrowserDead() browser or bridge instance not exist in PuppetWeb instance') log.verbose('PuppetWebEvent', 'onScan(%d)', data && data.code)
}
log.verbose('PuppetWebEvent', 'onBrowserDead() Browser:state target(%s) current(%s) stable(%s)', if (this.state.target() === 'dead') {
this.browser.state.target(), log.verbose('PuppetWebEvent', 'onScan(%s) state.target=%s, NOOP',
this.browser.state.current(), data, this.state.target())
this.browser.state.stable(),
)
if (this.browser.state.target() === 'close' || this.browser.state.inprocess()) {
log.verbose('PuppetWebEvent', 'onBrowserDead() will do nothing because %s, or %s',
'browser.state.target() === close',
'browser.state.inprocess()',
)
return return
} }
const TIMEOUT = 3 * 60 * 1000 // 3 minutes this.scanInfo = data
// this.watchDog(`onBrowserDead() set a timeout of ${Math.floor(TIMEOUT / 1000)} seconds to prevent unknown state change`, {timeout: TIMEOUT})
this.emit('watchdog', {
data: `onBrowserDead() set a timeout of ${Math.floor(TIMEOUT / 1000)} seconds to prevent unknown state change`,
timeout: TIMEOUT,
})
this.scan = null
try {
await this.browser.quit()
.catch(err => { // fail safe
log.verbose('PuppetWebEvent', 'onBrowserDead() onBrowserDead.quit() soft exception: %s', err.message)
})
log.verbose('PuppetWebEvent', 'onBrowserDead() browser.quit() done')
/**
* browser.quit() will set target() to `close`
*/
// if (this.browser.state.target() === 'close') {
// log.warn('PuppetWebEvent', 'onBrowserDead() will not init browser because browser.state.target(%s)'
// , this.browser.state.target()
// )
// return
// }
await this.initBrowser()
log.verbose('PuppetWebEvent', 'onBrowserDead() new browser inited')
await this.initBridge()
log.verbose('PuppetWebEvent', 'onBrowserDead() bridge re-inited')
const dong = await this.ding()
if (!/dong/i.test(dong)) {
const err = new Error('ding() got "' + dong + '", should be "dong" ')
log.warn('PuppetWebEvent', 'onBrowserDead() %s', err.message)
throw err
}
log.verbose('PuppetWebEvent', 'onBrowserDead() ding() works well after reset')
} catch (err) {
log.error('PuppetWebEvent', 'onBrowserDead() exception: %s', err.message)
try {
await this.quit()
await this.init()
} catch (error) {
log.warn('PuppetWebEvent', 'onBrowserDead() fail safe for this.quit(): %s', error.message)
}
}
log.verbose('PuppetWebEvent', 'onBrowserDead() new browser borned')
// why POISON here... forgot, faint. comment it out to treat dog nicer... 20161128
// this.emit('watchdog', {
// data: `onBrowserDead() new browser borned`
// , type: 'POISON'
// })
return
}
function onServerDing(this: PuppetWeb, data): void {
log.silly('PuppetWebEvent', 'onServerDing(%s)', data)
this.emit('watchdog', { data })
}
async function onServerScan(this: PuppetWeb, data: ScanInfo) {
log.verbose('PuppetWebEvent', 'onServerScan(%d)', data && data.code)
this.scan = data
/** /**
* When wx.qq.com push a new QRCode to Scan, there will be cookie updates(?) * When wx.qq.com push a new QRCode to Scan, there will be cookie updates(?)
*/ */
await this.browser.saveCookie() await this.saveCookie()
.catch(() => {/* fail safe */})
if (this.user) { if (this.user) {
log.verbose('PuppetWebEvent', 'onServerScan() there has user when got a scan event. emit logout and set it to null') log.verbose('PuppetWebEvent', 'onScan() there has user when got a scan event. emit logout and set it to null')
this.emit('logout', this.user)
const bak = this.user || this.userId || ''
this.user = this.userId = null this.user = this.userId = null
this.emit('logout', bak)
} }
// feed watchDog a `scan` type of food // feed watchDog a `scan` type of food
const food: WatchdogFood = { const food = {
data, data,
type: 'SCAN', type: 'scan',
} } as WatchratFood
this.emit('watchdog', food) this.emit('watchdog', food)
this.emit('scan' , data.url, data.code) this.emit('scan' , data.url, data.code)
} }
function onServerConnection(data) { function onLog(data: any): void {
log.verbose('PuppetWebEvent', 'onServerConnection: %s', typeof data) log.silly('PuppetWebEvent', 'onLog(%s)', data)
} }
/** async function onLogin(this: PuppetWeb, memo: string, attempt = 0): Promise<void> {
* `disconnect` event log.verbose('PuppetWebEvent', 'onLogin(%s, %d)', memo, attempt)
* after received `disconnect`, we should fix bridge by re-inject the Wechaty js code into browser.
* possible conditions:
* 1. browser refresh
* 2. browser navigated to a new url
* 3. browser quit(crash?)
* 4. ...
*/
async function onServerDisconnect(this: PuppetWeb, data): Promise<void> {
log.verbose('PuppetWebEvent', 'onServerDisconnect(%s)', data)
if (this.user) {
log.verbose('PuppetWebEvent', 'onServerDisconnect() there has user set. emit a logout event and set it to null')
this.emit('logout', this.user)
this.user = this.userId = null
}
if (this.state.current() === 'dead' && this.state.inprocess()) { if (this.state.target() === 'dead') {
log.verbose('PuppetWebEvent', 'onServerDisconnect() be called when state.current() is `dead` and inprocess()') log.verbose('PuppetWebEvent', 'onLogin(%s, %d) state.target=%s, NOOP',
return memo, attempt, this.state.target())
}
if (!this.browser || !this.bridge) {
const e = new Error('onServerDisconnect() no browser or bridge')
log.error('PuppetWebEvent', '%s', e.message)
throw e
}
/**
* conditions:
* 1. browser crash(i.e.: be killed)
*/
if (this.browser.dead()) { // browser is dead
log.verbose('PuppetWebEvent', 'onServerDisconnect() found dead browser. wait it to restore')
return return
} }
const live = await this.browser.readyLive() this.scanInfo = null
if (!live) { // browser is in indeed dead, or almost dead. readyLive() will auto recover itself.
log.verbose('PuppetWebEvent', 'onServerDisconnect() browser dead after readyLive() check. waiting it recover itself')
return
}
// browser is alive, and we have a bridge to it
log.verbose('PuppetWebEvent', 'onServerDisconnect() re-initing bridge')
// must use setTimeout to wait a while.
// because the browser has just refreshed, need some time to re-init to be ready.
// if the browser is not ready, bridge init will fail,
// caused browser dead and have to be restarted. 2016/6/12
setTimeout(_ => {
if (!this.bridge) {
// XXX: sometimes this.bridge gone in this timeout. why?
// what's happend between the last if(!this.bridge) check and the timeout call?
const e = new Error('bridge gone after setTimeout? why???')
log.warn('PuppetWebEvent', 'onServerDisconnect() setTimeout() %s', e.message)
throw e
}
this.bridge.init()
.then(() => log.verbose('PuppetWebEvent', 'onServerDisconnect() setTimeout() bridge.init() done.'))
.catch(e => log.error('PuppetWebEvent', 'onServerDisconnect() setTimeout() bridge.init() exception: [%s]', e))
}, 1000) // 1 second instead of 10 seconds? try. (should be enough to wait)
return
}
function onServerLog(data) {
log.verbose('PuppetWebEvent', 'onServerLog(%s)', data)
}
async function onServerLogin(this: PuppetWeb, data, attempt = 0): Promise<void> {
log.verbose('PuppetWebEvent', 'onServerLogin(%s, %d)', data, attempt)
// issue #772
// if `login` event fired before this.bridge inited, we delay the event for 1 second.
const args = Array.prototype.slice.call(arguments)
if (!this.bridge) {
log.verbose('PuppetWebEvent', 'onServerLogin() fired before bridge inited. delay for 1 second.')
setTimeout(() => {
onServerLogin.apply(this, args)
}, 1000)
return
}
this.scan = null
if (this.userId) { if (this.userId) {
log.verbose('PuppetWebEvent', 'onServerLogin() be called but with userId set?') log.warn('PuppetWebEvent', 'onLogin(%s) userId had already set: "%s"', memo, this.userId)
} }
try { try {
/** /**
* save login user id to this.userId * save login user id to this.userId
...@@ -265,20 +116,22 @@ async function onServerLogin(this: PuppetWeb, data, attempt = 0): Promise<void> ...@@ -265,20 +116,22 @@ async function onServerLogin(this: PuppetWeb, data, attempt = 0): Promise<void>
this.userId = await this.bridge.getUserName() this.userId = await this.bridge.getUserName()
if (!this.userId) { if (!this.userId) {
log.verbose('PuppetWebEvent', 'onServerLogin: browser not fully loaded(%d), retry later', attempt) log.verbose('PuppetWebEvent', 'onLogin: browser not fully loaded(%d), retry later', attempt)
setTimeout(onServerLogin.bind(this, data, ++attempt), 500) setTimeout(onLogin.bind(this, memo, ++attempt), 100)
return return
} }
log.silly('PuppetWebEvent', 'bridge.getUserName: %s', this.userId) log.silly('PuppetWebEvent', 'bridge.getUserName: %s', this.userId)
this.user = Contact.load(this.userId) this.user = Contact.load(this.userId)
await this.user.ready() await this.user.ready()
log.silly('PuppetWebEvent', `onServerLogin() user ${this.user.name()} logined`) log.silly('PuppetWebEvent', `onLogin() user ${this.user.name()} logined`)
try { try {
await this.browser.saveCookie() if (this.state.target() === 'live' && this.state.stable()) {
await this.saveCookie()
}
} catch (e) { // fail safe } catch (e) { // fail safe
log.verbose('PuppetWebEvent', 'onServerLogin() browser.saveSession() exception: %s', e.message) log.verbose('PuppetWebEvent', 'onLogin() this.saveCookie() exception: %s', e.message)
} }
// fix issue #668 // fix issue #668
...@@ -291,26 +144,26 @@ async function onServerLogin(this: PuppetWeb, data, attempt = 0): Promise<void> ...@@ -291,26 +144,26 @@ async function onServerLogin(this: PuppetWeb, data, attempt = 0): Promise<void>
this.emit('login', this.user) this.emit('login', this.user)
} catch (e) { } catch (e) {
log.error('PuppetWebEvent', 'onServerLogin() exception: %s', e) log.error('PuppetWebEvent', 'onLogin() exception: %s', e)
console.log(e.stack)
throw e throw e
} }
return return
} }
function onServerLogout(this: PuppetWeb, data) { function onLogout(this: PuppetWeb, data) {
this.emit('logout', this.user || this.userId) log.verbose('PuppetWebEvent', 'onLogout(%s)', data)
if (!this.user && !this.userId) { if (!this.user && !this.userId) {
log.warn('PuppetWebEvent', 'onServerLogout() without this.user or userId initialized') log.warn('PuppetWebEvent', 'onLogout() without this.user or userId initialized')
} }
this.userId = null const bak = this.user || this.userId || ''
this.user = null this.userId = this.user = null
this.emit('logout', bak)
} }
async function onServerMessage(this: PuppetWeb, obj: MsgRawObj): Promise<void> { async function onMessage(this: PuppetWeb, obj: MsgRawObj): Promise<void> {
let m = new Message(obj) let m = new Message(obj)
try { try {
...@@ -348,23 +201,23 @@ async function onServerMessage(this: PuppetWeb, obj: MsgRawObj): Promise<void> { ...@@ -348,23 +201,23 @@ async function onServerMessage(this: PuppetWeb, obj: MsgRawObj): Promise<void> {
case MsgType.VOICE: case MsgType.VOICE:
case MsgType.MICROVIDEO: case MsgType.MICROVIDEO:
case MsgType.APP: case MsgType.APP:
log.verbose('PuppetWebEvent', 'onServerMessage() EMOTICON/IMAGE/VIDEO/VOICE/MICROVIDEO message') log.verbose('PuppetWebEvent', 'onMessage() EMOTICON/IMAGE/VIDEO/VOICE/MICROVIDEO message')
m = new MediaMessage(obj) m = new MediaMessage(obj)
break break
case MsgType.TEXT: case MsgType.TEXT:
if (m.typeSub() === MsgType.LOCATION) { if (m.typeSub() === MsgType.LOCATION) {
log.verbose('PuppetWebEvent', 'onServerMessage() (TEXT&LOCATION) message') log.verbose('PuppetWebEvent', 'onMessage() (TEXT&LOCATION) message')
m = new MediaMessage(obj) m = new MediaMessage(obj)
} }
break break
} }
await m.ready() // TODO: EventEmitter2 for video/audio/app/sys.... await m.ready()
this.emit('message', m) this.emit('message', m)
} catch (e) { } catch (e) {
log.error('PuppetWebEvent', 'onServerMessage() exception: %s', e.stack) log.error('PuppetWebEvent', 'onMessage() exception: %s', e.stack)
throw e throw e
} }
......
#!/usr/bin/env ts-node
/** /**
* Wechaty - https://github.com/chatie/wechaty * Wechaty - https://github.com/chatie/wechaty
* *
...@@ -21,15 +22,18 @@ ...@@ -21,15 +22,18 @@
* Process the Message to find which event to FIRE * Process the Message to find which event to FIRE
*/ */
import { test } from 'ava' // tslint:disable:no-shadowed-variable
import * as test from 'blue-tape'
// import * as sinon from 'sinon'
// const sinonTest = require('sinon-test')(sinon)
import { Firer } from './firer' import { Firer } from './firer'
test('Firer smoke testing', t => { test('Firer smoke testing', async t => {
t.true(true, 'should be true') t.true(true, 'should be true')
}) })
test('parseFriendConfirm()', t => { test('parseFriendConfirm()', async t => {
const contentList = [ const contentList = [
[ [
'You have added 李卓桓 as your WeChat contact. Start chatting!', 'You have added 李卓桓 as your WeChat contact. Start chatting!',
...@@ -59,7 +63,7 @@ test('parseFriendConfirm()', t => { ...@@ -59,7 +63,7 @@ test('parseFriendConfirm()', t => {
t.false(result, 'should be falsy for other msg') t.false(result, 'should be falsy for other msg')
}) })
test('parseRoomJoin()', t => { test('parseRoomJoin()', async t => {
const contentList: [string, string, string[]][] = [ const contentList: [string, string, string[]][] = [
[ [
`You've invited "李卓桓" to the group chat`, `You've invited "李卓桓" to the group chat`,
...@@ -121,7 +125,7 @@ test('parseRoomJoin()', t => { ...@@ -121,7 +125,7 @@ test('parseRoomJoin()', t => {
let result let result
contentList.forEach(([content, inviter, inviteeList]) => { contentList.forEach(([content, inviter, inviteeList]) => {
result = Firer.parseRoomJoin(content) result = Firer.parseRoomJoin(content)
t.truthy(result, 'should check room join message right for ' + content) t.ok(result, 'should check room join message right for ' + content)
t.deepEqual(result[0], inviteeList, 'should get inviteeList right') t.deepEqual(result[0], inviteeList, 'should get inviteeList right')
t.is(result[1], inviter, 'should get inviter right') t.is(result[1], inviter, 'should get inviter right')
}) })
...@@ -131,7 +135,7 @@ test('parseRoomJoin()', t => { ...@@ -131,7 +135,7 @@ test('parseRoomJoin()', t => {
}, Error, 'should throws if message is not expected') }, Error, 'should throws if message is not expected')
}) })
test('parseRoomLeave()', t => { test('parseRoomLeave()', async t => {
const contentLeaverList = [ const contentLeaverList = [
[ [
`You removed "Bruce LEE" from the group chat`, `You removed "Bruce LEE" from the group chat`,
...@@ -156,13 +160,13 @@ test('parseRoomLeave()', t => { ...@@ -156,13 +160,13 @@ test('parseRoomLeave()', t => {
contentLeaverList.forEach(([content, leaver]) => { contentLeaverList.forEach(([content, leaver]) => {
const resultLeaver = Firer.parseRoomLeave(content)[0] const resultLeaver = Firer.parseRoomLeave(content)[0]
t.truthy(resultLeaver, 'should get leaver for leave message: ' + content) t.ok(resultLeaver, 'should get leaver for leave message: ' + content)
t.is(resultLeaver, leaver, 'should get leaver name right') t.is(resultLeaver, leaver, 'should get leaver name right')
}) })
contentRemoverList.forEach(([content, remover]) => { contentRemoverList.forEach(([content, remover]) => {
const resultRemover = Firer.parseRoomLeave(content)[1] const resultRemover = Firer.parseRoomLeave(content)[1]
t.truthy(resultRemover, 'should get remover for leave message: ' + content) t.ok(resultRemover, 'should get remover for leave message: ' + content)
t.is(resultRemover, remover, 'should get leaver name right') t.is(resultRemover, remover, 'should get leaver name right')
}) })
...@@ -171,7 +175,7 @@ test('parseRoomLeave()', t => { ...@@ -171,7 +175,7 @@ test('parseRoomLeave()', t => {
}, Error, 'should throw if message is not expected') }, Error, 'should throw if message is not expected')
}) })
test('parseRoomTopic()', t => { test('parseRoomTopic()', async t => {
const contentList = [ const contentList = [
[ [
`"李卓桓.PreAngel" changed the group name to "ding"`, `"李卓桓.PreAngel" changed the group name to "ding"`,
...@@ -188,7 +192,7 @@ test('parseRoomTopic()', t => { ...@@ -188,7 +192,7 @@ test('parseRoomTopic()', t => {
let result let result
contentList.forEach(([content, changer, topic]) => { contentList.forEach(([content, changer, topic]) => {
result = Firer.parseRoomTopic(content) result = Firer.parseRoomTopic(content)
t.truthy(result, 'should check topic right for content: ' + content) t.ok(result, 'should check topic right for content: ' + content)
t.is(topic , result[0], 'should get right topic') t.is(topic , result[0], 'should get right topic')
t.is(changer, result[1], 'should get right changer') t.is(changer, result[1], 'should get right changer')
}) })
......
...@@ -21,7 +21,6 @@ ...@@ -21,7 +21,6 @@
const retryPromise = require('retry-promise').default const retryPromise = require('retry-promise').default
import { import {
// RecommendInfo
log, log,
} from '../config' } from '../config'
import Contact from '../contact' import Contact from '../contact'
......
#!/usr/bin/env ts-node
/** /**
* Wechaty - https://github.com/chatie/wechaty * Wechaty - https://github.com/chatie/wechaty
* *
...@@ -16,7 +17,10 @@ ...@@ -16,7 +17,10 @@
* limitations under the License. * limitations under the License.
* *
*/ */
import { test } from 'ava' // tslint:disable:no-shadowed-variable
import * as test from 'blue-tape'
// import * as sinon from 'sinon'
// const sinonTest = require('sinon-test')(sinon)
import config from '../config' import config from '../config'
import Contact from '../contact' import Contact from '../contact'
...@@ -28,7 +32,7 @@ config.puppetInstance({ ...@@ -28,7 +32,7 @@ config.puppetInstance({
userId: 'xxx', userId: 'xxx',
} as Puppet) } as Puppet)
test('PuppetWebFriendRequest.receive smoke testing', t => { test('PuppetWebFriendRequest.receive smoke testing', async t => {
/* tslint:disable:max-line-length */ /* tslint:disable:max-line-length */
const rawMessageData = ` const rawMessageData = `
{"MsgId":"3225371967511173931","FromUserName":"fmessage","ToUserName":"@f7321198e0349f1b38c9f2ef158f70eb","MsgType":37,"Content":"&lt;msg fromusername=\\"wxid_a8d806dzznm822\\" encryptusername=\\"v1_c1e03a32c60dd9a9e14f1092132808a2de0ad363f79b303693654282954fbe4d3e12481166f4b841f28de3dd58b0bd54@stranger\\" fromnickname=\\"李卓桓.PreAngel\\" content=\\"我是群聊&amp;quot;Wechaty&amp;quot;的李卓桓.PreAngel\\" shortpy=\\"LZHPREANGEL\\" imagestatus=\\"3\\" scene=\\"14\\" country=\\"CN\\" province=\\"Beijing\\" city=\\"Haidian\\" sign=\\"投资人中最会飞的程序员。好友请加 918999 ,因为本号好友已满。\\" percard=\\"1\\" sex=\\"1\\" alias=\\"zixia008\\" weibo=\\"\\" weibonickname=\\"\\" albumflag=\\"0\\" albumstyle=\\"0\\" albumbgimgid=\\"911623988445184_911623988445184\\" snsflag=\\"49\\" snsbgimgid=\\"http://mmsns.qpic.cn/mmsns/zZSYtpeVianSQYekFNbuiajROicLficBzzeGuvQjnWdGDZ4budZovamibQnoKWba7D2LeuQRPffS8aeE/0\\" snsbgobjectid=\\"12183966160653848744\\" mhash=\\"\\" mfullhash=\\"\\" bigheadimgurl=\\"http://wx.qlogo.cn/mmhead/ver_1/xct7OPTbuU6iaS8gTaK2VibhRs3rATwnU1rCUwWu8ic89EGOynaic2Y4MUdKr66khhAplcfFlm7xbXhum5reania3fXDXH6CI9c3Bb4BODmYAh04/0\\" smallheadimgurl=\\"http://wx.qlogo.cn/mmhead/ver_1/xct7OPTbuU6iaS8gTaK2VibhRs3rATwnU1rCUwWu8ic89EGOynaic2Y4MUdKr66khhAplcfFlm7xbXhum5reania3fXDXH6CI9c3Bb4BODmYAh04/132\\" ticket=\\"v2_ba70dfbdb1b10168d61c1ab491be19e219db11ed5c28701f605efb4dccbf132f664d8a4c9ef6e852b2a4e8d8638be81d125c2e641f01903669539c53f1e582b2@stranger\\" opcode=\\"2\\" googlecontact=\\"\\" qrticket=\\"\\" chatroomusername=\\"2332413729@chatroom\\" sourceusername=\\"\\" sourcenickname=\\"\\"&gt;&lt;brandlist count=\\"0\\" ver=\\"670564024\\"&gt;&lt;/brandlist&gt;&lt;/msg&gt;","Status":3,"ImgStatus":1,"CreateTime":1475567560,"VoiceLength":0,"PlayLength":0,"FileName":"","FileSize":"","MediaId":"","Url":"","AppMsgType":0,"StatusNotifyCode":0,"StatusNotifyUserName":"","RecommendInfo":{"UserName":"@04a0fa314d0d8d50dc54e2ec908744ebf46b87404d143fd9a6692182dd90bd49","NickName":"李卓桓.PreAngel","Province":"北京","City":"海淀","Content":"我是群聊\\"Wechaty\\"的李卓桓.PreAngel","Signature":"投资人中最会飞的程序员。好友请加 918999 ,因为本号好友已满。","Alias":"zixia008","Scene":14,"AttrStatus":233251,"Sex":1,"Ticket":"v2_ba70dfbdb1b10168d61c1ab491be19e219db11ed5c28701f605efb4dccbf132f664d8a4c9ef6e852b2a4e8d8638be81d125c2e641f01903669539c53f1e582b2@stranger","OpCode":2,"HeadImgUrl":"/cgi-bin/mmwebwx-bin/webwxgeticon?seq=0&username=@04a0fa314d0d8d50dc54e2ec908744ebf46b87404d143fd9a6692182dd90bd49&skey=@crypt_f9cec94b_5b073dca472bd5e41771d309bb8c37bd&msgid=3225371967511173931","MMFromVerifyMsg":true},"ForwardFlag":0,"AppInfo":{"AppID":"","Type":0},"HasProductId":0,"Ticket":"","ImgHeight":0,"ImgWidth":0,"SubMsgType":0,"NewMsgId":3225371967511174000,"MMPeerUserName":"fmessage","MMDigest":"李卓桓.PreAngel想要将你加为朋友","MMIsSend":false,"MMIsChatRoom":false,"MMUnread":true,"LocalID":"3225371967511173931","ClientMsgId":"3225371967511173931","MMActualContent":"&lt;msg fromusername=\\"wxid_a8d806dzznm822\\" encryptusername=\\"v1_c1e03a32c60dd9a9e14f1092132808a2de0ad363f79b303693654282954fbe4d3e12481166f4b841f28de3dd58b0bd54@stranger\\" fromnickname=\\"李卓桓.PreAngel\\" content=\\"我是群聊&amp;quot;Wechaty&amp;quot;的李卓桓.PreAngel\\" shortpy=\\"LZHPREANGEL\\" imagestatus=\\"3\\" scene=\\"14\\" country=\\"CN\\" province=\\"Beijing\\" city=\\"Haidian\\" sign=\\"投资人中最会飞的程序员。好友请加 918999 ,因为本号好友已满。\\" percard=\\"1\\" sex=\\"1\\" alias=\\"zixia008\\" weibo=\\"\\" weibonickname=\\"\\" albumflag=\\"0\\" albumstyle=\\"0\\" albumbgimgid=\\"911623988445184_911623988445184\\" snsflag=\\"49\\" snsbgimgid=\\"http://mmsns.qpic.cn/mmsns/zZSYtpeVianSQYekFNbuiajROicLficBzzeGuvQjnWdGDZ4budZovamibQnoKWba7D2LeuQRPffS8aeE/0\\" snsbgobjectid=\\"12183966160653848744\\" mhash=\\"\\" mfullhash=\\"\\" bigheadimgurl=\\"http://wx.qlogo.cn/mmhead/ver_1/xct7OPTbuU6iaS8gTaK2VibhRs3rATwnU1rCUwWu8ic89EGOynaic2Y4MUdKr66khhAplcfFlm7xbXhum5reania3fXDXH6CI9c3Bb4BODmYAh04/0\\" smallheadimgurl=\\"http://wx.qlogo.cn/mmhead/ver_1/xct7OPTbuU6iaS8gTaK2VibhRs3rATwnU1rCUwWu8ic89EGOynaic2Y4MUdKr66khhAplcfFlm7xbXhum5reania3fXDXH6CI9c3Bb4BODmYAh04/132\\" ticket=\\"v2_ba70dfbdb1b10168d61c1ab491be19e219db11ed5c28701f605efb4dccbf132f664d8a4c9ef6e852b2a4e8d8638be81d125c2e641f01903669539c53f1e582b2@stranger\\" opcode=\\"2\\" googlecontact=\\"\\" qrticket=\\"\\" chatroomusername=\\"2332413729@chatroom\\" sourceusername=\\"\\" sourcenickname=\\"\\"&gt;&lt;brandlist count=\\"0\\" ver=\\"670564024\\"&gt;&lt;/brandlist&gt;&lt;/msg&gt;","MMActualSender":"fmessage","MMDigestTime":"15:52","MMDisplayTime":1475567560,"MMTime":"15:52"} {"MsgId":"3225371967511173931","FromUserName":"fmessage","ToUserName":"@f7321198e0349f1b38c9f2ef158f70eb","MsgType":37,"Content":"&lt;msg fromusername=\\"wxid_a8d806dzznm822\\" encryptusername=\\"v1_c1e03a32c60dd9a9e14f1092132808a2de0ad363f79b303693654282954fbe4d3e12481166f4b841f28de3dd58b0bd54@stranger\\" fromnickname=\\"李卓桓.PreAngel\\" content=\\"我是群聊&amp;quot;Wechaty&amp;quot;的李卓桓.PreAngel\\" shortpy=\\"LZHPREANGEL\\" imagestatus=\\"3\\" scene=\\"14\\" country=\\"CN\\" province=\\"Beijing\\" city=\\"Haidian\\" sign=\\"投资人中最会飞的程序员。好友请加 918999 ,因为本号好友已满。\\" percard=\\"1\\" sex=\\"1\\" alias=\\"zixia008\\" weibo=\\"\\" weibonickname=\\"\\" albumflag=\\"0\\" albumstyle=\\"0\\" albumbgimgid=\\"911623988445184_911623988445184\\" snsflag=\\"49\\" snsbgimgid=\\"http://mmsns.qpic.cn/mmsns/zZSYtpeVianSQYekFNbuiajROicLficBzzeGuvQjnWdGDZ4budZovamibQnoKWba7D2LeuQRPffS8aeE/0\\" snsbgobjectid=\\"12183966160653848744\\" mhash=\\"\\" mfullhash=\\"\\" bigheadimgurl=\\"http://wx.qlogo.cn/mmhead/ver_1/xct7OPTbuU6iaS8gTaK2VibhRs3rATwnU1rCUwWu8ic89EGOynaic2Y4MUdKr66khhAplcfFlm7xbXhum5reania3fXDXH6CI9c3Bb4BODmYAh04/0\\" smallheadimgurl=\\"http://wx.qlogo.cn/mmhead/ver_1/xct7OPTbuU6iaS8gTaK2VibhRs3rATwnU1rCUwWu8ic89EGOynaic2Y4MUdKr66khhAplcfFlm7xbXhum5reania3fXDXH6CI9c3Bb4BODmYAh04/132\\" ticket=\\"v2_ba70dfbdb1b10168d61c1ab491be19e219db11ed5c28701f605efb4dccbf132f664d8a4c9ef6e852b2a4e8d8638be81d125c2e641f01903669539c53f1e582b2@stranger\\" opcode=\\"2\\" googlecontact=\\"\\" qrticket=\\"\\" chatroomusername=\\"2332413729@chatroom\\" sourceusername=\\"\\" sourcenickname=\\"\\"&gt;&lt;brandlist count=\\"0\\" ver=\\"670564024\\"&gt;&lt;/brandlist&gt;&lt;/msg&gt;","Status":3,"ImgStatus":1,"CreateTime":1475567560,"VoiceLength":0,"PlayLength":0,"FileName":"","FileSize":"","MediaId":"","Url":"","AppMsgType":0,"StatusNotifyCode":0,"StatusNotifyUserName":"","RecommendInfo":{"UserName":"@04a0fa314d0d8d50dc54e2ec908744ebf46b87404d143fd9a6692182dd90bd49","NickName":"李卓桓.PreAngel","Province":"北京","City":"海淀","Content":"我是群聊\\"Wechaty\\"的李卓桓.PreAngel","Signature":"投资人中最会飞的程序员。好友请加 918999 ,因为本号好友已满。","Alias":"zixia008","Scene":14,"AttrStatus":233251,"Sex":1,"Ticket":"v2_ba70dfbdb1b10168d61c1ab491be19e219db11ed5c28701f605efb4dccbf132f664d8a4c9ef6e852b2a4e8d8638be81d125c2e641f01903669539c53f1e582b2@stranger","OpCode":2,"HeadImgUrl":"/cgi-bin/mmwebwx-bin/webwxgeticon?seq=0&username=@04a0fa314d0d8d50dc54e2ec908744ebf46b87404d143fd9a6692182dd90bd49&skey=@crypt_f9cec94b_5b073dca472bd5e41771d309bb8c37bd&msgid=3225371967511173931","MMFromVerifyMsg":true},"ForwardFlag":0,"AppInfo":{"AppID":"","Type":0},"HasProductId":0,"Ticket":"","ImgHeight":0,"ImgWidth":0,"SubMsgType":0,"NewMsgId":3225371967511174000,"MMPeerUserName":"fmessage","MMDigest":"李卓桓.PreAngel想要将你加为朋友","MMIsSend":false,"MMIsChatRoom":false,"MMUnread":true,"LocalID":"3225371967511173931","ClientMsgId":"3225371967511173931","MMActualContent":"&lt;msg fromusername=\\"wxid_a8d806dzznm822\\" encryptusername=\\"v1_c1e03a32c60dd9a9e14f1092132808a2de0ad363f79b303693654282954fbe4d3e12481166f4b841f28de3dd58b0bd54@stranger\\" fromnickname=\\"李卓桓.PreAngel\\" content=\\"我是群聊&amp;quot;Wechaty&amp;quot;的李卓桓.PreAngel\\" shortpy=\\"LZHPREANGEL\\" imagestatus=\\"3\\" scene=\\"14\\" country=\\"CN\\" province=\\"Beijing\\" city=\\"Haidian\\" sign=\\"投资人中最会飞的程序员。好友请加 918999 ,因为本号好友已满。\\" percard=\\"1\\" sex=\\"1\\" alias=\\"zixia008\\" weibo=\\"\\" weibonickname=\\"\\" albumflag=\\"0\\" albumstyle=\\"0\\" albumbgimgid=\\"911623988445184_911623988445184\\" snsflag=\\"49\\" snsbgimgid=\\"http://mmsns.qpic.cn/mmsns/zZSYtpeVianSQYekFNbuiajROicLficBzzeGuvQjnWdGDZ4budZovamibQnoKWba7D2LeuQRPffS8aeE/0\\" snsbgobjectid=\\"12183966160653848744\\" mhash=\\"\\" mfullhash=\\"\\" bigheadimgurl=\\"http://wx.qlogo.cn/mmhead/ver_1/xct7OPTbuU6iaS8gTaK2VibhRs3rATwnU1rCUwWu8ic89EGOynaic2Y4MUdKr66khhAplcfFlm7xbXhum5reania3fXDXH6CI9c3Bb4BODmYAh04/0\\" smallheadimgurl=\\"http://wx.qlogo.cn/mmhead/ver_1/xct7OPTbuU6iaS8gTaK2VibhRs3rATwnU1rCUwWu8ic89EGOynaic2Y4MUdKr66khhAplcfFlm7xbXhum5reania3fXDXH6CI9c3Bb4BODmYAh04/132\\" ticket=\\"v2_ba70dfbdb1b10168d61c1ab491be19e219db11ed5c28701f605efb4dccbf132f664d8a4c9ef6e852b2a4e8d8638be81d125c2e641f01903669539c53f1e582b2@stranger\\" opcode=\\"2\\" googlecontact=\\"\\" qrticket=\\"\\" chatroomusername=\\"2332413729@chatroom\\" sourceusername=\\"\\" sourcenickname=\\"\\"&gt;&lt;brandlist count=\\"0\\" ver=\\"670564024\\"&gt;&lt;/brandlist&gt;&lt;/msg&gt;","MMActualSender":"fmessage","MMDigestTime":"15:52","MMDisplayTime":1475567560,"MMTime":"15:52"}
...@@ -45,7 +49,7 @@ test('PuppetWebFriendRequest.receive smoke testing', t => { ...@@ -45,7 +49,7 @@ test('PuppetWebFriendRequest.receive smoke testing', t => {
t.is(fr.type as any, 'receive', 'should be receive type') t.is(fr.type as any, 'receive', 'should be receive type')
}) })
test('PuppetWebFriendRequest.confirm smoke testing', t => { test('PuppetWebFriendRequest.confirm smoke testing', async t => {
/* tslint:disable:max-line-length */ /* tslint:disable:max-line-length */
const rawMessageData = ` const rawMessageData = `
{"MsgId":"3382012679535022763","FromUserName":"@04a0fa314d0d8d50dc54e2ec908744ebf46b87404d143fd9a6692182dd90bd49","ToUserName":"@f7321198e0349f1b38c9f2ef158f70eb","MsgType":10000,"Content":"You have added 李卓桓.PreAngel as your WeChat contact. Start chatting!","Status":4,"ImgStatus":1,"CreateTime":1475569920,"VoiceLength":0,"PlayLength":0,"FileName":"","FileSize":"","MediaId":"","Url":"","AppMsgType":0,"StatusNotifyCode":0,"StatusNotifyUserName":"","RecommendInfo":{"UserName":"","NickName":"","QQNum":0,"Province":"","City":"","Content":"","Signature":"","Alias":"","Scene":0,"VerifyFlag":0,"AttrStatus":0,"Sex":0,"Ticket":"","OpCode":0},"ForwardFlag":0,"AppInfo":{"AppID":"","Type":0},"HasProductId":0,"Ticket":"","ImgHeight":0,"ImgWidth":0,"SubMsgType":0,"NewMsgId":3382012679535022600,"MMPeerUserName":"@04a0fa314d0d8d50dc54e2ec908744ebf46b87404d143fd9a6692182dd90bd49","MMDigest":"You have added 李卓桓.PreAngel as your WeChat contact. Start chatting!","MMIsSend":false,"MMIsChatRoom":false,"LocalID":"3382012679535022763","ClientMsgId":"3382012679535022763","MMActualContent":"You have added 李卓桓.PreAngel as your WeChat contact. Start chatting!","MMActualSender":"@04a0fa314d0d8d50dc54e2ec908744ebf46b87404d143fd9a6692182dd90bd49","MMDigestTime":"16:32","MMDisplayTime":1475569920,"MMTime":"16:32"} {"MsgId":"3382012679535022763","FromUserName":"@04a0fa314d0d8d50dc54e2ec908744ebf46b87404d143fd9a6692182dd90bd49","ToUserName":"@f7321198e0349f1b38c9f2ef158f70eb","MsgType":10000,"Content":"You have added 李卓桓.PreAngel as your WeChat contact. Start chatting!","Status":4,"ImgStatus":1,"CreateTime":1475569920,"VoiceLength":0,"PlayLength":0,"FileName":"","FileSize":"","MediaId":"","Url":"","AppMsgType":0,"StatusNotifyCode":0,"StatusNotifyUserName":"","RecommendInfo":{"UserName":"","NickName":"","QQNum":0,"Province":"","City":"","Content":"","Signature":"","Alias":"","Scene":0,"VerifyFlag":0,"AttrStatus":0,"Sex":0,"Ticket":"","OpCode":0},"ForwardFlag":0,"AppInfo":{"AppID":"","Type":0},"HasProductId":0,"Ticket":"","ImgHeight":0,"ImgWidth":0,"SubMsgType":0,"NewMsgId":3382012679535022600,"MMPeerUserName":"@04a0fa314d0d8d50dc54e2ec908744ebf46b87404d143fd9a6692182dd90bd49","MMDigest":"You have added 李卓桓.PreAngel as your WeChat contact. Start chatting!","MMIsSend":false,"MMIsChatRoom":false,"LocalID":"3382012679535022763","ClientMsgId":"3382012679535022763","MMActualContent":"You have added 李卓桓.PreAngel as your WeChat contact. Start chatting!","MMActualSender":"@04a0fa314d0d8d50dc54e2ec908744ebf46b87404d143fd9a6692182dd90bd49","MMDigestTime":"16:32","MMDisplayTime":1475569920,"MMTime":"16:32"}
......
...@@ -32,11 +32,14 @@ const retryPromise = require('retry-promise').default ...@@ -32,11 +32,14 @@ const retryPromise = require('retry-promise').default
import { Contact } from '../contact' import { Contact } from '../contact'
import { import {
config, config,
RecommendInfo,
log, log,
} from '../config' } from '../config'
import FriendRequest from '../friend-request' import FriendRequest from '../friend-request'
import {
RecommendInfo,
} from './schema'
/** /**
* @alias FriendRequest * @alias FriendRequest
*/ */
......
#!/usr/bin/env ts-node
/** /**
* Wechaty - https://github.com/chatie/wechaty * Wechaty - https://github.com/chatie/wechaty
* *
...@@ -16,21 +17,18 @@ ...@@ -16,21 +17,18 @@
* limitations under the License. * limitations under the License.
* *
*/ */
import { test } from 'ava' // tslint:disable:no-shadowed-variable
import * as test from 'blue-tape'
// import * as sinon from 'sinon'
import { import {
Bridge, Bridge,
Browser,
Event, Event,
PuppetWeb, PuppetWeb,
Server,
Watchdog,
} from './index' } from './index'
test('PuppetWeb Module Exports', t => { test('PuppetWeb Module Exports', async t => {
t.truthy(PuppetWeb , 'should export PuppetWeb') t.ok(PuppetWeb , 'should export PuppetWeb')
t.truthy(Event , 'should export Event') t.ok(Event , 'should export Event')
t.truthy(Watchdog , 'should export Watchdog') t.ok(Bridge , 'should export Bridge')
t.truthy(Server , 'should export Server')
t.truthy(Browser , 'should export Browser')
t.truthy(Bridge , 'should export Bridge')
}) })
...@@ -17,17 +17,11 @@ ...@@ -17,17 +17,11 @@
* *
*/ */
export { Bridge } from './bridge' export { Bridge } from './bridge'
export {
Browser,
IWebDriverOptionsCookie,
} from './browser'
export { Event } from './event' export { Event } from './event'
export { export {
PuppetWebFriendRequest as FriendRequest, PuppetWebFriendRequest as FriendRequest,
} from './friend-request' } from './friend-request'
import { PuppetWeb } from './puppet-web' import { PuppetWeb } from './puppet-web'
export { Server } from './server'
export { Watchdog } from './watchdog'
export default PuppetWeb export default PuppetWeb
export { export {
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
/**
* Wechaty - https://github.com/chatie/wechaty
*
* @copyright 2016-2017 Huan LI <zixia@zixia.net>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
/**
*
* Ssl Key & Cert files.
*
* Hardcoded here, NO need to re-config.
* because there will only be visit from 127.0.0.1
* so it will not be a security issue.
*
* http://blog.mgechev.com/2014/02/19/create-https-tls-ssl-application-with-express-nodejs/
* openssl req -x509 -days 3650 -nodes -newkey rsa:2048 -keyout key.pem -out cert.pem
* openssl rsa -in key.pem -out newkey.pem && mv newkey.pem key.pem
*
* Reference:
* What is a Pem file - http://serverfault.com/a/9717
*/
const key = `
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAt1c10tCbJG5ydWPjBV5c4gA3f1/8ubhNnYj98dtFPR8a4VPk
ORCyst157tLq5uPgmlZLedAm08VFiDUwn8iGQI/RegQSuRjxaS2MccfP9jpzDazy
eMBi0mLg25z/4v9y/8N9nLSqbHrPrye+hzpSkSkyQ/zf/P85ZCdTGwCnFX6WlBQX
I/3o5wWWRv4DaZTaLhChHjAa6+HJdYFvDvI6QUxggj3Vq64HsJv2xGvG7pZWjxXp
FS0Mg8MQbHME1J92OwPNaqsNUY3JfPkaeYmfQi5Qy53ULGLxVgV0eyIFf6s4NSCr
FI4PjJXyBWYCAlxVIUm1WdylIAl4MVvGADA1mQIDAQABAoIBAEOFUsU5HmnkYzLo
fotTnVF+UvIOH70mKy+BbETOREmmUvf5NWvuwmEtP+K8utYdxnIQpetOxX3ogRsQ
u7+c0hSk4rjVFzAkB4R8yeR9ehFspUK8FvBxqfNhhv5aa8Ll4SxgirpTrxAUirgv
IvQafp4HVgPD9ZnvROulr+2Z5+7596qif4F1HrrxN6tl+cGNZFIZ7vk7uGsF8k4G
LQ8xik8QABcTE4pKpRtNlesRpojSGI8cnu/z8MgDIPMHu6wgdz+OR1rZwNMuREZi
zejf5gg82B1KxeFNEmtMI1GM+whKVkPBxwASJTOaiN2Oh7SdSO5SHxFv2bAxjJ6z
SC4mwqECgYEA7YSIINzOTCkUGGcJrg14P0m1KxuoPFSoAXk61F6kwW1FQVPmfk4i
n/MO8+2/CSAZiFEFNTUvWj5xM955wDVgSY8Z7l/aYxn11gGzV0XypsK77edTRrfp
6AlvIepclSX5ocmhizHe4mrm8KaZ014qMtO3RUaoDA9h7zqQOK5OyxcCgYEAxZty
Dy7IxOBk3QfGdVqto/oDX2fQ6PTAIYwuOAh6rrQDkSXShPWI3bUJegylQeVOYG40
3ti/fd/247OkFwPsPODNisWQTsdX5Kr4KWjmfTSpSDm1AkDvnuPk/tcyFhijxZ48
6Q0ZL529oy7cwel3p3uzDIFbAMEdATKIRp9css8CgYAkJNfmUFOgaVvifsONVgVn
dBr6rWHDlIpgdwdJzAE8Yhl44ICh1dgVCRLMcfBxPg5EnTeyqh5DmF73qrJSWo0F
hJ5IlRORoyCy6V1WOZG8aMPaZypYB6KzqcPcoGJoW/gJ87n+iZ9GS0hLdL7R2HGJ
fIhWJXNrKmgX1Iyf436gDwKBgQC57ekEICEIHZrJ3eb9xLRc9YD249fNWXzuE9fp
IRFOEFLK36uVLvH4qb6g+AUGW5vDX+6fP5Ht/i1vUjey8B33qg275OhDN42busKF
NA6rAEHHk4Sc+jx8ZDGzFwgpgkWWS61EGu73vpQQVqegTOwoyltOCOh3bTy9Q661
xHyUQQKBgA1vFFy09wJmQNCoo2kLvghkyPHrBXQlzoRW3cafw+vwoYZFukxst0dd
2mQ3CyRJ7buoDdj0cFVlScB7KSQtIvLMtAn6tkL6ecooep346OSoLlsQd/F5ODep
bBdRj0Orj7xQDeIicM7ASTYPAivh9NTu4yJL0r/YOX3OvXxaBSGf
-----END RSA PRIVATE KEY-----
`
const cert = `
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKMz7h5gRwqgMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTYwNTAxMTUyNzM5WhcNMTcwNTAxMTUyNzM5WjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAt1c10tCbJG5ydWPjBV5c4gA3f1/8ubhNnYj98dtFPR8a4VPkORCyst15
7tLq5uPgmlZLedAm08VFiDUwn8iGQI/RegQSuRjxaS2MccfP9jpzDazyeMBi0mLg
25z/4v9y/8N9nLSqbHrPrye+hzpSkSkyQ/zf/P85ZCdTGwCnFX6WlBQXI/3o5wWW
Rv4DaZTaLhChHjAa6+HJdYFvDvI6QUxggj3Vq64HsJv2xGvG7pZWjxXpFS0Mg8MQ
bHME1J92OwPNaqsNUY3JfPkaeYmfQi5Qy53ULGLxVgV0eyIFf6s4NSCrFI4PjJXy
BWYCAlxVIUm1WdylIAl4MVvGADA1mQIDAQABo1AwTjAdBgNVHQ4EFgQU/Sed0ljf
HEpQsmReiphJnSsPTFowHwYDVR0jBBgwFoAU/Sed0ljfHEpQsmReiphJnSsPTFow
DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAXjqJfXSEwVktRmeB+tW0
F837NEfzyedP82gSCCC+pbs+4E6DbDANupxP8vqIfqTe03YScZHVR/ha/f/WPLpc
HvDuyOSXrms9x0OHxsH70Ajx5/JBWyBbtFdox6yCEoeydOXl+MQDXgnGGv8VFXdN
dd2RP6/Ovx88hYGWcwf4RekTrbsM40n7BkkCCEedZPy7ouRmAs2FXpd+cm3zD9jt
Bas7b0wEOA7H2HejkbFOUierE40Kzh72vDD7M6DqUZFSvClY0O0+EYefB5TiRsN0
g+Xdc4Ag/St5eqgrp95KOlVeepSlb35LAD1Cc91LddTXCYS7+dc4ndQYpgrLU0ru
Sw==
-----END CERTIFICATE-----
`
export { cert, key }
此差异已折叠。
此差异已折叠。
#!/usr/bin/env ts-node
/** /**
* Wechaty - https://github.com/chatie/wechaty * Wechaty - https://github.com/chatie/wechaty
* *
...@@ -16,11 +17,16 @@ ...@@ -16,11 +17,16 @@
* limitations under the License. * limitations under the License.
* *
*/ */
import { test } from 'ava' // tslint:disable:no-shadowed-variable
import * as test from 'blue-tape'
// import * as sinon from 'sinon'
import Profile from './profile'
import PuppetWeb from './puppet-web' import PuppetWeb from './puppet-web'
test('Puppet smoke testing', t => { test('Puppet smoke testing', async t => {
const p = new PuppetWeb() const profile = new Profile(Math.random().toString(36).substr(2, 5))
const p = new PuppetWeb({ profile })
t.is(p.state.target(), 'dead', 'should be dead target state after instanciate') t.is(p.state.target(), 'dead', 'should be dead target state after instanciate')
t.is(p.state.current(), 'dead', 'should be dead current state after instanciate') t.is(p.state.current(), 'dead', 'should be dead current state after instanciate')
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
(function() {
const retObj = {
code: 42,
message: 'meaning of the life',
}
return retObj
}())
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册