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

Merge branch 'master' of github.com:Chatie/wechaty

language: node_js
node_js:
- "9"
- "10"
os:
- linux
......@@ -21,24 +22,21 @@ stages:
- test
- pack
- name: deploy
if: branch =~ ^(master|v\d+\.\d+)$
AND (type NOT IN (cron, pull_request))
if: branch =~ ^(master|v\d+\.\d+)$ AND (type NOT IN (cron, pull_request))
before_install:
- if [ "$TRAVIS_OS_NAME" == 'osx' ]; then ./scripts/prepare-osx.sh; fi
script:
- echo $TRAVIS_OS_NAME
- node --version
- npm --version
- echo "Testing started ..."
- npm test || travis_terminate 1
# - if [ "$TRAVIS_OS_NAME" == 'linux' ]; then npm run coverage; fi
jobs:
include:
- stage: test
script:
- echo $TRAVIS_OS_NAME
- node --version
- npm --version
- echo "Testing started ..."
- npm test || travis_terminate 1
after_success:
- if [ "$TRAVIS_OS_NAME" == 'linux' ]; then npm run coverage; fi
- stage: pack
script:
......
WECHATY CONTRIBUTORS
--------------------
# CHANGELOG
## WECHATY CONTRIBUTORS
### Active Contributors
1. @[zixia](https://github.com/zixia): [\#1160](https://github.com/Chatie/wechaty/pull/1160),[\#1232](https://github.com/Chatie/wechaty/pull/1232),[\#1231](https://github.com/Chatie/wechaty/pull/1231),[\#1190](https://github.com/Chatie/wechaty/pull/1190),[\#1159](https://github.com/Chatie/wechaty/pull/1159),[\#1143](https://github.com/Chatie/wechaty/pull/1143),[\#1131](https://github.com/Chatie/wechaty/pull/1131),[\#1083](https://github.com/Chatie/wechaty/pull/1083),[\#1075](https://github.com/Chatie/wechaty/pull/1075),[\#1074](https://github.com/Chatie/wechaty/pull/1074),[\#1073](https://github.com/Chatie/wechaty/pull/1073),[\#1072](https://github.com/Chatie/wechaty/pull/1072),[\#1071](https://github.com/Chatie/wechaty/pull/1071),[\#860](https://github.com/Chatie/wechaty/pull/860),[\#854](https://github.com/Chatie/wechaty/pull/854),[\#841](https://github.com/Chatie/wechaty/pull/841),[\#831](https://github.com/Chatie/wechaty/pull/831),[\#810](https://github.com/Chatie/wechaty/pull/810),[\#644](https://github.com/Chatie/wechaty/pull/644),[\#643](https://github.com/Chatie/wechaty/pull/643),[\#608](https://github.com/Chatie/wechaty/pull/608),[\#569](https://github.com/Chatie/wechaty/pull/569),[\#560](https://github.com/Chatie/wechaty/pull/560),[\#542](https://github.com/Chatie/wechaty/pull/542),[\#496](https://github.com/Chatie/wechaty/pull/496),[\#495](https://github.com/Chatie/wechaty/pull/495),[\#469](https://github.com/Chatie/wechaty/pull/469),[\#462](https://github.com/Chatie/wechaty/pull/462),[\#455](https://github.com/Chatie/wechaty/pull/455),[\#449](https://github.com/Chatie/wechaty/pull/449),[\#396](https://github.com/Chatie/wechaty/pull/396),[\#351](https://github.com/Chatie/wechaty/pull/351),[\#317](https://github.com/Chatie/wechaty/pull/317),[\#316](https://github.com/Chatie/wechaty/pull/316),[\#315](https://github.com/Chatie/wechaty/pull/315),[\#314](https://github.com/Chatie/wechaty/pull/314),[\#313](https://github.com/Chatie/wechaty/pull/313),[\#312](https://github.com/Chatie/wechaty/pull/312),[\#311](https://github.com/Chatie/wechaty/pull/311),[\#168](https://github.com/Chatie/wechaty/pull/168),[\#158](https://github.com/Chatie/wechaty/pull/158),[\#149](https://github.com/Chatie/wechaty/pull/149),[\#146](https://github.com/Chatie/wechaty/pull/146),[\#143](https://github.com/Chatie/wechaty/pull/143),[\#142](https://github.com/Chatie/wechaty/pull/142),[\#141](https://github.com/Chatie/wechaty/pull/141),[\#25](https://github.com/Chatie/wechaty/pull/25)
1. @[lijiarui](https://github.com/lijiarui): [\#1116](https://github.com/Chatie/wechaty/pull/1116),[\#1086](https://github.com/Chatie/wechaty/pull/1086),[\#816](https://github.com/Chatie/wechaty/pull/816),[\#812](https://github.com/Chatie/wechaty/pull/812),[\#805](https://github.com/Chatie/wechaty/pull/805),[\#798](https://github.com/Chatie/wechaty/pull/798),[\#757](https://github.com/Chatie/wechaty/pull/757),[\#729](https://github.com/Chatie/wechaty/pull/729),[\#725](https://github.com/Chatie/wechaty/pull/725),[\#651](https://github.com/Chatie/wechaty/pull/651),[\#627](https://github.com/Chatie/wechaty/pull/627),[\#619](https://github.com/Chatie/wechaty/pull/619),[\#604](https://github.com/Chatie/wechaty/pull/604),[\#515](https://github.com/Chatie/wechaty/pull/515),[\#490](https://github.com/Chatie/wechaty/pull/490),[\#440](https://github.com/Chatie/wechaty/pull/440),[\#370](https://github.com/Chatie/wechaty/pull/370),[\#364](https://github.com/Chatie/wechaty/pull/364),[\#362](https://github.com/Chatie/wechaty/pull/362),[\#328](https://github.com/Chatie/wechaty/pull/328),[\#324](https://github.com/Chatie/wechaty/pull/324),[\#323](https://github.com/Chatie/wechaty/pull/323),[\#321](https://github.com/Chatie/wechaty/pull/321),[\#318](https://github.com/Chatie/wechaty/pull/318),[\#303](https://github.com/Chatie/wechaty/pull/303),[\#292](https://github.com/Chatie/wechaty/pull/292),[\#275](https://github.com/Chatie/wechaty/pull/275),[\#266](https://github.com/Chatie/wechaty/pull/266),[\#264](https://github.com/Chatie/wechaty/pull/264),[\#249](https://github.com/Chatie/wechaty/pull/249),[\#239](https://github.com/Chatie/wechaty/pull/239),[\#234](https://github.com/Chatie/wechaty/pull/234),[\#211](https://github.com/Chatie/wechaty/pull/211),[\#199](https://github.com/Chatie/wechaty/pull/199),[\#182](https://github.com/Chatie/wechaty/pull/182),[\#162](https://github.com/Chatie/wechaty/pull/162),[\#139](https://github.com/Chatie/wechaty/pull/139),[\#112](https://github.com/Chatie/wechaty/pull/112),[\#110](https://github.com/Chatie/wechaty/pull/110),[\#93](https://github.com/Chatie/wechaty/pull/93),[\#92](https://github.com/Chatie/wechaty/pull/92),[\#91](https://github.com/Chatie/wechaty/pull/91),[\#87](https://github.com/Chatie/wechaty/pull/87),[\#38](https://github.com/Chatie/wechaty/pull/38)
1. @[zixia](https://github.com/zixia): [\#1143](https://github.com/Chatie/wechaty/pull/1143),[\#1131](https://github.com/Chatie/wechaty/pull/1131),[\#1083](https://github.com/Chatie/wechaty/pull/1083),[\#1075](https://github.com/Chatie/wechaty/pull/1075),[\#1074](https://github.com/Chatie/wechaty/pull/1074),[\#1073](https://github.com/Chatie/wechaty/pull/1073),[\#1072](https://github.com/Chatie/wechaty/pull/1072),[\#1071](https://github.com/Chatie/wechaty/pull/1071),[\#860](https://github.com/Chatie/wechaty/pull/860),[\#854](https://github.com/Chatie/wechaty/pull/854),[\#841](https://github.com/Chatie/wechaty/pull/841),[\#831](https://github.com/Chatie/wechaty/pull/831),[\#810](https://github.com/Chatie/wechaty/pull/810),[\#644](https://github.com/Chatie/wechaty/pull/644),[\#643](https://github.com/Chatie/wechaty/pull/643),[\#608](https://github.com/Chatie/wechaty/pull/608),[\#569](https://github.com/Chatie/wechaty/pull/569),[\#560](https://github.com/Chatie/wechaty/pull/560),[\#542](https://github.com/Chatie/wechaty/pull/542),[\#496](https://github.com/Chatie/wechaty/pull/496),[\#495](https://github.com/Chatie/wechaty/pull/495),[\#469](https://github.com/Chatie/wechaty/pull/469),[\#462](https://github.com/Chatie/wechaty/pull/462),[\#455](https://github.com/Chatie/wechaty/pull/455),[\#449](https://github.com/Chatie/wechaty/pull/449),[\#396](https://github.com/Chatie/wechaty/pull/396),[\#351](https://github.com/Chatie/wechaty/pull/351),[\#317](https://github.com/Chatie/wechaty/pull/317),[\#316](https://github.com/Chatie/wechaty/pull/316),[\#315](https://github.com/Chatie/wechaty/pull/315),[\#314](https://github.com/Chatie/wechaty/pull/314),[\#313](https://github.com/Chatie/wechaty/pull/313),[\#312](https://github.com/Chatie/wechaty/pull/312),[\#311](https://github.com/Chatie/wechaty/pull/311),[\#168](https://github.com/Chatie/wechaty/pull/168),[\#158](https://github.com/Chatie/wechaty/pull/158),[\#149](https://github.com/Chatie/wechaty/pull/149),[\#146](https://github.com/Chatie/wechaty/pull/146),[\#143](https://github.com/Chatie/wechaty/pull/143),[\#142](https://github.com/Chatie/wechaty/pull/142),[\#141](https://github.com/Chatie/wechaty/pull/141),[\#25](https://github.com/Chatie/wechaty/pull/25)
1. @[mukaiu](https://github.com/mukaiu): [\#1089](https://github.com/Chatie/wechaty/pull/1089),[\#962](https://github.com/Chatie/wechaty/pull/962),[\#337](https://github.com/Chatie/wechaty/pull/337),[\#470](https://github.com/Chatie/wechaty/pull/470),[\#438](https://github.com/Chatie/wechaty/pull/438),[\#421](https://github.com/Chatie/wechaty/pull/421),[\#420](https://github.com/Chatie/wechaty/pull/420),[\#415](https://github.com/Chatie/wechaty/pull/415),[\#376](https://github.com/Chatie/wechaty/pull/376)
1. @[binsee](https://github.com/binsee): [\#844](https://github.com/Chatie/wechaty/pull/844),[\#811](https://github.com/Chatie/wechaty/pull/811),[\#771](https://github.com/Chatie/wechaty/pull/771),[\#744](https://github.com/Chatie/wechaty/pull/744),[\#727](https://github.com/Chatie/wechaty/pull/727),[\#714](https://github.com/Chatie/wechaty/pull/714)
1. @[JasLin](https://github.com/JasLin): [\#404](https://github.com/Chatie/wechaty/pull/404),[\#358](https://github.com/Chatie/wechaty/pull/358),[\#105](https://github.com/Chatie/wechaty/pull/105),[\#100](https://github.com/Chatie/wechaty/pull/100),[\#78](https://github.com/Chatie/wechaty/pull/78),[\#76](https://github.com/Chatie/wechaty/pull/76)
......@@ -30,6 +30,65 @@ WECHATY CONTRIBUTORS
# Change Log
## [Unreleased](https://github.com/chatie/wechaty/tree/HEAD)
[Full Changelog](https://github.com/chatie/wechaty/compare/v0.14.0...HEAD)
**Implemented enhancements:**
- Add unit test to puppet accessory [\#1219](https://github.com/Chatie/wechaty/issues/1219)
- Use browser implementation of Node.js' stream library [\#1201](https://github.com/Chatie/wechaty/issues/1201)
- feat: Add `for \(const contact of room\) {}` support by ES6 iterators override [\#1198](https://github.com/Chatie/wechaty/issues/1198)
- BREAKING CHANGE: v0.16 on\('friend`\) arguments changed! [\#1196](https://github.com/Chatie/wechaty/issues/1196)
- TypeScript Strict Mode: set `noImplicitAny` to `true` [\#1180](https://github.com/Chatie/wechaty/issues/1180)
- Generic for Return Child Class Type in Abstract Class Implementation [\#1178](https://github.com/Chatie/wechaty/issues/1178)
- BREAKING CHANGE: v0.16 Message.ext\(\) return '.ext' instead of 'ext' before [\#1168](https://github.com/Chatie/wechaty/issues/1168)
- Encapsulated `Contact`, `Messag`, `FriendRequest`, and `Room` into `PuppetWeb` [\#1166](https://github.com/Chatie/wechaty/issues/1166)
- BREAKING CHANGE: v0.16 will remove `MediaMessage` class [\#1164](https://github.com/Chatie/wechaty/issues/1164)
- BREAKING CHANGE: v0.16 will replace `Message.content\(\)` with `Message.text\(\)` [\#1163](https://github.com/Chatie/wechaty/issues/1163)
- Continious Deploy to NPM with @next tag when the MINOR version number is odd\(in developing branch\) [\#1158](https://github.com/Chatie/wechaty/issues/1158)
- BREAKING CHANGE: first arg of `room-leave` event licener changed from `Contact` to `Contact\[\]` [\#723](https://github.com/Chatie/wechaty/issues/723)
- Should throw Exception when there have API Error. [\#683](https://github.com/Chatie/wechaty/issues/683)
- \[todo\] allow Wechaty to be multi-instance [\#518](https://github.com/Chatie/wechaty/issues/518)
- Decouple: Make `Contact`, `Room`, `Message`, and `FriendRequest` class Abstract. [\#1160](https://github.com/Chatie/wechaty/pull/1160) ([zixia](https://github.com/zixia))
**Fixed bugs:**
- Update the peerDependencies of `rx-queue`: rxjs@6 from rxjs@5 [\#1205](https://github.com/Chatie/wechaty/issues/1205)
- Cannot send image message on v0.15.21 [\#1175](https://github.com/Chatie/wechaty/issues/1175)
- How to avoid the memory leak [\#981](https://github.com/Chatie/wechaty/issues/981)
- SyntaxError: Unexpected end of JSON input [\#846](https://github.com/Chatie/wechaty/issues/846)
- function `Message.mention\(\)` should recognize both magic code and blank [\#813](https://github.com/Chatie/wechaty/issues/813)
- BREAKING CHANGE: first arg of `room-leave` event licener changed from `Contact` to `Contact\\[\\]` [\#723](https://github.com/Chatie/wechaty/issues/723)
- Should throw Exception when there have API Error. [\#683](https://github.com/Chatie/wechaty/issues/683)
**Closed issues:**
- Create a websocket ipad demo [\#1228](https://github.com/Chatie/wechaty/issues/1228)
- Proper wechaty and its dependency installation [\#1225](https://github.com/Chatie/wechaty/issues/1225)
- can't run the typescript examples [\#1221](https://github.com/Chatie/wechaty/issues/1221)
- Scan QR Code not shown on terminal, wechaty@0.14.4 [\#1220](https://github.com/Chatie/wechaty/issues/1220)
- 请问怎么添加微信群中的人当做自己的好友呢 有例子可以参考吗 [\#1207](https://github.com/Chatie/wechaty/issues/1207)
- room-bot.ts error [\#1199](https://github.com/Chatie/wechaty/issues/1199)
- TypeScript 2.9 with trailing comma after rest parameters. [\#1188](https://github.com/Chatie/wechaty/issues/1188)
- code example 'media-file-bot' not working [\#1183](https://github.com/Chatie/wechaty/issues/1183)
- QrCode `scan` event not refresh on v0.15.21 \#1175 [\#1176](https://github.com/Chatie/wechaty/issues/1176)
- Version 10 of node.js has been released [\#1170](https://github.com/Chatie/wechaty/issues/1170)
- 自动加好友,加好友成功后,向对方发信息报错 [\#1165](https://github.com/Chatie/wechaty/issues/1165)
- Use `injection-js` for Wechaty v1.0 provide the resolvers of the Wechaty Puppet [\#1146](https://github.com/Chatie/wechaty/issues/1146)
- findAll ,WARN Room parse\(\) on a empty rawObj [\#1141](https://github.com/Chatie/wechaty/issues/1141)
- Rename all `find\(\)` method to `search\(\)` [\#1132](https://github.com/Chatie/wechaty/issues/1132)
- ERR PuppetWebBridge init\(\) exception: Error: connect ECONNREFUSED 127.0.0.1:35493 [\#1113](https://github.com/Chatie/wechaty/issues/1113)
- Feature request: sending file with a stream \(creating media message with a stream\) [\#1092](https://github.com/Chatie/wechaty/issues/1092)
- node\_modules/\_wechaty@0.13.36@wechaty/dist/src/config.d.ts\(1,24\): error TS2307: Cannot find module 'raven'. [\#1035](https://github.com/Chatie/wechaty/issues/1035)
**Merged pull requests:**
- chore\(package\): update ts-node to version 6.0.5 [\#1232](https://github.com/Chatie/wechaty/pull/1232) ([zixia](https://github.com/zixia))
- Update to node 10 in .travis.yml [\#1231](https://github.com/Chatie/wechaty/pull/1231) ([zixia](https://github.com/zixia))
- fix\(package\): update rx-queue to version 0.4.4 [\#1190](https://github.com/Chatie/wechaty/pull/1190) ([zixia](https://github.com/zixia))
- Multi-Instance Support [\#1159](https://github.com/Chatie/wechaty/pull/1159) ([zixia](https://github.com/zixia))
## [v0.14.0](https://github.com/chatie/wechaty/tree/v0.14.0) (2018-04-15)
[Full Changelog](https://github.com/chatie/wechaty/compare/v0.12.0...v0.14.0)
......@@ -119,7 +178,6 @@ WECHATY CONTRIBUTORS
- Click Web Wechat `Switch Account` Automatically to get qrcode immediately when bot logout [\#636](https://github.com/Chatie/wechaty/issues/636)
- Upgrade docker image from Node.js v7 to v8 [\#577](https://github.com/Chatie/wechaty/issues/577)
- \[todo\] switch unit test tool from AVA to TAPE [\#513](https://github.com/Chatie/wechaty/issues/513)
- \[ci\] Provide a Mock PuppetWeb Instance for Integration Test [\#237](https://github.com/Chatie/wechaty/issues/237)
**Fixed bugs:**
......
......@@ -16,9 +16,6 @@
* limitations under the License.
*
*/
import { createWriteStream } from 'fs'
/* tslint:disable:variable-name */
const QrcodeTerminal = require('qrcode-terminal')
......@@ -30,7 +27,8 @@ const QrcodeTerminal = require('qrcode-terminal')
import {
Wechaty,
log,
} from '../src/'
Contact,
} from '../src/'
const welcome = `
=============== Powered by Wechaty ===============
......@@ -62,7 +60,7 @@ bot
/**
* Main Contact Bot start from here
*/
onLogin()
main()
})
.on('logout' , user => log.info('Bot', `${user.name()} logouted`))
......@@ -85,7 +83,7 @@ bot.start()
/**
* Main Contact Bot
*/
async function onLogin() {
async function main() {
const contactList = await bot.Contact.findAll()
log.info('Bot', '#######################')
......@@ -96,29 +94,18 @@ async function onLogin() {
*/
for (let i = 0; i < contactList.length; i++) {
const contact = contactList[i]
if (contact.official()) {
if (contact.type() === Contact.Type.Official) {
log.info('Bot', `official ${i}: ${contact}`)
}
}
// /**
// * Special contact list
// */
// for (let i = 0; i < contactList.length; i++) {
// const contact = contactList[i]
// if (contact.special()) {
// log.info('Bot', `special ${i}: ${contact.name()}`)
// }
// }
/**
* personal contact list
*/
for (let i = 0; i < contactList.length; i++) {
const contact = contactList[i]
if (contact.personal()) {
if (contact.type() === Contact.Type.Personal) {
log.info('Bot', `personal ${i}: ${contact.name()} : ${contact.id}`)
}
}
......@@ -127,22 +114,17 @@ async function onLogin() {
for (let i = 0; i < contactList.length; i++ ) {
const contact = contactList[i]
if (!contact.weixin()) {
await contact.refresh()
}
/**
* Save avatar to file like: "1-name.jpg"
*/
const avatarFileName = `${i}-${contact.name()}.jpg`
const avatarReadStream = await contact.avatar()
const avatarWriteStream = createWriteStream(avatarFileName)
const wait = new Promise(r => avatarWriteStream.once('close', r))
avatarReadStream.pipe(avatarWriteStream)
await wait
const file = await contact.avatar()
const name = file.name
await file.save(name)
log.info('Bot', 'Contact: %s: %s with avatar file: %s', contact.weixin(), contact.name(), avatarFileName)
log.info('Bot', 'Contact: "%s" with avatar file: "%s"',
contact.name(),
name,
)
if (i > MAX) {
log.info('Bot', 'Contacts too many, I only show you the first %d ... ', MAX)
......@@ -150,10 +132,8 @@ async function onLogin() {
}
}
// const SLEEP = 7
// log.info('Bot', 'I will re-dump contact weixin id & names after %d second... ', SLEEP)
// setTimeout(main, SLEEP * 1000)
const SLEEP = 7
log.info('Bot', 'I will re-dump contact weixin id & names after %d second... ', SLEEP)
setTimeout(main, SLEEP * 1000)
await bot.logout()
await bot.stop()
}
......@@ -16,7 +16,8 @@
* limitations under the License.
*
*/
import * as path from 'path'
import * as fs from 'fs'
import * as path from 'path'
/* tslint:disable:variable-name */
import * as QrcodeTerminal from 'qrcode-terminal'
......@@ -33,7 +34,7 @@ import {
log,
} from '../src/'
const BOT_QR_CODE_IMAGE_FILE = path.join(
const BOT_QR_CODE_IMAGE_FILE = path.resolve(
__dirname,
'../docs/images/bot-qr-code.png',
)
......@@ -80,39 +81,39 @@ bot
})
}
})
.on('message', async m => {
.on('message', async msg => {
try {
const room = m.room()
console.log(
(room ? `${room}` : '')
+ `${m.from()}:${m}`,
)
console.log(msg.toString())
if (/^(ding|ping|bing|code)$/i.test(m.text()) && !m.self()) {
if (/^(ding|ping|bing|code)$/i.test(msg.text()) /*&& !msg.self()*/) {
/**
* 1. reply 'dong'
*/
log.info('Bot', 'REPLY: dong')
m.say('dong')
msg.say('dong')
const joinWechaty = `Join Wechaty Developers' Community\n\n` +
`Wechaty is used in many ChatBot projects by hundreds of developers.\n\n` +
`If you want to talk with other developers, just scan the following QR Code in WeChat with secret code: wechaty,\n\n` +
`you can join our Wechaty Developers' Home at once`
await m.say(joinWechaty)
await msg.say(joinWechaty)
/**
* 2. reply qrcode image
*/
const fileBox = FileBox.fromLocal(BOT_QR_CODE_IMAGE_FILE)
// const fileBox = FileBox.fromLocal(BOT_QR_CODE_IMAGE_FILE)
const fileBox = FileBox.fromStream(
fs.createReadStream(BOT_QR_CODE_IMAGE_FILE),
BOT_QR_CODE_IMAGE_FILE,
)
log.info('Bot', 'REPLY: %s', fileBox)
await m.say(fileBox)
await msg.say(fileBox)
/**
* 3. reply 'scan now!'
*/
await m.say('Scan now, because other Wechaty developers want to talk with you too!\n\n(secret code: wechaty)')
await msg.say('Scan now, because other Wechaty developers want to talk with you too!\n\n(secret code: wechaty)')
}
} catch (e) {
......
......@@ -95,7 +95,7 @@ bot
*/
case FriendRequest.Type.Receive:
if (request.hello() === 'ding') {
logMsg = 'accepted because verify messsage is "ding"'
logMsg = 'accepted automatically because verify messsage is "ding"'
request.accept()
} else {
......
......@@ -16,13 +16,7 @@
* limitations under the License.
*
*/
// import { inspect } from 'util'
import {
createWriteStream,
statSync,
// writeFileSync,
} from 'fs'
/* tslint:disable:variable-name */
import * as qrcodeTerminal from 'qrcode-terminal'
......@@ -35,7 +29,6 @@ import * as qrcodeTerminal from 'qrcode-terminal'
import {
config,
Message,
// MsgType,
Wechaty,
} from '../src/'
......@@ -53,50 +46,12 @@ bot
.on('message', msg => {
console.log(`RECV: ${msg}`)
// console.log(inspect(m))
// saveRawObj(m.rawObj)
// if ( m.type() === MsgType.IMAGE
// || m.type() === MsgType.EMOTICON
// || m.type() === MsgType.VIDEO
// || m.type() === MsgType.VOICE
// || m.type() === MsgType.MICROVIDEO
// || m.type() === MsgType.APP
// || (m.type() === MsgType.TEXT && m.typeSub() === MsgType.LOCATION) // LOCATION
// ) {
if (msg.type() !== Message.Type.Text) {
saveMediaFile(msg)
const file = msg.file()
const name = msg.file().name
file.save(name)
}
// }
})
.start()
.catch(e => console.error('bot.start() error: ' + e))
async function saveMediaFile(message: Message) {
const fileBox = message.file()
const filename = fileBox.name
console.log('IMAGE local filename: ' + filename)
if (!filename) {
throw new Error('no filename found for media file')
}
const fileStream = createWriteStream(filename)
console.log('start to readyStream()')
try {
fileBox
.pipe(fileStream)
.on('close', () => {
const stat = statSync(filename)
console.log('finish readyStream() for ', filename, ' size: ', stat.size)
})
} catch (e) {
console.error('stream error:', e)
}
}
// function saveRawObj(o) {
// writeFileSync('rawObj.log', JSON.stringify(o, null, ' ') + '\n\n\n', { flag: 'a' })
// }
{
"name": "wechaty",
"version": "0.15.42",
"version": "0.15.59",
"description": "Wechat for Bot(Personal Account)",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
......@@ -92,18 +92,19 @@
"node": ">= 8.5"
},
"dependencies": {
"bl": "^1.2.0",
"bl": "^2.0.0",
"brolog": "^1.2.0",
"clone-class": "^0.6.3",
"clone-class": "^0.6.11",
"cuid": "^2.1.1",
"file-box": "^0.2.1",
"express": "^4.16.3",
"file-box": "^0.4.1",
"hot-import": "^0.2.1",
"mime": "^2.2.0",
"normalize-package-data": "^2.4.0",
"puppeteer": "^1.2.0",
"raven": "^2.2.0",
"raven": "^2.6.2",
"read-pkg-up": "^3.0.0",
"request": "^2.83.0",
"request": "^2.87.0",
"retry-promise": "^1.0.0",
"rx-queue": "^0.4.7",
"rxjs": "^6.1.0",
......@@ -111,25 +112,25 @@
"vinyl": "^2.1.0",
"watchdog": "^0.8.1",
"wechat4u": "^0.7.6",
"ws": "^5.1.0",
"ws": "^5.2.0",
"xml2js": "^0.4.0"
},
"devDependencies": {
"@types/blessed": "^0.1.10",
"@types/blue-tape": "^0.1.0",
"@types/cuid": "^1.3.0",
"@types/express": "^4.0.0",
"@types/express": "^4.11.1",
"@types/fluent-ffmpeg": "^2.1.0",
"@types/glob": "^5.0.0p",
"@types/mime": "^2.0.0",
"@types/node": "^10.0.3",
"@types/node": "^10.1.2",
"@types/normalize-package-data": "^2.4.0",
"@types/puppeteer": "^1.0.0",
"@types/raven": "^2.1.0",
"@types/read-pkg-up": "^3.0.0",
"@types/request": "^2.0.0",
"@types/semver": "^5.5.0",
"@types/sinon": "^4.3.1",
"@types/sinon": "^5.0.0",
"@types/vinyl": "^2.0.2",
"@types/ws": "^5.1.0",
"@types/xml2js": "^0.4.0",
......@@ -143,15 +144,14 @@
"check-node-version": "^3.0.0",
"cookie-parser": "^1.4.0",
"coveralls": "^3.0.0",
"cross-env": "^5.0.0",
"cross-env": "^5.1.6",
"eslint": "^4.8.0",
"express": "^4.15.0",
"finis": "^0.4.1",
"fluent-ffmpeg": "^2.1.0",
"git-scripts": "git+https://github.com/nkzawa/git-scripts.git",
"glob": "^7.1.0",
"jsdoc-to-markdown": "^4.0.0",
"markdownlint-cli": "^0.8.1",
"markdownlint-cli": "^0.9.0",
"nyc": "^11.2.0",
"qrcode-terminal": "^0.12.0",
"semver": "^5.5.0",
......@@ -159,9 +159,9 @@
"sinon": "^5.0.2",
"sinon-test": "^2.1.2",
"sloc": "^0.2.0",
"ts-node": "^6.0.0",
"ts-node": "^6.0.5",
"tslint": "^5.9.1",
"tslint-eslint-rules": "^5.1.0",
"tslint-eslint-rules": "^5.3.0",
"tslint-jsdoc-rules": "^0.1.0",
"tuling123-client": "^0.0.1",
"typescript": "^2.9.0-dev.20180509"
......
......@@ -12,6 +12,7 @@ cp tests/fixtures/smoke-testing.ts "$TMPDIR"
cd $TMPDIR
npm init -y
npm install *-*.*.*.tgz \
@types/node \
rxjs \
brolog \
typescript@latest
......
......@@ -4,16 +4,21 @@ import * as test from 'blue-tape'
import { cloneClass } from 'clone-class'
import { Contact } from './contact'
import { Contact as GlobalContact } from './contact'
// tslint:disable-next-line:variable-name
const Contact = cloneClass(GlobalContact)
test('Should not be able to instanciate directly', async t => {
// tslint:disable-next-line:variable-name
const MyContact = cloneClass(Contact)
t.throws(() => {
const c = new Contact('xxx')
const c = MyContact.load('xxx')
t.fail(c.name())
}, 'should throw when `new Contact()`')
}, 'should throw when `Contact.load()`')
t.throws(() => {
const c = Contact.load('xxx')
const c = MyContact.load('xxx')
t.fail(c.name())
}, 'should throw when `Contact.load()`')
})
......@@ -23,9 +28,9 @@ test('Should not be able to instanciate through cloneClass without puppet', asyn
const MyContact = cloneClass(Contact)
t.throws(() => {
const c = new MyContact('xxx')
const c = MyContact.load('xxx')
t.fail(c.name())
}, 'should throw when `new MyContact()` without puppet')
}, 'should throw when `MyContact.load()` without puppet')
t.throws(() => {
const c = MyContact.load('xxx')
......@@ -34,15 +39,15 @@ test('Should not be able to instanciate through cloneClass without puppet', asyn
})
test('Should be able to instanciate through cloneClass with puppet', async t => {
test('should be able to instanciate through cloneClass with puppet', async t => {
// tslint:disable-next-line:variable-name
const MyContact = cloneClass(Contact)
MyContact.puppet = {} as any
t.doesNotThrow(() => {
const c = new MyContact('xxx')
t.ok(c, 'should get contact instance from `new MyContact()')
}, 'should not throw when `new MyContact()`')
const c = MyContact.load('xxx')
t.ok(c, 'should get contact instance from `MyContact.load()')
}, 'should not throw when `MyContact().load`')
t.doesNotThrow(() => {
const c = MyContact.load('xxx')
......@@ -50,3 +55,11 @@ test('Should be able to instanciate through cloneClass with puppet', async t =>
}, 'should not throw when `MyContact.load()`')
})
test('should throw when instanciate the global class', async t => {
t.throws(() => {
const c = GlobalContact.load('xxx')
t.fail('should not run to here')
t.fail(c.toString())
}, 'should throw when we instanciate a global class')
})
......@@ -25,9 +25,9 @@ import {
Raven,
Sayable,
} from './config'
import PuppetAccessory from './puppet-accessory'
import { PuppetAccessory } from './puppet-accessory'
import Message from './message'
// import Message from './message'
/**
* Enum for Gender values.
......@@ -70,6 +70,8 @@ export interface ContactPayload {
weixin?: string,
}
export const POOL = Symbol('pool')
/**
* All wechat contacts(friend) will be encapsulated as a Contact.
*
......@@ -79,20 +81,44 @@ export interface ContactPayload {
export class Contact extends PuppetAccessory implements Sayable {
// tslint:disable-next-line:variable-name
public static Type = ContactType
public static Gender = Gender
public static Type = ContactType
public static Gender = Gender
protected static readonly pool = new Map<string, Contact>()
protected static [POOL]: Map<string, Contact>
protected static get pool() {
return this[POOL]
}
protected static set pool(newPool: Map<string, Contact>) {
if (this === Contact) {
throw new Error(
'The global Contact class can not be used directly!'
+ 'See: https://github.com/Chatie/wechaty/issues/1217',
)
}
this[POOL] = newPool
}
/**
* @private
* About the Generic: https://stackoverflow.com/q/43003970/1123955
*/
public static load<T extends typeof Contact>(this: T, id: string): T['prototype'] {
if (!id || typeof id !== 'string') {
throw new Error('Contact.load(): id not found: ' + id)
public static load<T extends typeof Contact>(
this : T,
id : string,
): T['prototype'] {
if (!this.pool) {
log.verbose('Contact', 'load(%s) init pool', id)
this.pool = new Map<string, Contact>()
}
if (this === Contact) {
throw new Error(
'The lgobal Contact class can not be used directly!'
+ 'See: https://github.com/Chatie/wechaty/issues/1217',
)
}
if (this.pool === Contact.pool) {
throw new Error('the current pool is equal to the global pool error!')
}
const existingContact = this.pool.get(id)
if (existingContact) {
return existingContact
......@@ -101,7 +127,7 @@ export class Contact extends PuppetAccessory implements Sayable {
// when we call `load()`, `this` should already be extend-ed a child class.
// so we force `this as any` at here to make the call.
const newContact = new (this as any)(id)
Contact.pool.set(id, newContact)
this.pool.set(id, newContact)
return newContact
}
......@@ -167,7 +193,7 @@ export class Contact extends PuppetAccessory implements Sayable {
// log.verbose('Cotnact', 'findAll({ name: %s })', query.name)
log.verbose('Cotnact', 'findAll({ %s })',
Object.keys(query)
.map((k: keyof ContactQueryFilter) => `${k}: ${query[k]}`)
.map(k => `${k}: ${query[k as keyof ContactQueryFilter]}`)
.join(', '),
)
......@@ -176,7 +202,8 @@ export class Contact extends PuppetAccessory implements Sayable {
}
try {
const contactList: Contact[] = await this.puppet.contactFindAll(query)
const contactIdList: string[] = await this.puppet.contactFindAll(query)
const contactList = contactIdList.map(id => this.load(id))
await Promise.all(contactList.map(c => c.ready()))
return contactList
......@@ -188,7 +215,9 @@ export class Contact extends PuppetAccessory implements Sayable {
}
/**
*
* Instance properties
*
*/
protected payload?: ContactPayload
......@@ -205,7 +234,10 @@ export class Contact extends PuppetAccessory implements Sayable {
const MyClass = instanceToClass(this, Contact)
if (MyClass === Contact) {
throw new Error('Contact class can not be instanciated directly! See: https://github.com/Chatie/wechaty/issues/1217')
throw new Error(
'Contact class can not be instanciated directly!'
+ 'See: https://github.com/Chatie/wechaty/issues/1217',
)
}
if (!this.puppet) {
......@@ -217,8 +249,11 @@ export class Contact extends PuppetAccessory implements Sayable {
* @private
*/
public toString(): string {
if (!this.payload) {
return this.constructor.name
}
const identity = this.alias() || this.name() || this.id
return `Contact<${identity}>`
return `Contact<${identity || 'Unknown'}>`
}
/**
......@@ -252,27 +287,33 @@ export class Contact extends PuppetAccessory implements Sayable {
public async say(textOrFile: string | FileBox): Promise<void> {
log.verbose('Contact', 'say(%s)', textOrFile)
let msg: Message
// let msg: Message
if (typeof textOrFile === 'string') {
msg = Message.createMO({
text : textOrFile,
to : this,
})
await this.puppet.messageSendText({
contactId: this.id,
}, textOrFile)
// msg = Message.createMO({
// text : textOrFile,
// to : this,
// })
} else if (textOrFile instanceof FileBox) {
msg = Message.createMO({
to : this,
file : textOrFile,
})
await this.puppet.messageSendFile({
contactId: this.id,
}, textOrFile)
// msg = Message.createMO({
// to : this,
// file : textOrFile,
// })
} else {
throw new Error('unsupported')
}
log.silly('Contact', 'say() from: %s to: %s content: %s',
this.puppet.userSelf(),
this,
msg,
)
await this.puppet.messageSend(msg)
// log.silly('Contact', 'say() from: %s to: %s content: %s',
// this.puppet.userSelf(),
// this,
// msg,
// )
// await this.puppet.messageSend(msg)
}
/**
......@@ -332,7 +373,7 @@ export class Contact extends PuppetAccessory implements Sayable {
return this.payload && this.payload.alias || null
}
const future = this.puppet.contactAlias(this, newAlias)
const future = this.puppet.contactAlias(this.id, newAlias)
future
.then(() => this.payload!.alias = newAlias)
......@@ -481,7 +522,7 @@ export class Contact extends PuppetAccessory implements Sayable {
public async avatar(): Promise<FileBox> {
log.verbose('Contact', 'avatar()')
return this.puppet.contactAvatar(this)
return this.puppet.contactAvatar(this.id)
}
/**
......@@ -518,7 +559,7 @@ export class Contact extends PuppetAccessory implements Sayable {
* @private
*/
public async ready(): Promise<void> {
log.silly('Contact', 'ready()')
log.silly('Contact', 'ready() @ %s', this.puppet)
if (this.isReady()) { // already ready
log.silly('Contact', 'ready() isReady() true')
......
......@@ -39,10 +39,10 @@ export enum FriendRequestType {
}
export interface FriendRequestPayload {
contact? : Contact,
hello? : string,
ticket? : string
type? : FriendRequestType,
contactId? : string,
hello? : string,
ticket? : string
type? : FriendRequestType,
}
/**
......@@ -69,8 +69,8 @@ export class FriendRequest extends PuppetAccessory {
)
const sentRequest = new this({
type: FriendRequestType.Send,
contact,
type : FriendRequestType.Send,
contactId : contact.id,
hello,
})
......@@ -85,8 +85,8 @@ export class FriendRequest extends PuppetAccessory {
)
const confirmedRequest = new this({
type: FriendRequestType.Confirm,
contact,
type : FriendRequestType.Confirm,
contactId : contact.id,
})
return confirmedRequest
......@@ -104,8 +104,8 @@ export class FriendRequest extends PuppetAccessory {
)
const receivedRequest = new this({
type: FriendRequestType.Receive,
contact,
type : FriendRequestType.Receive,
contactId : contact.id,
hello,
ticket,
})
......@@ -139,15 +139,15 @@ export class FriendRequest extends PuppetAccessory {
public async send(): Promise<void> {
if (!this.payload) {
throw new Error('no payload')
} else if (!this.payload.contact) {
} else if (!this.payload.contactId) {
throw new Error('no contact')
} else if (this.payload.type !== FriendRequest.Type.Send) {
throw new Error('not a send request')
}
log.verbose('PuppeteerFriendRequest', 'send() to %s', this.payload.contact)
log.verbose('PuppeteerFriendRequest', 'send() to %s', this.payload.contactId)
await this.puppet.friendRequestSend(
this.payload.contact,
this.payload.contactId,
this.payload.hello,
)
}
......@@ -155,16 +155,16 @@ export class FriendRequest extends PuppetAccessory {
public async accept(): Promise<void> {
if (!this.payload) {
throw new Error('no payload')
} else if (!this.payload.contact) {
throw new Error('no contact')
} else if (!this.payload.contactId) {
throw new Error('no contactId')
} else if (!this.payload.ticket) {
throw new Error('no ticket')
} else if (this.payload.type !== FriendRequest.Type.Receive) {
throw new Error('not a receive request, its a ' + FriendRequest.Type[this.payload.type!])
}
log.verbose('FriendRequest', 'accept() to %s', this.payload.contact)
log.verbose('FriendRequest', 'accept() to %s', this.payload.contactId)
await this.puppet.friendRequestAccept(this.payload.contact, this.payload.ticket)
await this.puppet.friendRequestAccept(this.payload.contactId, this.payload.ticket)
const max = 20
const backoff = 300
......@@ -202,10 +202,12 @@ export class FriendRequest extends PuppetAccessory {
public contact(): Contact {
if (!this.payload) {
throw new Error('no payload')
} else if (!this.payload.contact) {
} else if (!this.payload.contactId) {
throw new Error('no contact')
}
return this.payload.contact
const contact = this.puppet.Contact.load(this.payload.contactId)
return contact
}
public async reject(): Promise<void> {
......
export {
FileBox,
} from 'file-box'
export {
config,
log,
......
......@@ -68,14 +68,14 @@ export class Io {
private readonly cuid : string
private readonly protocol : string
private eventBuffer : IoEvent[] = []
private ws : WebSocket
private ws : undefined | WebSocket
private readonly state = new StateSwitch('Io', log)
private reconnectTimer? : NodeJS.Timer
private reconnectTimeout? : number
private onMessage: Function
private onMessage: undefined | Function
private scanData: ScanData
......@@ -392,6 +392,12 @@ export class Io {
}
private async send(ioEvent?: IoEvent): Promise<void> {
if (!this.ws) {
throw new Error('no ws')
}
const ws = this.ws
if (ioEvent) {
log.silly('Io', 'send(%s: %s)', ioEvent.name, ioEvent.payload)
this.eventBuffer.push(ioEvent)
......@@ -404,7 +410,7 @@ export class Io {
const list: Promise<any>[] = []
while (this.eventBuffer.length) {
const p = new Promise((resolve, reject) => this.ws.send(
const p = new Promise((resolve, reject) => ws.send(
JSON.stringify(
this.eventBuffer.shift(),
),
......@@ -425,6 +431,10 @@ export class Io {
}
public async quit(): Promise<void> {
if (!this.ws) {
throw new Error('no ws')
}
this.state.off('pending')
// try to send IoEvents in buffer
......
......@@ -16,8 +16,8 @@
* limitations under the License.
* @ignore
*/
import * as path from 'path'
import * as cuid from 'cuid'
// import * as path from 'path'
// import * as cuid from 'cuid'
import {
FileBox,
......@@ -37,9 +37,9 @@ import Room from './room'
import {
MessageDirection,
MessageMOOptions,
MessageMOOptionsText,
MessageMOOptionsFile,
// MessageMOOptions,
// MessageMOOptionsText,
// MessageMOOptionsFile,
MessagePayload,
MessageType,
} from './message.type'
......@@ -91,86 +91,95 @@ export class Message extends PuppetAccessory implements Sayable {
* "mobile originated" or "mobile terminated"
* https://www.tatango.com/resources/video-lessons/video-mo-mt-sms-messaging/
*/
public static createMO(
options: MessageMOOptions,
): Message {
log.verbose('Message', 'static createMobileOriginated()')
/**
* Must NOT use `Message` at here
* MUST use `this` at here
*
* because the class will be `cloneClass`-ed
*/
const msg = new this(cuid())
const direction = MessageDirection.MO
const to = options.to
const room = options.room
const text = (options as MessageMOOptionsText).text
const date = new Date()
const file = (options as MessageMOOptionsFile).file
if (text) {
/**
* 1. Text
*/
msg.payload = {
type: MessageType.Text,
direction,
to,
room,
text,
date,
}
} else if (file) {
/**
* 2. File
*/
let type: MessageType
const ext = path.extname(file.name)
switch (ext.toLowerCase()) {
case '.bmp':
case '.jpg':
case '.jpeg':
case '.png':
case '.gif': // type = WebMsgType.EMOTICON
type = MessageType.Image
break
// private static createMO(
// options: MessageMOOptions,
// ): Message {
// log.verbose('Message', 'static createMobileOriginated()')
// /**
// * Must NOT use `Message` at here
// * MUST use `this` at here
// *
// * because the class will be `cloneClass`-ed
// */
// const msg = new this(cuid())
// const direction = MessageDirection.MO
// const to = options.to
// const room = options.room
// const text = (options as MessageMOOptionsText).text
// const date = new Date()
// const file = (options as MessageMOOptionsFile).file
// const roomId = room && room.id
// const toId = to && to.id
// if (text) {
// /**
// * 1. Text
// */
// msg.payload = {
// type: MessageType.Text,
// direction,
// toId,
// roomId,
// text,
// date,
// }
// } else if (file) {
// /**
// * 2. File
// */
// let type: MessageType
// const ext = path.extname(file.name)
// switch (ext.toLowerCase()) {
// case '.bmp':
// case '.jpg':
// case '.jpeg':
// case '.png':
// case '.gif': // type = WebMsgType.EMOTICON
// type = MessageType.Image
// break
case '.mp4':
type = MessageType.Video
break
// case '.mp4':
// type = MessageType.Video
// break
case '.mp3':
type = MessageType.Audio
break
// case '.mp3':
// type = MessageType.Audio
// break
default:
throw new Error('unknown ext:' + ext)
}
// default:
// throw new Error('unknown ext:' + ext)
// }
msg.payload = {
type,
direction,
to,
room,
file,
date,
}
} else {
throw new Error('neither text nor file!?')
}
// msg.payload = {
// type,
// direction,
// toId,
// roomId,
// file,
// date,
// }
// } else {
// throw new Error('neither text nor file!?')
// }
return msg
}
// return msg
// }
/**
* "mobile originated" or "mobile terminated"
* https://www.tatango.com/resources/video-lessons/video-mo-mt-sms-messaging/
*/
public static createMT(
id: string,
id : string,
payload? : MessagePayload,
): Message {
log.verbose('Message', 'static createMobileTerminated(%s)',
log.verbose('Message', 'static createMobileTerminated(%s, %s)',
id,
payload ? payload : '',
)
/**
* Must NOT use `Message` at here
......@@ -181,11 +190,25 @@ export class Message extends PuppetAccessory implements Sayable {
const msg = new this(id)
msg.direction = MessageDirection.MT
if (payload) {
msg.payload = payload
}
return msg
}
public static create(options: MessageMOOptions): Message {
return this.createMO(options)
/**
* @alias createMT
* Create a Mobile Terminated Message
*/
public static create(
id : string,
payload? : MessagePayload,
): Message {
return this.createMT(
id,
payload,
)
}
/**
......@@ -227,6 +250,10 @@ export class Message extends PuppetAccessory implements Sayable {
* @private
*/
public toString() {
if (!this.isReady()) {
return this.constructor.name
}
const msgStrList = [
'Message',
`#${MessageDirection[this.direction]}`,
......@@ -235,6 +262,8 @@ export class Message extends PuppetAccessory implements Sayable {
if (this.type() === Message.Type.Text) {
msgStrList.push(`<${this.text()}>`)
} else {
log.verbose('Message', 'toString() this.type()=%s', Message.Type[this.type()])
if (!this.payload) {
throw new Error('no payload')
}
......@@ -271,11 +300,12 @@ export class Message extends PuppetAccessory implements Sayable {
// return
// }
const from = this.payload.from
if (!from) {
const fromId = this.payload.fromId
if (!fromId) {
throw new Error('no from')
}
const from = this.puppet.Contact.load(fromId)
return from
}
......@@ -303,7 +333,13 @@ export class Message extends PuppetAccessory implements Sayable {
// return
// }
return this.payload.to || null
const toId = this.payload.toId
if (!toId) {
return null
}
const to = this.puppet.Contact.load(toId)
return to
}
// /**
......@@ -330,8 +366,13 @@ export class Message extends PuppetAccessory implements Sayable {
// this.payload.room = room
// return
// }
const roomId = this.payload.roomId
if (!roomId) {
return null
}
return this.payload.room || null
const room = this.puppet.Room.load(roomId)
return room
}
// /**
......@@ -401,7 +442,10 @@ export class Message extends PuppetAccessory implements Sayable {
textOrFile : string | FileBox,
mention? : Contact | Contact[],
): Promise<void> {
log.verbose('Message', 'say(%s, %s)', textOrFile, mention)
log.verbose('Message', 'say(%s, %s)',
textOrFile.toString(),
mention,
)
// const user = this.puppet.userSelf()
const from = this.from()
......@@ -420,12 +464,10 @@ export class Message extends PuppetAccessory implements Sayable {
/**
* File Message
*/
const msg = this.puppet.Message.createMO({
file : textOrFile,
to : from,
room,
})
await this.puppet.messageSend(msg)
await this.puppet.messageSendFile({
roomId : room && room.id || undefined,
contactId : from.id,
}, textOrFile)
}
}
......@@ -435,43 +477,25 @@ export class Message extends PuppetAccessory implements Sayable {
room : Room | null,
mentionList : Contact[],
): Promise<void> {
let msg: Message
if (!room) {
if (room && mentionList.length > 0) {
/**
* 1. to Individual
* 1 had mentioned someone
*/
msg = this.puppet.Message.createMO({
to,
text,
})
const mentionContact = mentionList[0]
const textMentionList = mentionList.map(c => '@' + c.name()).join(' ')
await this.puppet.messageSendText({
contactId: mentionContact.id,
roomId: room.id,
}, textMentionList + ' ' + text)
} else {
/**
* 2. in Room
* 2 did not mention anyone
*/
if (mentionList.length > 0) {
/**
* 2.1 had mentioned someone
*/
const mentionContact = mentionList[0]
const textMentionList = mentionList.map(c => '@' + c.name()).join(' ')
msg = this.puppet.Message.createMO({
to: mentionContact,
room,
text: textMentionList + ' ' + text,
})
} else {
/**
* 2.2 did not mention anyone
*/
msg = this.puppet.Message.createMO({
to,
room,
text,
})
}
await this.puppet.messageSendText({
contactId : to.id,
roomId : room && room.id || undefined,
}, text)
}
await this.puppet.messageSend(msg)
}
public file(): FileBox {
......@@ -814,8 +838,22 @@ export class Message extends PuppetAccessory implements Sayable {
public async forward(to: Room | Contact): Promise<void> {
log.verbose('Message', 'forward(%s)', to)
let roomId, contactId
if (to instanceof Room) {
roomId = to.id
}
if (to instanceof Contact) {
contactId = to.id
}
try {
await this.puppet.messageForward(this, to)
await this.puppet.messageForward(
{
contactId,
roomId,
},
this.id,
)
} catch (e) {
log.error('Message', 'forward(%s) exception: %s', to, e)
throw e
......
......@@ -10,7 +10,7 @@ import {
} from './room'
export enum MessageDirection {
MO,
// MO,
MT,
}
......@@ -18,6 +18,7 @@ export enum MessageType {
Unknown = 0,
Attachment,
Audio,
Emoticon,
Image,
Text,
Video,
......@@ -65,8 +66,8 @@ export interface MessagePayload {
text? : string,
file? : FileBox,
direction : MessageDirection,
from? : Contact,
fromId? : string,
date : Date,
to? : null | Contact, // if to is not set, then room must be set
room? : null | Room,
toId? : null | string, // if to is not set, then room must be set
roomId? : null | string,
}
......@@ -25,14 +25,15 @@ import {
} from './config'
export interface ProfileSchema {
cookies?: any[]
cookies? : any[]
[idx: string] : any
}
export type ProfileSection = keyof ProfileSchema
export class Profile {
private obj : ProfileSchema
private file? : string
private payload : ProfileSchema
private file? : string
constructor(
public name = config.profile,
......@@ -44,7 +45,7 @@ export class Profile {
} else {
this.file = path.isAbsolute(name)
? name
: path.join(
: path.resolve(
process.cwd(),
name,
)
......@@ -52,6 +53,8 @@ export class Profile {
this.file += '.wechaty.json'
}
}
this.payload = {}
}
public toString() {
......@@ -60,7 +63,7 @@ export class Profile {
public async load(): Promise<void> {
log.verbose('Profile', 'load() file: %s', this.file)
this.obj = {}
this.payload = {}
if (!this.file) {
log.verbose('Profile', 'load() no file, NOOP')
......@@ -74,7 +77,7 @@ export class Profile {
const text = fs.readFileSync(this.file).toString()
try {
this.obj = JSON.parse(text)
this.payload = JSON.parse(text)
} catch (e) {
log.error('Profile', 'load() exception: %s', e)
}
......@@ -86,13 +89,13 @@ export class Profile {
log.verbose('Profile', 'save() no file, NOOP')
return
}
if (!this.obj) {
if (!this.payload) {
log.verbose('Profile', 'save() no obj, NOOP')
return
}
try {
const text = JSON.stringify(this.obj)
const text = JSON.stringify(this.payload)
fs.writeFileSync(this.file, text)
} catch (e) {
log.error('Profile', 'save() exception: %s', e)
......@@ -102,23 +105,23 @@ export class Profile {
public async get<T = any>(section: ProfileSection): Promise<null | T> {
log.verbose('Profile', 'get(%s)', section)
if (!this.obj) {
if (!this.payload) {
return null
}
return this.obj[section] as any as T
return this.payload[section] as any as T
}
public async set(section: ProfileSection, data: any): Promise<void> {
log.verbose('Profile', 'set(%s, %s)', section, data)
if (!this.obj) {
this.obj = {}
if (!this.payload) {
this.payload = {}
}
this.obj[section] = data
this.payload[section] = data
}
public async destroy(): Promise<void> {
log.verbose('Profile', 'destroy() file: %s', this.file)
this.obj = {}
this.payload = {}
if (this.file && fs.existsSync(this.file)) {
fs.unlinkSync(this.file)
this.file = undefined
......
......@@ -9,14 +9,17 @@ import { Puppet } from './puppet/'
// use Symbol to prevent conflicting with the child class properties
// This symbol must be exported (for now).
// See: https://github.com/Microsoft/TypeScript/issues/20080
export const NAME = Symbol('name')
export const SYMBOL_NAME = Symbol('name')
export const SYMBOL_COUNTER = Symbol('counter')
let COUNTER = 0
export abstract class PuppetAccessory extends EventEmitter {
// Not work???
// private static readonly PUPPET_ACCESSORY_NAME = Symbol('name')
private static [NAME]: string
private [NAME]: string
private [SYMBOL_NAME] : string
private [SYMBOL_COUNTER] : number
/**
*
......@@ -27,15 +30,15 @@ export abstract class PuppetAccessory extends EventEmitter {
public static set puppet(puppet: Puppet) {
log.silly('PuppetAccessory', '<%s> static set puppet(%s)',
this[NAME] || this.name,
puppet.constructor.name,
this.name,
puppet,
)
this._puppet = puppet
}
public static get puppet(): Puppet {
log.silly('PuppetAccessory', '<%s> static get puppet()',
this[NAME] || this.name,
this.name,
)
if (this._puppet) {
......@@ -43,7 +46,7 @@ export abstract class PuppetAccessory extends EventEmitter {
}
throw new Error('static puppet not found for '
+ this[NAME] || this.name,
+ this.name,
)
}
......@@ -56,15 +59,16 @@ export abstract class PuppetAccessory extends EventEmitter {
public set puppet(puppet: Puppet) {
log.silly('PuppetAccessory', '<%s> set puppet(%s)',
this[NAME],
puppet.constructor.name,
this[SYMBOL_NAME] || this,
puppet,
)
this._puppet = puppet
}
public get puppet(): Puppet {
log.silly('PuppetAccessory', '<%s> get puppet()',
this[NAME],
log.silly('PuppetAccessory', '#%d<%s> get puppet()',
this[SYMBOL_COUNTER],
this[SYMBOL_NAME] || this,
)
if (this._puppet) {
......@@ -84,10 +88,12 @@ export abstract class PuppetAccessory extends EventEmitter {
) {
super()
this[NAME] = name || this.constructor.name
this[SYMBOL_NAME] = name || this.toString()
this[SYMBOL_COUNTER] = COUNTER++
log.silly('PuppetAccessory', '<%s> constructor(%s)',
this[NAME],
log.silly('PuppetAccessory', '#%d<%s> constructor(%s)',
this[SYMBOL_COUNTER],
this[SYMBOL_NAME],
name || '',
)
}
......
......@@ -23,18 +23,18 @@ import {
} from 'file-box'
import {
Message,
// Message,
MessagePayload,
} from '../message'
import {
Contact,
// Contact,
ContactQueryFilter,
Gender,
ContactType,
ContactPayload,
} from '../contact'
import {
Room,
// Room,
RoomPayload,
RoomQueryFilter,
} from '../room'
......@@ -44,6 +44,7 @@ import {
import {
Puppet,
PuppetOptions,
Receiver,
} from '../puppet/'
import {
......@@ -77,10 +78,6 @@ export class PuppetMock extends Puppet {
super(options)
}
public toString() {
return `PuppetMock<${this.options.profile.name}>`
}
public ding(data?: any): Promise<string> {
return data
}
......@@ -92,14 +89,15 @@ export class PuppetMock extends Puppet {
// await some tasks...
this.state.on(true)
const user = this.Contact.load('logined_user_id')
const msg = this.Message.createMT('mock_id')
this.user = user
this.userId = 'logined_user_id'
const user = this.Contact.load(this.userId)
this.emit('login', user)
const msg = this.Message.createMT('mock_id')
await msg.ready()
setInterval(() => {
log.verbose('PuppetMock', `start() setInterval() pretending received a new message: ${msg}`)
log.verbose('PuppetMock', `start() setInterval() pretending received a new message: ${msg + ''}`)
this.emit('message', msg)
}, 3000)
......@@ -126,8 +124,8 @@ export class PuppetMock extends Puppet {
throw new Error('logout before login?')
}
this.emit('logout', this.user!) // becore we will throw above by logonoff() when this.user===undefined
this.user = undefined
this.emit('logout', this.userId!) // becore we will throw above by logonoff() when this.user===undefined
this.userId = undefined
// TODO: do the logout job
}
......@@ -137,11 +135,11 @@ export class PuppetMock extends Puppet {
* Contact
*
*/
public contactAlias(contact: Contact) : Promise<string>
public contactAlias(contact: Contact, alias: string | null): Promise<void>
public contactAlias(contactId: string) : Promise<string>
public contactAlias(contactId: string, alias: string | null): Promise<void>
public async contactAlias(contact: Contact, alias?: string|null): Promise<void | string> {
log.verbose('PuppetMock', 'contactAlias(%s, %s)', contact, alias)
public async contactAlias(contactId: string, alias?: string|null): Promise<void | string> {
log.verbose('PuppetMock', 'contactAlias(%s, %s)', contactId, alias)
if (typeof alias === 'undefined') {
return 'mock alias'
......@@ -149,14 +147,14 @@ export class PuppetMock extends Puppet {
return
}
public async contactFindAll(query: ContactQueryFilter): Promise<Contact[]> {
public async contactFindAll(query: ContactQueryFilter): Promise<string[]> {
log.verbose('PuppetMock', 'contactFindAll(%s)', query)
return []
}
public async contactAvatar(contact: Contact): Promise<FileBox> {
log.verbose('PuppetMock', 'contactAvatar(%s)', contact)
public async contactAvatar(contactId: string): Promise<FileBox> {
log.verbose('PuppetMock', 'contactAvatar(%s)', contactId)
const WECHATY_ICON_PNG = path.resolve('../../docs/images/wechaty-icon.png')
return FileBox.fromLocal(WECHATY_ICON_PNG)
......@@ -200,22 +198,35 @@ export class PuppetMock extends Puppet {
const payload: MessagePayload = {
date : new Date(),
direction : this.Message.Direction.MT,
from : this.Contact.load('xxx'),
fromId : 'xxx',
text : 'mock message text',
to : this.userSelf(),
toId : this.userSelf().id,
type : this.Message.Type.Text,
}
return payload
}
public async messageSend(message: Message): Promise<void> {
log.verbose('PuppetMock', 'messageSend(%s)', message)
public async messageSendText(
receiver : Receiver,
text : string,
): Promise<void> {
log.verbose('PuppetMock', 'messageSend(%s, %s)', receiver, text)
}
public async messageSendFile(
receiver : Receiver,
file : FileBox,
): Promise<void> {
log.verbose('PuppetMock', 'messageSend(%s, %s)', receiver, file)
}
public async messageForward(message: Message, to: Contact | Room): Promise<void> {
public async messageForward(
receiver : Receiver,
messageId : string,
): Promise<void> {
log.verbose('PuppetMock', 'messageForward(%s, %s)',
message,
to,
receiver,
messageId,
)
}
......@@ -224,7 +235,9 @@ export class PuppetMock extends Puppet {
* Room
*
*/
public async roomRawPayload(id: string): Promise<MockRoomRawPayload> {
public async roomRawPayload(
id: string,
): Promise<MockRoomRawPayload> {
log.verbose('PuppetMock', 'roomRawPayload(%s)', id)
const rawPayload: MockRoomRawPayload = {
......@@ -235,12 +248,14 @@ export class PuppetMock extends Puppet {
return rawPayload
}
public async roomRawPayloadParser(rawPayload: MockRoomRawPayload): Promise<RoomPayload> {
public async roomRawPayloadParser(
rawPayload: MockRoomRawPayload,
): Promise<RoomPayload> {
log.verbose('PuppetMock', 'roomRawPayloadParser(%s)', rawPayload)
const payload: RoomPayload = {
topic : 'mock topic',
memberList : [],
memberIdList : [],
nameMap : {} as any,
roomAliasMap : {} as any,
contactAliasMap: {} as any,
......@@ -251,28 +266,31 @@ export class PuppetMock extends Puppet {
public async roomFindAll(
query: RoomQueryFilter = { topic: /.*/ },
): Promise<Room[]> {
): Promise<string[]> {
log.verbose('PuppetMock', 'roomFindAll(%s)', query)
return []
}
public async roomDel(
room : Room,
contact : Contact,
roomId : string,
contactId : string,
): Promise<void> {
log.verbose('PuppetMock', 'roomDel(%s, %s)', room, contact)
log.verbose('PuppetMock', 'roomDel(%s, %s)', roomId, contactId)
}
public async roomAdd(
room : Room,
contact : Contact,
roomId : string,
contactId : string,
): Promise<void> {
log.verbose('PuppetMock', 'roomAdd(%s, %s)', room, contact)
log.verbose('PuppetMock', 'roomAdd(%s, %s)', roomId, contactId)
}
public async roomTopic(room: Room, topic?: string): Promise<void | string> {
log.verbose('PuppetMock', 'roomTopic(%s, %s)', room, topic)
public async roomTopic(
roomId: string,
topic?: string,
): Promise<void | string> {
log.verbose('PuppetMock', 'roomTopic(%s, %s)', roomId, topic)
if (typeof topic === 'undefined') {
return 'mock room topic'
......@@ -280,18 +298,17 @@ export class PuppetMock extends Puppet {
return
}
public async roomCreate(contactList: Contact[], topic: string): Promise<Room> {
log.verbose('PuppetMock', 'roomCreate(%s, %s)', contactList, topic)
public async roomCreate(
contactIdList : string[],
topic : string,
): Promise<string> {
log.verbose('PuppetMock', 'roomCreate(%s, %s)', contactIdList, topic)
if (!contactList || ! contactList.map) {
throw new Error('contactList not found')
}
const r = this.Room.load('mock room id') as Room
return r
return 'mock_room_id'
}
public async roomQuit(room: Room): Promise<void> {
log.verbose('PuppetMock', 'roomQuit(%s)', room)
public async roomQuit(roomId: string): Promise<void> {
log.verbose('PuppetMock', 'roomQuit(%s)', roomId)
}
/**
......@@ -300,12 +317,18 @@ export class PuppetMock extends Puppet {
* FriendRequest
*
*/
public async friendRequestSend(contact: Contact, hello: string): Promise<void> {
log.verbose('PuppetMock', 'friendRequestSend(%s, %s)', contact, hello)
public async friendRequestSend(
contactId : string,
hello : string,
): Promise<void> {
log.verbose('PuppetMock', 'friendRequestSend(%s, %s)', contactId, hello)
}
public async friendRequestAccept(contact: Contact, ticket: string): Promise<void> {
log.verbose('PuppetMock', 'friendRequestAccept(%s, %s)', contact, ticket)
public async friendRequestAccept(
contactId : string,
ticket : string,
): Promise<void> {
log.verbose('PuppetMock', 'friendRequestAccept(%s, %s)', contactId, ticket)
}
}
......
......@@ -57,8 +57,8 @@ export interface BridgeOptions {
}
export class Bridge extends EventEmitter {
private browser : Browser
private page : Page
private browser : undefined | Browser
private page : undefined | Page
private state : StateSwitch
constructor(
......@@ -276,6 +276,14 @@ export class Bridge extends EventEmitter {
public async quit(): Promise<void> {
log.verbose('PuppetPuppeteerBridge', 'quit()')
if (!this.page) {
throw new Error('no page')
}
if (!this.browser) {
throw new Error('no browser')
}
this.state.off('pending')
try {
......@@ -639,12 +647,16 @@ export class Bridge extends EventEmitter {
...args : any[]
): Promise<any> {
log.silly('PuppetPuppeteerBridge', 'proxyWechaty(%s%s)',
wechatyFunc,
args.length
? ' , ' + args.join(', ')
: '',
wechatyFunc,
args.length === 0
? ''
: ', ' + args.join(', '),
)
if (!this.page) {
throw new Error('no page')
}
try {
const noWechaty = await this.page.evaluate(() => {
return typeof WechatyBro === 'undefined'
......@@ -833,6 +845,11 @@ export class Bridge extends EventEmitter {
public async hostname(): Promise<string | null> {
log.verbose('PuppetPuppeteerBridge', 'hostname()')
if (!this.page) {
throw new Error('no page')
}
try {
const hostname = await this.page.evaluate(() => location.hostname) as string
log.silly('PuppetPuppeteerBridge', 'hostname() got %s', hostname)
......@@ -848,6 +865,10 @@ export class Bridge extends EventEmitter {
public async cookies(): Promise<Cookie[]>
public async cookies(cookieList?: Cookie[]): Promise<void | Cookie[]> {
if (!this.page) {
throw new Error('no page')
}
if (cookieList) {
try {
await this.page.setCookie(...cookieList)
......@@ -906,12 +927,22 @@ export class Bridge extends EventEmitter {
public async reload(): Promise<void> {
log.verbose('PuppetPuppeteerBridge', 'reload()')
if (!this.page) {
throw new Error('no page')
}
await this.page.reload()
return
}
public async evaluate(fn: () => any, ...args: any[]): Promise<any> {
log.silly('PuppetPuppeteerBridge', 'evaluate()')
if (!this.page) {
throw new Error('no page')
}
try {
return await this.page.evaluate(fn, ...args)
} catch (e) {
......
......@@ -31,15 +31,15 @@ import {
} from './puppet-puppeteer'
test('Puppet Puppeteer Event smoke testing', async t => {
const pw = new PuppetPuppeteer({
const puppet = new PuppetPuppeteer({
profile: new Profile(),
wechaty: new Wechaty(),
})
try {
await pw.start()
await puppet.start()
t.pass('should be inited')
await pw.stop()
await puppet.stop()
t.pass('should be quited')
} catch (e) {
t.fail('exception: ' + e.message)
......
......@@ -190,7 +190,7 @@ async function onMessage(
this : PuppetPuppeteer,
rawPayload : WebMessageRawPayload,
): Promise<void> {
let msg = this.Message.createMT(rawPayload.MsgId)
const msg = this.Message.createMT(rawPayload.MsgId)
try {
await msg.ready()
......@@ -206,9 +206,9 @@ async function onMessage(
case WebMessageType.SYS:
if (msg.room()) {
const joinResult = await Firer.checkRoomJoin.call(this , msg)
const leaveResult = await Firer.checkRoomLeave.call(this , msg)
const topicRestul = await Firer.checkRoomTopic.call(this , msg)
const joinResult = await Firer.checkRoomJoin.call(this, msg)
const leaveResult = await Firer.checkRoomLeave.call(this, msg)
const topicRestul = await Firer.checkRoomTopic.call(this, msg)
if (!joinResult && !leaveResult && !topicRestul) {
log.warn('PuppetPuppeteerEvent', `checkRoomSystem message: <${msg.text()}> not found`)
......@@ -219,31 +219,31 @@ async function onMessage(
break
}
/**
* Check Type for special Message
* reload if needed
*/
switch (rawPayload.MsgType) {
case WebMessageType.EMOTICON:
case WebMessageType.IMAGE:
case WebMessageType.VIDEO:
case WebMessageType.VOICE:
case WebMessageType.MICROVIDEO:
case WebMessageType.APP:
log.verbose('PuppetPuppeteerEvent', 'onMessage() EMOTICON/IMAGE/VIDEO/VOICE/MICROVIDEO message')
msg = this.Message.createMT(rawPayload.MsgId)
break
// /**
// * Check Type for special Message
// * reload if needed
// */
// switch (rawPayload.MsgType) {
// case WebMessageType.EMOTICON:
// case WebMessageType.IMAGE:
// case WebMessageType.VIDEO:
// case WebMessageType.VOICE:
// case WebMessageType.MICROVIDEO:
// case WebMessageType.APP:
// log.verbose('PuppetPuppeteerEvent', 'onMessage() EMOTICON/IMAGE/VIDEO/VOICE/MICROVIDEO message')
// msg = this.Message.createMT(rawPayload.MsgId)
// break
// case WebMessageType.TEXT:
// if (rawPayload.SubMsgType === WebMessageType.LOCATION) {
// log.verbose('PuppetPuppeteerEvent', 'onMessage() (TEXT&LOCATION) message')
// msg = this.Message.createMT(rawPayload.MsgId)
// }
// break
// }
// await msg.ready()
case WebMessageType.TEXT:
if (rawPayload.SubMsgType === WebMessageType.LOCATION) {
log.verbose('PuppetPuppeteerEvent', 'onMessage() (TEXT&LOCATION) message')
msg = this.Message.createMT(rawPayload.MsgId)
}
break
}
await msg.ready()
this.emit('message', msg)
} catch (e) {
......
......@@ -204,7 +204,7 @@ function parseRoomJoin(
this: PuppetPuppeteer,
content: string,
): [string[], string] {
log.verbose('PuppetPuppeteerFirer', 'checkRoomJoin(%s)', content)
log.verbose('PuppetPuppeteerFirer', 'parseRoomJoin(%s)', content)
const reListInvite = regexConfig.roomJoinInvite
const reListQrcode = regexConfig.roomJoinQrcode
......@@ -214,7 +214,7 @@ function parseRoomJoin(
let foundQrcode: string[]|null = []
reListQrcode.some(re => !!(foundQrcode = content.match(re)))
if ((!foundInvite || !foundInvite.length) && (!foundQrcode || !foundQrcode.length)) {
throw new Error('checkRoomJoin() not found matched re of ' + content)
throw new Error('parseRoomJoin() not found matched re of ' + content)
}
/**
* 管理员 invited 庆次、小桔妹 to the group chat
......@@ -233,7 +233,7 @@ async function checkRoomJoin(
const room = msg.room()
if (!room) {
log.warn('PuppetPuppeteerFirer', 'fireRoomJoin() `room` not found')
log.warn('PuppetPuppeteerFirer', 'checkRoomJoin() `room` not found')
return false
}
......@@ -243,23 +243,23 @@ async function checkRoomJoin(
try {
[inviteeList, inviter] = parseRoomJoin.call(this, text)
} catch (e) {
log.silly('PuppetPuppeteerFirer', 'fireRoomJoin() "%s" is not a join message', text)
log.silly('PuppetPuppeteerFirer', 'checkRoomJoin() "%s" is not a join message', text)
return false // not a room join message
}
log.silly('PuppetPuppeteerFirer', 'fireRoomJoin() inviteeList: %s, inviter: %s',
log.silly('PuppetPuppeteerFirer', 'checkRoomJoin() inviteeList: %s, inviter: %s',
inviteeList.join(','),
inviter,
)
let inviterContact: Contact | null = null
let inviteeContactList: Contact[] = []
let inviterContact: null | Contact = null
let inviteeContactList: Contact[] = []
try {
if (inviter === 'You' || inviter === '' || inviter === 'you') {
inviterContact = this.userSelf()
}
const max = 20
const max = 20
const backoff = 300
const timeout = max * (backoff * max) / 2
// 20 / 300 => 63,000
......
......@@ -33,9 +33,9 @@ const sinonTest = require('sinon-test')(sinon, {
import Profile from '../profile'
import Wechaty from '../wechaty'
import {
Contact,
} from '../contact'
// import {
// Contact as GlobalContact,
// } from '../contact'
import PuppetPuppeteer from './puppet-puppeteer'
import Bridge from './bridge'
......@@ -45,67 +45,69 @@ test('Puppet smoke testing', async t => {
const profile = new Profile(Math.random().toString(36).substr(2, 5))
const wechaty = new Wechaty()
const p = new PuppetPuppeteer({
const puppet = new PuppetPuppeteer({
profile,
wechaty,
})
;
(wechaty as any).initPuppetAccessory(puppet)
t.ok(p.state.off(), 'should be OFF state after instanciate')
p.state.on('pending')
t.ok(p.state.on(), 'should be ON state after set')
t.ok(p.state.pending(), 'should be pending state after set')
t.ok(puppet.state.off(), 'should be OFF state after instanciate')
puppet.state.on('pending')
t.ok(puppet.state.on(), 'should be ON state after set')
t.ok(puppet.state.pending(), 'should be pending state after set')
})
test('login/logout events', sinonTest(async function (t: test.Test) {
const sandbox = sinon.createSandbox()
sandbox.stub(Contact, 'findAll')
.onFirstCall().resolves([])
.onSecondCall().resolves([1])
.resolves([1, 2])
sandbox.stub(Event, 'onScan') // block the scan event to prevent reset logined user
sandbox.stub(Bridge.prototype, 'getUserName').resolves('mockedUserName')
sandbox.stub(PuppetPuppeteer.prototype, 'contactPayload').resolves({
NickName: 'mockedNickName',
UserName: 'mockedUserName',
})
try {
const profile = new Profile()
const wechaty = new Wechaty()
const pw = new PuppetPuppeteer({
const puppet = new PuppetPuppeteer({
profile,
wechaty,
})
t.ok(pw, 'should instantiated a PuppetPuppeteer')
;
(wechaty as any).initPuppetAccessory(puppet)
// FIXME: do not modify global instance
Contact.puppet = pw
t.ok(puppet, 'should instantiated a PuppetPuppeteer')
sandbox.stub(wechaty.Contact, 'findAll')
.onFirstCall().resolves([])
.onSecondCall().resolves([1])
.resolves([1, 2])
sandbox.stub(Event, 'onScan') // block the scan event to prevent reset logined user
sandbox.stub(Bridge.prototype, 'getUserName').resolves('mockedUserName')
sandbox.stub(puppet, 'contactRawPayload').resolves({
NickName: 'mockedNickName',
UserName: 'mockedUserName',
})
await pw.start()
await puppet.start()
t.pass('should be inited')
t.is(pw.logonoff() , false , 'should be not logined')
t.is(puppet.logonoff() , false , 'should be not logined')
const EXPECTED_CHIPER = 'loginFired'
const loginPromise = new Promise(r => pw.once('login', _ => r(EXPECTED_CHIPER)))
pw.bridge.emit('login', 'TestPuppetPuppeteer')
const loginPromise = new Promise(r => puppet.once('login', _ => r(EXPECTED_CHIPER)))
puppet.bridge.emit('login', 'TestPuppetPuppeteer')
t.is(await loginPromise, EXPECTED_CHIPER, 'should fired login event')
t.is(pw.logonoff(), true , 'should be logined')
t.is(puppet.logonoff(), true , 'should be logined')
t.ok((pw.bridge.getUserName as any).called, 'bridge.getUserName should be called')
t.ok((pw.contactPayload as any).called, 'pw.getContact should be called')
t.ok((puppet.bridge.getUserName as any).called, 'bridge.getUserName should be called')
t.ok((puppet.contactRawPayload as any).called, 'puppet.contactRawPayload should be called')
t.ok((Contact.findAll as any).called, 'contactFind stub should be called')
t.is((Contact.findAll as any).callCount, 4, 'should call stubContactFind 4 times')
t.ok((wechaty.Contact.findAll as any).called, 'contactFind stub should be called')
t.is((wechaty.Contact.findAll as any).callCount, 4, 'should call stubContactFind 4 times')
const logoutPromise = new Promise(resolve => pw.once('logout', _ => resolve('logoutFired')))
pw.bridge.emit('logout')
const logoutPromise = new Promise(resolve => puppet.once('logout', _ => resolve('logoutFired')))
puppet.bridge.emit('logout')
t.is(await logoutPromise, 'logoutFired', 'should fire logout event')
t.is(pw.logonoff(), false, 'should be logouted')
t.is(puppet.logonoff(), false, 'should be logouted')
await pw.stop()
await puppet.stop()
profile.destroy()
} catch (e) {
t.fail(e)
......
......@@ -33,9 +33,9 @@ import Wechaty from '../wechaty'
// import { PuppetMock } from '../puppet-mock/'
import {
Contact,
} from '../contact'
// import {
// Contact,
// } from '../contact'
import {
Message,
MessagePayload,
......@@ -108,10 +108,8 @@ test('constructor()', async t => {
const sandbox = sinon.createSandbox()
sandbox.stub(puppet, 'messagePayload').callsFake((_: string) => {
const payload = {
type: Message.Type.Text,
from: {
id: EXPECTED.from,
},
type : Message.Type.Text,
fromId : EXPECTED.from,
}
return payload
})
......@@ -253,8 +251,8 @@ test('self()', async t => {
function mockMessagePayload() {
const payload: MessagePayload = {
from : MOCK_CONTACT,
to : {} as any,
fromId : MOCK_CONTACT.id,
toId : 'to_id',
type : {} as any,
direction : {} as any,
date : {} as any,
......@@ -263,17 +261,16 @@ test('self()', async t => {
}
sandbox.stub(puppet, 'messagePayload').callsFake(mockMessagePayload)
sandbox.stub((puppet as any as { 'user': Contact }), 'user').value(MOCK_CONTACT)
sandbox.stub((puppet as any as { 'userId': string }), 'userId')
.value(MOCK_CONTACT.id)
const selfMsg = wechaty.Message.createMT('xxx')
await selfMsg.ready()
t.true(selfMsg.self(), 'should identify self message true where message from userId')
sandbox.stub((puppet as any as { 'user': Contact }), 'user').value(
// FIXME: use wechaty.Contact.load()
wechaty.Contact.load('fsadfjas;dlkdjfl;asjflk;sjfl;as'),
)
sandbox.stub((puppet as any as { 'userId': string }), 'userId')
.value('fasdfafasdfsdf')
const otherMsg = wechaty.Message.createMT('xxx')
await otherMsg.ready()
......
......@@ -837,6 +837,7 @@
send, // send message to wechat user
getContact,
getMessage,
getUserName,
getMsgImg,
getMsgEmoticon,
......
......@@ -2,5 +2,6 @@ export {
Puppet,
PuppetEventName,
PuppetOptions,
Receiver,
ScanData,
} from './puppet'
......@@ -85,16 +85,22 @@ export interface PuppetOptions {
wechaty: Wechaty,
}
export interface Receiver {
contactId? : string,
roomId? : string,
}
let PUPPET_COUNTER = 0
/**
* Abstract Puppet Class
*/
export abstract class Puppet extends EventEmitter implements Sayable {
public readonly state : StateSwitch
// public readonly classes : PuppetClasses
protected readonly watchdog: Watchdog
protected readonly watchdog : Watchdog
protected readonly counter : number
protected user?: Contact
protected userId?: string
/* tslint:disable:variable-name */
public readonly Contact : typeof Contact
......@@ -105,16 +111,20 @@ export abstract class Puppet extends EventEmitter implements Sayable {
/* tslint:disable:variable-name */
public readonly Room : typeof Room
public readonly state : StateSwitch
/**
* childPkg stores the `package.json` that the NPM module who extends the `Puppet`
*/
private readonly childPkg: normalize.Package
private readonly childPkg: undefined | normalize.Package
constructor(
public options: PuppetOptions,
public options: PuppetOptions,
) {
super()
this.counter = PUPPET_COUNTER++
const WATCHDOG_TIMEOUT = 1 * 60 * 1000 // default 1 minute
this.state = new StateSwitch(this.constructor.name, log)
......@@ -154,7 +164,7 @@ export abstract class Puppet extends EventEmitter implements Sayable {
}
public toString() {
return `Puppet<${this.options.profile.name}>`
return `Puppet#${this.counter}<${this.constructor.name}>(${this.options.profile.name})`
}
public emit(event: 'error', e: Error) : boolean
......@@ -199,7 +209,10 @@ export abstract class Puppet extends EventEmitter implements Sayable {
}
public version(): string {
return this.childPkg.version
if (this.childPkg) {
return this.childPkg.version
}
return '0.0.0'
}
/**
......@@ -236,11 +249,12 @@ export abstract class Puppet extends EventEmitter implements Sayable {
public userSelf(): Contact {
log.verbose('Puppet', 'self()')
if (!this.user) {
if (!this.userId) {
throw new Error('not logged in, no userSelf yet.')
}
return this.user
const user = this.Contact.load(this.userId)
return user
}
public async say(textOrFile: string | FileBox) : Promise<void> {
......@@ -248,30 +262,24 @@ export abstract class Puppet extends EventEmitter implements Sayable {
throw new Error('can not say before login')
}
let msg: Message
if (typeof textOrFile === 'string') {
msg = this.Message.createMO({
text : textOrFile,
to : this.userSelf(),
})
await this.messageSendText({
contactId: this.userSelf().id,
}, textOrFile)
} else if (textOrFile instanceof FileBox) {
msg = this.Message.createMO({
file: textOrFile,
to: this.userSelf(),
})
await this.messageSendFile({
contactId: this.userSelf().id,
}, textOrFile)
} else {
throw new Error('say() arg unknown')
}
await this.messageSend(msg)
}
/**
* Login / Logout
*/
public logonoff(): boolean {
if (this.user) {
if (this.userId) {
return true
} else {
return false
......@@ -286,8 +294,9 @@ export abstract class Puppet extends EventEmitter implements Sayable {
* Message
*
*/
public abstract async messageForward(message: Message, to: Contact | Room) : Promise<void>
public abstract async messageSend(message: Message) : Promise<void>
public abstract async messageForward(to: Receiver, messageId: string) : Promise<void>
public abstract async messageSendText(to: Receiver, text: string) : Promise<void>
public abstract async messageSendFile(to: Receiver, file: FileBox) : Promise<void>
public abstract async messageRawPayload(id: string) : Promise<any>
public abstract async messageRawPayloadParser(rawPayload: any) : Promise<MessagePayload>
......@@ -300,14 +309,22 @@ export abstract class Puppet extends EventEmitter implements Sayable {
/**
* Make sure all the contacts & room have already been ready
*/
if (payload.from && !payload.from.isReady()) {
await payload.from.ready()
const fromId = payload.fromId
const roomId = payload.roomId
const toId = payload.toId
const from = fromId && this.Contact.load(fromId)
const room = roomId && this.Room.load(roomId)
const to = toId && this.Contact.load(toId)
if (from && !from.isReady()) {
await from.ready()
}
if (payload.to && !payload.to.isReady()) {
await payload.to.ready()
if (to && !to.isReady()) {
await to.ready()
}
if (payload.room && !payload.room.isReady()) {
await payload.room.ready()
if (room && !room.isReady()) {
await room.ready()
}
return payload
......@@ -318,20 +335,20 @@ export abstract class Puppet extends EventEmitter implements Sayable {
* FriendRequest
*
*/
public abstract async friendRequestSend(contact: Contact, hello?: string) : Promise<void>
public abstract async friendRequestAccept(contact: Contact, ticket: string) : Promise<void>
public abstract async friendRequestSend(contactId: string, hello?: string) : Promise<void>
public abstract async friendRequestAccept(contactId: string, ticket: string) : Promise<void>
/**
*
* Room
*
*/
public abstract async roomAdd(room: Room, contact: Contact) : Promise<void>
public abstract async roomCreate(contactList: Contact[], topic?: string) : Promise<Room>
public abstract async roomDel(room: Room, contact: Contact) : Promise<void>
public abstract async roomFindAll(query?: RoomQueryFilter) : Promise<Room[]>
public abstract async roomQuit(room: Room) : Promise<void>
public abstract async roomTopic(room: Room, topic?: string) : Promise<string | void>
public abstract async roomAdd(roomId: string, contactId: string) : Promise<void>
public abstract async roomCreate(contactIdList: string[], topic?: string) : Promise<string>
public abstract async roomDel(roomId: string, contactId: string) : Promise<void>
public abstract async roomFindAll(query?: RoomQueryFilter) : Promise<string[]>
public abstract async roomQuit(roomId: string) : Promise<void>
public abstract async roomTopic(roomId: string, topic?: string) : Promise<string | void>
public abstract async roomRawPayload(id: string) : Promise<any>
public abstract async roomRawPayloadParser(rawPayload: any) : Promise<RoomPayload>
......@@ -348,17 +365,17 @@ export abstract class Puppet extends EventEmitter implements Sayable {
* Contact
*
*/
public abstract async contactAlias(contact: Contact) : Promise<string>
public abstract async contactAlias(contact: Contact, alias: string | null) : Promise<void>
public abstract async contactAlias(contact: Contact, alias?: string|null) : Promise<string | void>
public abstract async contactAvatar(contact: Contact) : Promise<FileBox>
public abstract async contactFindAll(query?: ContactQueryFilter) : Promise<Contact[]>
public abstract async contactAlias(contactId: string) : Promise<string>
public abstract async contactAlias(contactId: string, alias: string | null) : Promise<void>
public abstract async contactAlias(contactId: string, alias?: string|null) : Promise<string | void>
public abstract async contactAvatar(contactId: string) : Promise<FileBox>
public abstract async contactFindAll(query?: ContactQueryFilter) : Promise<string[]>
public abstract async contactRawPayload(id: string) : Promise<any>
public abstract async contactRawPayloadParser(rawPayload: any) : Promise<ContactPayload>
public async contactPayload(id: string): Promise<ContactPayload> {
log.verbose('Puppet', 'contactPayload(%s)', id)
log.verbose('Puppet', 'contactPayload(%s) @ %s', id, this)
const rawPayload = await this.contactRawPayload(id)
const payload = await this.contactRawPayloadParser(rawPayload)
return payload
......
......@@ -17,7 +17,7 @@
*
* @ignore
*/
import * as util from 'util'
// import * as util from 'util'
import {
FileBox,
......@@ -32,10 +32,10 @@ import {
Sayable,
log,
} from './config'
import PuppetAccessory from './puppet-accessory'
import { PuppetAccessory } from './puppet-accessory'
import Contact from './contact'
import Message from './message'
import { Contact } from './contact'
// import Message from './message'
export const ROOM_EVENT_DICT = {
join: 'tbw',
......@@ -57,9 +57,9 @@ export interface RoomQueryFilter {
export interface RoomPayload {
// id: string,
// encryId: string,
topic: string,
memberList: Contact[],
owner?: Contact,
topic : string,
memberIdList : string[],
ownerId? : string,
nameMap: Map<string, string>,
roomAliasMap: Map<string, string>,
......@@ -75,7 +75,7 @@ export interface RoomPayload {
*/
export class Room extends PuppetAccessory implements Sayable {
protected static readonly pool = new Map<string, Room>()
protected static pool: Map<string, Room>
/**
* Create a new room.
......@@ -102,7 +102,9 @@ export class Room extends PuppetAccessory implements Sayable {
}
try {
const room = await this.puppet.roomCreate(contactList, topic)
const contactIdList = contactList.map(contact => contact.id)
const roomId = await this.puppet.roomCreate(contactIdList, topic)
const room = this.load(roomId)
return room
} catch (e) {
log.error('Room', 'create() exception: %s', e && e.stack || e.message || e)
......@@ -132,7 +134,8 @@ export class Room extends PuppetAccessory implements Sayable {
}
try {
const roomList = await this.puppet.roomFindAll(query)
const roomIdList = await this.puppet.roomFindAll(query)
const roomList = roomIdList.map(id => this.load(id))
await Promise.all(roomList.map(room => room.ready()))
return roomList
......@@ -170,9 +173,12 @@ export class Room extends PuppetAccessory implements Sayable {
* @private
* About the Generic: https://stackoverflow.com/q/43003970/1123955
*/
public static load<T extends typeof Room>(this: T, id: string): T['prototype'] {
if (!id) {
throw new Error('Room.load() no id')
public static load<T extends typeof Room>(
this : T,
id : string,
): T['prototype'] {
if (!this.pool) {
this.pool = new Map<string, Room>()
}
const existingRoom = this.pool.get(id)
......@@ -185,6 +191,15 @@ export class Room extends PuppetAccessory implements Sayable {
return newRoom
}
// public load(
// this : Room,
// id : string,
// ): Room {
// const klass = instanceToClass(this, Room)
// const room = klass.load(id)
// return room
// }
/**
*
*
......@@ -243,13 +258,13 @@ export class Room extends PuppetAccessory implements Sayable {
const payload = await this.puppet.roomPayload(this.id)
await Promise.all(
payload.memberList.map(
contact => contact.ready(),
),
payload.memberIdList
.map(id => this.puppet.Contact.load(id))
.map(contact => contact.ready()),
)
log.silly('Room', 'ready() this.payload="%s"',
util.inspect(payload),
)
// log.silly('Room', 'ready() this.payload="%s"',
// util.inspect(payload),
// )
this.payload = payload
}
......@@ -258,7 +273,7 @@ export class Room extends PuppetAccessory implements Sayable {
* @private
*/
public isReady(): boolean {
return !!(this.payload && this.payload.memberList && this.payload.memberList.length)
return !!(this.payload && this.payload.memberIdList && this.payload.memberIdList.length)
}
public say(text: string) : Promise<void>
......@@ -298,7 +313,6 @@ export class Room extends PuppetAccessory implements Sayable {
? mention.map(c => c.name()).join(', ')
: mention ? mention.name() : '',
)
let msg: Message
let text: string
const replyToList: Contact[] = [].concat(mention as any || [])
......@@ -312,22 +326,17 @@ export class Room extends PuppetAccessory implements Sayable {
} else {
text = textOrFile
}
msg = this.puppet.Message.createMO({
room : this,
to : replyToList[0], // FIXME: is this right?
text,
})
await this.puppet.messageSendText({
roomId: this.id,
contactId: replyToList[0].id,
}, text)
} else if (textOrFile instanceof FileBox) {
msg = this.puppet.Message.createMO({
room: this,
to: replyToList[0],
file: textOrFile,
})
await this.puppet.messageSendFile({
roomId: this.id,
}, textOrFile)
} else {
throw new Error('arg unsupported')
}
await this.puppet.messageSend(msg)
}
public emit(event: 'leave', leaver: Contact[], remover?: Contact) : boolean
......@@ -423,7 +432,7 @@ export class Room extends PuppetAccessory implements Sayable {
*/
public async add(contact: Contact): Promise<void> {
log.verbose('Room', 'add(%s)', contact)
await this.puppet.roomAdd(this, contact)
await this.puppet.roomAdd(this.id, contact.id)
}
/**
......@@ -445,18 +454,18 @@ export class Room extends PuppetAccessory implements Sayable {
*/
public async del(contact: Contact): Promise<void> {
log.verbose('Room', 'del(%s)', contact)
await this.puppet.roomDel(this, contact)
await this.puppet.roomDel(this.id, contact.id)
this.delLocal(contact)
}
private delLocal(contact: Contact): void {
log.verbose('Room', 'delLocal(%s)', contact)
const memberList = this.payload && this.payload.memberList
if (memberList && memberList.length > 0) {
for (let i = 0; i < memberList.length; i++) {
if (memberList[i].id === contact.id) {
memberList.splice(i, 1)
const memberIdList = this.payload && this.payload.memberIdList
if (memberIdList && memberIdList.length > 0) {
for (let i = 0; i < memberIdList.length; i++) {
if (memberIdList[i] === contact.id) {
memberIdList.splice(i, 1)
break
}
}
......@@ -468,7 +477,7 @@ export class Room extends PuppetAccessory implements Sayable {
*/
public async quit(): Promise<void> {
log.verbose('Room', 'quit() %s', this)
await this.puppet.roomQuit(this)
await this.puppet.roomQuit(this.id)
}
public topic() : string
......@@ -514,7 +523,7 @@ export class Room extends PuppetAccessory implements Sayable {
}
const future = this.puppet
.roomTopic(this, newTopic)
.roomTopic(this.id, newTopic)
.then(() => {
this.payload = {
...this.payload || {} as RoomPayload,
......@@ -580,11 +589,11 @@ export class Room extends PuppetAccessory implements Sayable {
* }
*/
public has(contact: Contact): boolean {
if (!this.payload || !this.payload.memberList) {
if (!this.payload || !this.payload.memberIdList) {
return false
}
return this.payload.memberList
.filter(c => c.id === contact.id)
return this.payload.memberIdList
.filter(id => id === contact.id)
.length > 0
}
......@@ -636,7 +645,7 @@ export class Room extends PuppetAccessory implements Sayable {
throw new Error('Room member find queryArg only support one key. multi key support is not availble now.')
}
if (!this.payload || !this.payload.memberList) {
if (!this.payload || !this.payload.memberIdList) {
log.warn('Room', 'member() not ready')
return []
}
......@@ -737,7 +746,7 @@ export class Room extends PuppetAccessory implements Sayable {
public memberList(): Contact[] {
log.verbose('Room', 'memberList')
if (!this.payload || !this.payload.memberList || this.payload.memberList.length < 1) {
if (!this.payload || !this.payload.memberIdList || this.payload.memberIdList.length < 1) {
log.warn('Room', 'memberList() not ready')
log.verbose('Room', 'memberList() trying call refresh() to update')
this.sync().then(() => {
......@@ -745,7 +754,8 @@ export class Room extends PuppetAccessory implements Sayable {
})
return []
}
return this.payload.memberList
const memberList = this.payload.memberIdList.map(id => this.puppet.Contact.load(id))
return memberList
}
/**
......@@ -781,7 +791,13 @@ export class Room extends PuppetAccessory implements Sayable {
public owner(): Contact | null {
log.info('Room', 'owner()')
return this.payload && this.payload.owner || null
const ownerId = this.payload && this.payload.ownerId
if (!ownerId) {
return null
}
const owner = this.puppet.Contact.load(ownerId)
return owner
}
}
......
......@@ -189,7 +189,17 @@ export class Wechaty extends PuppetAccessory implements Sayable {
/**
* @private
*/
public toString() { return `Wechaty<${this.options.puppet}, ${this.profile.name}>`}
public toString() {
if (!this.options) {
return this.constructor.name
}
return [
'Wechaty',
`<${this.options && this.options.puppet || ''}>`,
`(${this.profile && this.profile.name || ''})`,
].join('')
}
/**
* @private
......@@ -622,15 +632,15 @@ export class Wechaty extends PuppetAccessory implements Sayable {
/**
* @private
*/
public async send(message: Message): Promise<void> {
try {
await this.puppet.messageSend(message)
} catch (e) {
log.error('Wechaty', 'send() exception: %s', e.message)
Raven.captureException(e)
throw e
}
}
// public async send(message: Message): Promise<void> {
// try {
// await this.puppet.messageSend(message)
// } catch (e) {
// log.error('Wechaty', 'send() exception: %s', e.message)
// Raven.captureException(e)
// throw e
// }
// }
/**
* Send message to filehelper
......@@ -690,6 +700,9 @@ export class Wechaty extends PuppetAccessory implements Sayable {
return
}
public unref(): void {
log.warn('Wechaty', 'unref() To Be Implemented. See: https://github.com/Chatie/wechaty/issues/1197')
}
}
export default Wechaty
#!/usr/bin/env node
#!/usr/bin/env ts-node
import { Wechaty } from 'wechaty'
......
......@@ -3,6 +3,7 @@
"target": "es6"
, "module": "commonjs"
, "outDir": "dist"
, "strict": true
, "experimentalDecorators": true
, "emitDecoratorMetadata": true
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册