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

merge

...@@ -92,7 +92,7 @@ bot ...@@ -92,7 +92,7 @@ bot
* when request is set, we can get verify message from `request.hello`, * when request is set, we can get verify message from `request.hello`,
* and accept this request by `request.accept()` * and accept this request by `request.accept()`
*/ */
if (request.hello === 'ding') { if (request.hello() === 'ding') {
logMsg = 'accepted because verify messsage is "ding"' logMsg = 'accepted because verify messsage is "ding"'
request.accept() request.accept()
......
...@@ -53,7 +53,7 @@ export async function onFriend( ...@@ -53,7 +53,7 @@ export async function onFriend(
3000, 3000,
) )
if (request.hello === 'ding') { if (request.hello() === 'ding') {
const myRoom = await this.Room.find({ topic: 'ding' }) const myRoom = await this.Room.find({ topic: 'ding' })
if (!myRoom) return if (!myRoom) return
setTimeout( setTimeout(
......
{ {
"name": "wechaty", "name": "wechaty",
"version": "0.15.27", "version": "0.15.30",
"description": "Wechat for Bot(Personal Account)", "description": "Wechat for Bot(Personal Account)",
"main": "dist/src/index.js", "main": "dist/src/index.js",
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",
...@@ -97,13 +97,14 @@ ...@@ -97,13 +97,14 @@
"node": ">= 8.5" "node": ">= 8.5"
}, },
"dependencies": { "dependencies": {
"@types/ws": "^4.0.1", "@types/ws": "^5.1.0",
"bl": "^1.2.0", "bl": "^1.2.0",
"brolog": "^1.2.0", "brolog": "^1.2.0",
"clone-class": "^0.6.1", "clone-class": "^0.6.3",
"cuid": "^2.1.1", "cuid": "^2.1.1",
"hot-import": "^0.1.0", "hot-import": "^0.1.0",
"mime": "^2.2.0", "mime": "^2.2.0",
"normalize-package-data": "^2.4.0",
"puppeteer": "^1.2.0", "puppeteer": "^1.2.0",
"raven": "^2.2.0", "raven": "^2.2.0",
"read-pkg-up": "^3.0.0", "read-pkg-up": "^3.0.0",
...@@ -125,13 +126,14 @@ ...@@ -125,13 +126,14 @@
"@types/fluent-ffmpeg": "^2.1.0", "@types/fluent-ffmpeg": "^2.1.0",
"@types/glob": "^5.0.0p", "@types/glob": "^5.0.0p",
"@types/mime": "^2.0.0", "@types/mime": "^2.0.0",
"@types/node": "^9.6.7", "@types/node": "^10.0.3",
"@types/normalize-package-data": "^2.4.0",
"@types/puppeteer": "^1.0.0", "@types/puppeteer": "^1.0.0",
"@types/raven": "^2.1.0", "@types/raven": "^2.1.0",
"@types/read-pkg-up": "^3.0.0", "@types/read-pkg-up": "^3.0.0",
"@types/request": "^2.0.0", "@types/request": "^2.0.0",
"@types/semver": "^5.5.0", "@types/semver": "^5.5.0",
"@types/sinon": "^4.3.0", "@types/sinon": "^4.3.1",
"@types/xml2js": "^0.4.0", "@types/xml2js": "^0.4.0",
"apiai": "^4.0.0", "apiai": "^4.0.0",
"babel-cli": "^6.26.0", "babel-cli": "^6.26.0",
...@@ -156,7 +158,7 @@ ...@@ -156,7 +158,7 @@
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",
"semver": "^5.5.0", "semver": "^5.5.0",
"shx": "^0.2.0", "shx": "^0.2.0",
"sinon": "^5.0.0", "sinon": "^5.0.2",
"sinon-test": "^2.1.2", "sinon-test": "^2.1.2",
"sloc": "^0.2.0", "sloc": "^0.2.0",
"ts-node": "^6.0.0", "ts-node": "^6.0.0",
......
...@@ -49,12 +49,17 @@ export abstract class PuppetAccessory extends EventEmitter { ...@@ -49,12 +49,17 @@ export abstract class PuppetAccessory extends EventEmitter {
private _puppet?: Puppet private _puppet?: Puppet
public set puppet(puppet: Puppet) { public set puppet(puppet: Puppet) {
log.silly('PuppetAccessory', '<%s> set puppet(%s)', this[PUPPET_ACCESSORY_NAME], puppet.constructor.name) log.silly('PuppetAccessory', '<%s> set puppet(%s)',
this[PUPPET_ACCESSORY_NAME],
puppet.constructor.name,
)
this._puppet = puppet this._puppet = puppet
} }
public get puppet(): Puppet { public get puppet(): Puppet {
log.silly('PuppetAccessory', '<%s> get puppet()', this[PUPPET_ACCESSORY_NAME]) log.silly('PuppetAccessory', '<%s> get puppet()',
this[PUPPET_ACCESSORY_NAME],
)
if (this._puppet) { if (this._puppet) {
return this._puppet return this._puppet
......
# PUPPET-MOCK
```ts
import PuppetMock from 'wechaty-puppet-mock'
const puppet = new PuppetMock({
profile,
})
const wechaty = new Wechaty({
puppet,
})
```
## HELPER UTILITIES
### StateSwitch
```ts
this.state.on('pending')
this.state.on(true)
this.state.off('pending')
this.state.off(true)
await this.state.ready('on')
await this.state.ready('off')
```
### Watchdog
```ts
```
### Profile
```ts
this.profile.set('config', { id: 1, key: 'xxx' })
const config = await this.profile.get('config')
```
...@@ -22,23 +22,27 @@ import { ...@@ -22,23 +22,27 @@ import {
log, log,
} from '../config' } from '../config'
import { FriendRequest } from '../puppet/' import { FriendRequest, FriendRequestType } from '../puppet/'
import MockContact from './mock-contact' import MockContact from './mock-contact'
export class MockFriendRequest extends FriendRequest { export class MockFriendRequest extends FriendRequest {
public info: any public payload: any
private ticket: string private ticket: string
private _contact: MockContact
private _type: FriendRequestType
private _hello: string
constructor() { constructor() {
log.verbose('MockFriendRequest', 'constructor()') log.verbose('MockFriendRequest', 'constructor()')
super() super()
} }
public receive(info: any): void { public receive(payload: any): void {
log.verbose('MockFriendRequest', 'receive(%s)', info) log.verbose('MockFriendRequest', 'receive(%s)', payload)
this.payload = payload
} }
public confirm(contact: MockContact): void { public confirm(contact: MockContact): void {
...@@ -52,9 +56,24 @@ export class MockFriendRequest extends FriendRequest { ...@@ -52,9 +56,24 @@ export class MockFriendRequest extends FriendRequest {
public async accept(): Promise<void> { public async accept(): Promise<void> {
log.verbose('FriendRequest', 'accept() %s', this.contact) log.verbose('FriendRequest', 'accept() %s', this.contact)
await this.puppet.friendRequestAccept(this.contact, this.ticket) await this.puppet.friendRequestAccept(this.contact(), this.ticket)
} }
public contact(): MockContact {
return this._contact
}
public type(): FriendRequestType {
return this._type
}
public async reject(): Promise<void> {
log.verbose('MockFriendRequest', 'reject()')
}
public hello(): string {
return this._hello
}
} }
export default MockFriendRequest export default MockFriendRequest
...@@ -91,6 +91,21 @@ export class MockMessage extends Message { ...@@ -91,6 +91,21 @@ export class MockMessage extends Message {
return loadedContact return loadedContact
} }
public to(contact: MockContact): this
public to(id: string): this
public to(): MockContact | null // if to is not set, then room must had set
public to(contact?: MockContact | string): MockContact | null | this {
if (contact) {
return this
}
const to = MockContact.load('mockid') as MockContact
to.puppet = this.puppet
return to
}
public room(room: MockRoom): this public room(room: MockRoom): this
public room(): MockRoom | null public room(): MockRoom | null
...@@ -112,12 +127,24 @@ export class MockMessage extends Message { ...@@ -112,12 +127,24 @@ export class MockMessage extends Message {
} }
public async say( public async say(
textOrMessage: string | MockMessage, textOrMessage: string | MockMessage,
replyTo?: MockContact | MockContact[], replyTo?: MockContact | MockContact[],
): Promise<void> { ): Promise<void> {
log.verbose('MockMessage', 'say(%s, %s)', textOrMessage, replyTo) log.verbose('MockMessage', 'say(%s, %s)', textOrMessage, replyTo)
const m = new MockMessage()
await this.puppet.send(m) const message = new MockMessage()
message.from(this.puppet.userSelf() as MockContact)
message.to(this.from())
const room = this.room()
if (room) {
message.room(room)
}
// TODO: implement the replyTo
await this.puppet.send(message)
} }
public type(): MsgType { public type(): MsgType {
...@@ -146,21 +173,6 @@ export class MockMessage extends Message { ...@@ -146,21 +173,6 @@ export class MockMessage extends Message {
return this return this
} }
public to(contact: MockContact): this
public to(id: string): this
public to(): MockContact | null // if to is not set, then room must had set
public to(contact?: MockContact | string): MockContact | null | this {
if (contact) {
return this
}
const to = MockContact.load('mockid') as MockContact
to.puppet = this.puppet
return to
}
public async readyStream(): Promise<Readable> { public async readyStream(): Promise<Readable> {
log.verbose('MockMessage', 'readyStream()') log.verbose('MockMessage', 'readyStream()')
throw new Error('to be mocked') throw new Error('to be mocked')
...@@ -178,7 +190,7 @@ export class MockMessage extends Message { ...@@ -178,7 +190,7 @@ export class MockMessage extends Message {
return 'text/plain' return 'text/plain'
} }
public async forward(to: MockRoom|MockContact): Promise<void> { public async forward(to: MockRoom | MockContact): Promise<void> {
/** /**
* 1. Text message * 1. Text message
*/ */
......
...@@ -91,6 +91,7 @@ export class PuppetMock extends Puppet { ...@@ -91,6 +91,7 @@ export class PuppetMock extends Puppet {
await this.state.ready('off') await this.state.ready('off')
return return
} }
this.state.off('pending') this.state.off('pending')
// await some tasks... // await some tasks...
this.state.off(true) this.state.off(true)
...@@ -139,12 +140,6 @@ export class PuppetMock extends Puppet { ...@@ -139,12 +140,6 @@ export class PuppetMock extends Puppet {
return await this.userSelf().say(text) return await this.userSelf().say(text)
} }
public async login(user: MockContact): Promise<void> {
log.verbose('PuppetMock', 'login(%s)', user)
this.user = user
this.emit('login', user)
}
public async logout(): Promise<void> { public async logout(): Promise<void> {
log.verbose('PuppetMock', 'logout()') log.verbose('PuppetMock', 'logout()')
......
...@@ -59,7 +59,10 @@ function onDing( ...@@ -59,7 +59,10 @@ function onDing(
this.emit('watchdog', { data }) this.emit('watchdog', { data })
} }
async function onScan(this: PuppetPuppeteer, data: ScanData): Promise<void> { async function onScan(
this: PuppetPuppeteer,
data: ScanData,
): Promise<void> {
log.verbose('PuppetPuppeteerEvent', 'onScan({code: %d, url: %s})', data.code, data.url) log.verbose('PuppetPuppeteerEvent', 'onScan({code: %d, url: %s})', data.code, data.url)
if (this.state.off()) { if (this.state.off()) {
...@@ -93,7 +96,11 @@ function onLog(data: any): void { ...@@ -93,7 +96,11 @@ function onLog(data: any): void {
log.silly('PuppetPuppeteerEvent', 'onLog(%s)', data) log.silly('PuppetPuppeteerEvent', 'onLog(%s)', data)
} }
async function onLogin(this: PuppetPuppeteer, note: string, ttl = 30): Promise<void> { async function onLogin(
this: PuppetPuppeteer,
note: string,
ttl = 30,
): Promise<void> {
log.verbose('PuppetPuppeteerEvent', 'onLogin(%s, %d)', note, ttl) log.verbose('PuppetPuppeteerEvent', 'onLogin(%s, %d)', note, ttl)
const TTL_WAIT_MILLISECONDS = 1 * 1000 const TTL_WAIT_MILLISECONDS = 1 * 1000
......
...@@ -27,11 +27,12 @@ import * as test from 'blue-tape' ...@@ -27,11 +27,12 @@ import * as test from 'blue-tape'
// import * as sinon from 'sinon' // import * as sinon from 'sinon'
// const sinonTest = require('sinon-test')(sinon) // const sinonTest = require('sinon-test')(sinon)
import { Firer } from './firer' import { PuppetPuppeteer } from './puppet-puppeteer'
import { Firer } from './firer'
test('Firer smoke testing', async t => { const mockPuppetPuppeteer = {
t.true(true, 'should be true') userSelf: () => ({}),
}) } as any as PuppetPuppeteer
test('parseFriendConfirm()', async t => { test('parseFriendConfirm()', async t => {
const contentList = [ const contentList = [
...@@ -55,11 +56,11 @@ test('parseFriendConfirm()', async t => { ...@@ -55,11 +56,11 @@ test('parseFriendConfirm()', async t => {
let result: boolean let result: boolean
contentList.forEach(([content]) => { contentList.forEach(([content]) => {
result = Firer.parseFriendConfirm(content) result = Firer.parseFriendConfirm.call(mockPuppetPuppeteer, content)
t.true(result, 'should be truthy for confirm msg: ' + content) t.true(result, 'should be truthy for confirm msg: ' + content)
}) })
result = Firer.parseFriendConfirm('fsdfsdfasdfasdfadsa') result = Firer.parseFriendConfirm.call(mockPuppetPuppeteer, 'fsdfsdfasdfasdfadsa')
t.false(result, 'should be falsy for other msg') t.false(result, 'should be falsy for other msg')
}) })
...@@ -119,14 +120,14 @@ test('parseRoomJoin()', async t => { ...@@ -119,14 +120,14 @@ test('parseRoomJoin()', async t => {
let result let result
contentList.forEach(([content, inviter, inviteeList]) => { contentList.forEach(([content, inviter, inviteeList]) => {
result = Firer.parseRoomJoin(content) result = Firer.parseRoomJoin.call(mockPuppetPuppeteer, content)
t.ok(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')
}) })
t.throws(() => { t.throws(() => {
Firer.parseRoomJoin('fsadfsadfsdfsdfs') Firer.parseRoomJoin.call(mockPuppetPuppeteer, 'fsadfsadfsdfsdfs')
}, Error, 'should throws if message is not expected') }, Error, 'should throws if message is not expected')
}) })
...@@ -154,19 +155,19 @@ test('parseRoomLeave()', async t => { ...@@ -154,19 +155,19 @@ test('parseRoomLeave()', async t => {
] ]
contentLeaverList.forEach(([content, leaver]) => { contentLeaverList.forEach(([content, leaver]) => {
const resultLeaver = Firer.parseRoomLeave(content)[0] const resultLeaver = Firer.parseRoomLeave.call(mockPuppetPuppeteer, content)[0]
t.ok(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.call(mockPuppetPuppeteer, content)[1]
t.ok(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')
}) })
t.throws(() => { t.throws(() => {
Firer.parseRoomLeave('fafdsfsdfafa') Firer.parseRoomLeave.call(mockPuppetPuppeteer, 'fafdsfsdfafa')
}, Error, 'should throw if message is not expected') }, Error, 'should throw if message is not expected')
}) })
...@@ -186,14 +187,14 @@ test('parseRoomTopic()', async t => { ...@@ -186,14 +187,14 @@ test('parseRoomTopic()', async t => {
let result let result
contentList.forEach(([content, changer, topic]) => { contentList.forEach(([content, changer, topic]) => {
result = Firer.parseRoomTopic(content) result = Firer.parseRoomTopic.call(mockPuppetPuppeteer, content)
t.ok(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')
}) })
t.throws(() => { t.throws(() => {
Firer.parseRoomTopic('fafdsfsdfafa') Firer.parseRoomTopic.call(mockPuppetPuppeteer, 'fafdsfsdfafa')
}, Error, 'should throw if message is not expected') }, Error, 'should throw if message is not expected')
}) })
...@@ -102,11 +102,14 @@ const regexConfig = { ...@@ -102,11 +102,14 @@ const regexConfig = {
], ],
} }
async function checkFriendRequest(m: PuppeteerMessage) { async function checkFriendRequest(
if (!m.rawObj) { this: PuppetPuppeteer,
msg: PuppeteerMessage,
) {
if (!msg.rawObj) {
throw new Error('message empty') throw new Error('message empty')
} }
const info = m.rawObj.RecommendInfo const info = msg.rawObj.RecommendInfo
log.verbose('PuppetPuppeteerFirer', 'fireFriendRequest(%s)', info) log.verbose('PuppetPuppeteerFirer', 'fireFriendRequest(%s)', info)
if (!info) { if (!info) {
...@@ -114,22 +117,25 @@ async function checkFriendRequest(m: PuppeteerMessage) { ...@@ -114,22 +117,25 @@ async function checkFriendRequest(m: PuppeteerMessage) {
} }
const request = new PuppeteerFriendRequest() const request = new PuppeteerFriendRequest()
request.puppet = m.puppet request.puppet = msg.puppet
request.receive(info) request.receive(info)
await request.contact.ready() await request.contact().ready()
if (!request.contact.isReady()) { if (!request.contact().isReady()) {
log.warn('PuppetPuppeteerFirer', 'fireFriendConfirm() contact still not ready after `ready()` call') log.warn('PuppetPuppeteerFirer', 'fireFriendConfirm() contact still not ready after `ready()` call')
} }
this.emit('friend', request.contact, request) this.emit('friend', request.contact(), request)
} }
/** /**
* try to find FriendRequest Confirmation Message * try to find FriendRequest Confirmation Message
*/ */
function parseFriendConfirm(content: string): boolean { function parseFriendConfirm(
this: PuppetPuppeteer,
content: string,
): boolean {
const reList = regexConfig.friendConfirm const reList = regexConfig.friendConfirm
let found = false let found = false
...@@ -141,11 +147,14 @@ function parseFriendConfirm(content: string): boolean { ...@@ -141,11 +147,14 @@ function parseFriendConfirm(content: string): boolean {
} }
} }
async function checkFriendConfirm(m: PuppeteerMessage) { async function checkFriendConfirm(
this: PuppetPuppeteer,
m: PuppeteerMessage,
) {
const content = m.text() const content = m.text()
log.silly('PuppetPuppeteerFirer', 'fireFriendConfirm(%s)', content) log.silly('PuppetPuppeteerFirer', 'fireFriendConfirm(%s)', content)
if (!parseFriendConfirm(content)) { if (!parseFriendConfirm.call(this, content)) {
return return
} }
const request = new PuppeteerFriendRequest() const request = new PuppeteerFriendRequest()
...@@ -171,7 +180,10 @@ async function checkFriendConfirm(m: PuppeteerMessage) { ...@@ -171,7 +180,10 @@ async function checkFriendConfirm(m: PuppeteerMessage) {
* 管理员 invited 小桔建群助手 to the group chat * 管理员 invited 小桔建群助手 to the group chat
* 管理员 invited 庆次、小桔妹 to the group chat * 管理员 invited 庆次、小桔妹 to the group chat
*/ */
function parseRoomJoin(content: string): [string[], string] { function parseRoomJoin(
this: PuppetPuppeteer,
content: string,
): [string[], string] {
log.verbose('PuppetPuppeteerFirer', 'checkRoomJoin(%s)', content) log.verbose('PuppetPuppeteerFirer', 'checkRoomJoin(%s)', content)
const reListInvite = regexConfig.roomJoinInvite const reListInvite = regexConfig.roomJoinInvite
...@@ -194,19 +206,22 @@ function parseRoomJoin(content: string): [string[], string] { ...@@ -194,19 +206,22 @@ function parseRoomJoin(content: string): [string[], string] {
return [inviteeList, inviter] // put invitee at first place return [inviteeList, inviter] // put invitee at first place
} }
async function checkRoomJoin(m: PuppeteerMessage): Promise<boolean> { async function checkRoomJoin(
this: PuppetPuppeteer,
msg: PuppeteerMessage,
): Promise<boolean> {
const room = m.room() const room = msg.room()
if (!room) { if (!room) {
log.warn('PuppetPuppeteerFirer', 'fireRoomJoin() `room` not found') log.warn('PuppetPuppeteerFirer', 'fireRoomJoin() `room` not found')
return false return false
} }
const text = m.text() const text = msg.text()
let inviteeList: string[], inviter: string let inviteeList: string[], inviter: string
try { try {
[inviteeList, inviter] = parseRoomJoin(text) [inviteeList, inviter] = parseRoomJoin.call(this, text)
} catch (e) { } catch (e) {
log.silly('PuppetPuppeteerFirer', 'fireRoomJoin() "%s" is not a join message', text) log.silly('PuppetPuppeteerFirer', 'fireRoomJoin() "%s" is not a join message', text)
return false // not a room join message return false // not a room join message
...@@ -221,8 +236,7 @@ async function checkRoomJoin(m: PuppeteerMessage): Promise<boolean> { ...@@ -221,8 +236,7 @@ async function checkRoomJoin(m: PuppeteerMessage): Promise<boolean> {
try { try {
if (inviter === 'You' || inviter === '' || inviter === 'you') { if (inviter === 'You' || inviter === '' || inviter === 'you') {
inviterContact = PuppeteerContact.load(this.userId) as PuppeteerContact inviterContact = this.userSelf()
inviterContact.puppet = m.puppet
} }
const max = 20 const max = 20
...@@ -321,7 +335,10 @@ async function checkRoomJoin(m: PuppeteerMessage): Promise<boolean> { ...@@ -321,7 +335,10 @@ async function checkRoomJoin(m: PuppeteerMessage): Promise<boolean> {
} }
function parseRoomLeave(content: string): [string, string] { function parseRoomLeave(
this: PuppetPuppeteer,
content: string,
): [string, string] {
const reListByBot = regexConfig.roomLeaveByBot const reListByBot = regexConfig.roomLeaveByBot
const reListByOther = regexConfig.roomLeaveByOther const reListByOther = regexConfig.roomLeaveByOther
let foundByBot: string[]|null = [] let foundByBot: string[]|null = []
...@@ -331,7 +348,7 @@ function parseRoomLeave(content: string): [string, string] { ...@@ -331,7 +348,7 @@ function parseRoomLeave(content: string): [string, string] {
if ((!foundByBot || !foundByBot.length) && (!foundByOther || !foundByOther.length)) { if ((!foundByBot || !foundByBot.length) && (!foundByOther || !foundByOther.length)) {
throw new Error('checkRoomLeave() no matched re for ' + content) throw new Error('checkRoomLeave() no matched re for ' + content)
} }
const [leaver, remover] = foundByBot ? [ foundByBot[1], this.userId ] : [ this.userId, foundByOther[1] ] const [leaver, remover] = foundByBot ? [ foundByBot[1], this.userSelf().id ] : [ this.userSelf().id, foundByOther[1] ]
return [leaver, remover] return [leaver, remover]
} }
...@@ -346,7 +363,7 @@ async function checkRoomLeave( ...@@ -346,7 +363,7 @@ async function checkRoomLeave(
let leaver: string, remover: string let leaver: string, remover: string
try { try {
[leaver, remover] = parseRoomLeave(m.text()) [leaver, remover] = parseRoomLeave.call(this, m.text())
} catch (e) { } catch (e) {
return false return false
} }
...@@ -363,8 +380,7 @@ async function checkRoomLeave( ...@@ -363,8 +380,7 @@ async function checkRoomLeave(
*/ */
let leaverContact: PuppeteerContact | null, removerContact: PuppeteerContact | null let leaverContact: PuppeteerContact | null, removerContact: PuppeteerContact | null
if (leaver === this.userSelf().id) { if (leaver === this.userSelf().id) {
leaverContact = PuppeteerContact.load(this.userSelf().id) leaverContact = this.userSelf()
leaverContact.puppet = m.puppet
// not sure which is better // not sure which is better
// removerContact = room.member({contactAlias: remover}) || room.member({name: remover}) // removerContact = room.member({contactAlias: remover}) || room.member({name: remover})
...@@ -406,7 +422,10 @@ async function checkRoomLeave( ...@@ -406,7 +422,10 @@ async function checkRoomLeave(
return true return true
} }
function parseRoomTopic(content: string): [string, string] { function parseRoomTopic(
this: PuppetPuppeteer,
content: string,
): [string, string] {
const reList = regexConfig.roomTopic const reList = regexConfig.roomTopic
let found: string[]|null = [] let found: string[]|null = []
...@@ -418,10 +437,12 @@ function parseRoomTopic(content: string): [string, string] { ...@@ -418,10 +437,12 @@ function parseRoomTopic(content: string): [string, string] {
return [topic, changer] return [topic, changer]
} }
async function checkRoomTopic(m: PuppeteerMessage): Promise<boolean> { async function checkRoomTopic(
this: PuppetPuppeteer,
m: PuppeteerMessage): Promise<boolean> {
let topic, changer let topic, changer
try { try {
[topic, changer] = parseRoomTopic(m.text()) [topic, changer] = parseRoomTopic.call(this, m.text())
} catch (e) { // not found } catch (e) { // not found
return false return false
} }
...@@ -436,7 +457,7 @@ async function checkRoomTopic(m: PuppeteerMessage): Promise<boolean> { ...@@ -436,7 +457,7 @@ async function checkRoomTopic(m: PuppeteerMessage): Promise<boolean> {
let changerContact: PuppeteerContact | null let changerContact: PuppeteerContact | null
if (/^You$/.test(changer) || /^你$/.test(changer)) { if (/^You$/.test(changer) || /^你$/.test(changer)) {
changerContact = PuppeteerContact.load(this.userId) as PuppeteerContact changerContact = this.userSelf()
changerContact.puppet = m.puppet changerContact.puppet = m.puppet
} else { } else {
changerContact = room.member(changer) changerContact = room.member(changer)
......
...@@ -58,15 +58,17 @@ test('Puppet smoke testing', async t => { ...@@ -58,15 +58,17 @@ test('Puppet smoke testing', async t => {
test('login/logout events', sinonTest(async function (t: test.Test) { test('login/logout events', sinonTest(async function (t: test.Test) {
sinon.stub(Contact, 'findAll') const sandbox = sinon.sandbox.create()
sandbox.stub(Contact, 'findAll')
.onFirstCall().resolves([]) .onFirstCall().resolves([])
.onSecondCall().resolves([1]) .onSecondCall().resolves([1])
.resolves([1, 2]) .resolves([1, 2])
sinon.stub(Event, 'onScan') // block the scan event to prevent reset logined user sandbox.stub(Event, 'onScan') // block the scan event to prevent reset logined user
sinon.stub(Bridge.prototype, 'getUserName').resolves('mockedUserName') sandbox.stub(Bridge.prototype, 'getUserName').resolves('mockedUserName')
sinon.stub(PuppetPuppeteer.prototype, 'getContact') .resolves({ sandbox.stub(PuppetPuppeteer.prototype, 'getContact') .resolves({
NickName: 'mockedNickName', NickName: 'mockedNickName',
UserName: 'mockedUserName', UserName: 'mockedUserName',
}) })
...@@ -110,6 +112,7 @@ test('login/logout events', sinonTest(async function (t: test.Test) { ...@@ -110,6 +112,7 @@ test('login/logout events', sinonTest(async function (t: test.Test) {
} catch (e) { } catch (e) {
t.fail(e) t.fail(e)
} finally { } finally {
sandbox.restore()
t.end() t.end()
} }
})) }))
...@@ -24,18 +24,17 @@ import * as sinon from 'sinon' ...@@ -24,18 +24,17 @@ import * as sinon from 'sinon'
import cloneClass from 'clone-class' import cloneClass from 'clone-class'
import Profile from '../profile'
import Wechaty from '../wechaty' // `Wechaty` need to be imported before `Puppet`
import PuppetPuppeteer from './puppet-puppeteer' import PuppetPuppeteer from './puppet-puppeteer'
import PuppeteerContact from './puppeteer-contact' import PuppeteerContact from './puppeteer-contact'
import Profile from '../profile'
import Wechaty from '../wechaty'
test('Contact smoke testing', async t => { test('Contact smoke testing', async t => {
// tslint:disable-next-line:variable-name // tslint:disable-next-line:variable-name
const MyContact = cloneClass(PuppeteerContact) const MyContact = cloneClass(PuppeteerContact)
const puppet = new PuppetPuppeteer({
MyContact.puppet = new PuppetPuppeteer({
profile: new Profile(), profile: new Profile(),
wechaty: new Wechaty(), wechaty: new Wechaty(),
}) })
...@@ -45,19 +44,24 @@ test('Contact smoke testing', async t => { ...@@ -45,19 +44,24 @@ test('Contact smoke testing', async t => {
const NickName = 'NickNameTest' const NickName = 'NickNameTest'
const RemarkName = 'AliasTest' const RemarkName = 'AliasTest'
sinon.stub((MyContact.puppet as PuppetPuppeteer), 'getContact', function(id: string) { const sandbox = sinon.sandbox.create()
sandbox.stub(puppet, 'getContact')
.callsFake(function(id: string) {
return new Promise<any>((resolve, reject) => { return new Promise<any>((resolve, reject) => {
if (id !== UserName) return resolve({}) if (id !== UserName) return resolve({})
setTimeout(() => { setTimeout(() => {
return resolve({ return resolve({
UserName: UserName, UserName: UserName,
NickName: NickName, NickName: NickName,
RemarkName: RemarkName, RemarkName: RemarkName,
}) })
}, 200) }, 10)
}) })
}) })
MyContact.puppet = puppet
const c = new MyContact(UserName) const c = new MyContact(UserName)
t.is(c.id, UserName, 'id/UserName right') t.is(c.id, UserName, 'id/UserName right')
...@@ -66,8 +70,7 @@ test('Contact smoke testing', async t => { ...@@ -66,8 +70,7 @@ test('Contact smoke testing', async t => {
t.is(r.name(), NickName, 'NickName set') t.is(r.name(), NickName, 'NickName set')
t.is(r.alias(), RemarkName, 'should get the right alias from Contact') t.is(r.alias(), RemarkName, 'should get the right alias from Contact')
const s = r.toString() sandbox.restore()
t.is(typeof s, 'string', 'toString()')
// const contact1 = await Contact.find({name: 'NickNameTest'}) // const contact1 = await Contact.find({name: 'NickNameTest'})
// t.is(contact1.id, UserName, 'should find contact by name') // t.is(contact1.id, UserName, 'should find contact by name')
......
...@@ -29,10 +29,11 @@ import Wechaty from '../wechaty' ...@@ -29,10 +29,11 @@ import Wechaty from '../wechaty'
import { import {
Puppet, Puppet,
} from '../puppet/' FriendRequestType,
} from '../puppet/'
import { import {
PuppetMock, PuppetMock,
} from '../puppet-mock/' } from '../puppet-mock/'
import PuppeteerContact from './puppeteer-contact' import PuppeteerContact from './puppeteer-contact'
import PuppeteerMessage from './puppeteer-message' import PuppeteerMessage from './puppeteer-message'
...@@ -55,9 +56,9 @@ test('PuppetPuppeteerFriendRequest.receive smoke testing', async t => { ...@@ -55,9 +56,9 @@ test('PuppetPuppeteerFriendRequest.receive smoke testing', async t => {
fr.receive(rawObj.RecommendInfo) fr.receive(rawObj.RecommendInfo)
t.true(typeof fr.payload === 'object', 'should has info object') t.true(typeof fr.payload === 'object', 'should has info object')
t.is(fr.hello, '我是群聊"Wechaty"的李卓桓.PreAngel', 'should has right request message') t.is(fr.hello(), '我是群聊"Wechaty"的李卓桓.PreAngel', 'should has right request message')
t.true(fr.contact instanceof PuppeteerContact, 'should have a Contact instance') t.true(fr.contact() instanceof PuppeteerContact, 'should have a Contact instance')
t.is(fr.type as any, 'receive', 'should be receive type') t.is(fr.type(), FriendRequestType.RECEIVE, 'should be receive type')
}) })
test('PuppetPuppeteerFriendRequest.confirm smoke testing', async t => { test('PuppetPuppeteerFriendRequest.confirm smoke testing', async t => {
...@@ -89,6 +90,6 @@ test('PuppetPuppeteerFriendRequest.confirm smoke testing', async t => { ...@@ -89,6 +90,6 @@ test('PuppetPuppeteerFriendRequest.confirm smoke testing', async t => {
const contact = m.from() const contact = m.from()
fr.confirm(contact || new MyContact('xx')) fr.confirm(contact || new MyContact('xx'))
t.true(fr.contact instanceof PuppeteerContact, 'should have a Contact instance') t.true(fr.contact() instanceof PuppeteerContact, 'should have a Contact instance')
t.is(fr.type as any, 'confirm', 'should be confirm type') t.is(fr.type(), FriendRequestType.CONFIRM, 'should be confirm type')
}) })
...@@ -35,6 +35,7 @@ import { ...@@ -35,6 +35,7 @@ import {
} from '../config' } from '../config'
import { import {
FriendRequest, FriendRequest,
FriendRequestType,
} from '../puppet/' } from '../puppet/'
import { import {
...@@ -50,6 +51,9 @@ export class PuppeteerFriendRequest extends FriendRequest { ...@@ -50,6 +51,9 @@ export class PuppeteerFriendRequest extends FriendRequest {
public payload: RecommendPayload public payload: RecommendPayload
private ticket: string private ticket: string
private _contact: PuppeteerContact
private _hello: string
private _type: FriendRequestType
constructor() { constructor() {
log.verbose('PuppeteerFriendRequest', 'constructor()') log.verbose('PuppeteerFriendRequest', 'constructor()')
...@@ -67,8 +71,8 @@ export class PuppeteerFriendRequest extends FriendRequest { ...@@ -67,8 +71,8 @@ export class PuppeteerFriendRequest extends FriendRequest {
const contact = PuppeteerContact.load(payload.UserName) const contact = PuppeteerContact.load(payload.UserName)
contact.puppet = this.puppet contact.puppet = this.puppet
this.contact = contact this._contact = contact
this.hello = payload.Content this._hello = payload.Content
this.ticket = payload.Ticket this.ticket = payload.Ticket
// ??? this.nick = info.NickName // ??? this.nick = info.NickName
...@@ -76,7 +80,7 @@ export class PuppeteerFriendRequest extends FriendRequest { ...@@ -76,7 +80,7 @@ export class PuppeteerFriendRequest extends FriendRequest {
throw new Error('ticket not found') throw new Error('ticket not found')
} }
this.type = 'receive' this._type = FriendRequestType.RECEIVE
return return
} }
...@@ -87,8 +91,8 @@ export class PuppeteerFriendRequest extends FriendRequest { ...@@ -87,8 +91,8 @@ export class PuppeteerFriendRequest extends FriendRequest {
if (!contact) { if (!contact) {
throw new Error('contact not found') throw new Error('contact not found')
} }
this.contact = contact this._contact = contact
this.type = 'confirm' this._type = FriendRequestType.CONFIRM
} }
/** /**
...@@ -107,11 +111,11 @@ export class PuppeteerFriendRequest extends FriendRequest { ...@@ -107,11 +111,11 @@ export class PuppeteerFriendRequest extends FriendRequest {
if (!contact) { if (!contact) {
throw new Error('contact not found') throw new Error('contact not found')
} }
this.contact = contact this._contact = contact
this.type = 'send' this._type = FriendRequestType.SEND
if (hello) { if (hello) {
this.hello = hello this._hello = hello
} }
await this.puppet.friendRequestSend(contact, hello) await this.puppet.friendRequestSend(contact, hello)
...@@ -125,11 +129,11 @@ export class PuppeteerFriendRequest extends FriendRequest { ...@@ -125,11 +129,11 @@ export class PuppeteerFriendRequest extends FriendRequest {
public async accept(): Promise<void> { public async accept(): Promise<void> {
log.verbose('FriendRequest', 'accept() %s', this.contact) log.verbose('FriendRequest', 'accept() %s', this.contact)
if (this.type !== 'receive') { if (this._type !== FriendRequestType.RECEIVE) {
throw new Error('request is not a `receive` type. it is a ' + this.type + ' type') throw new Error('request is not a `receive` type. it is a ' + this.type + ' type')
} }
await this.puppet.friendRequestAccept(this.contact, this.ticket) await this.puppet.friendRequestAccept(this.contact(), this.ticket)
const max = 20 const max = 20
const backoff = 300 const backoff = 300
...@@ -143,10 +147,10 @@ export class PuppeteerFriendRequest extends FriendRequest { ...@@ -143,10 +147,10 @@ export class PuppeteerFriendRequest extends FriendRequest {
await retryPromise({ max: max, backoff: backoff }, async (attempt: number) => { await retryPromise({ max: max, backoff: backoff }, async (attempt: number) => {
log.silly('PuppeteerFriendRequest', 'accept() retryPromise() attempt %d with timeout %d', attempt, timeout) log.silly('PuppeteerFriendRequest', 'accept() retryPromise() attempt %d with timeout %d', attempt, timeout)
await this.contact.ready() await this.contact().ready()
if ((this.contact as PuppeteerContact).isReady()) { if (this.contact().isReady()) {
log.verbose('PuppeteerFriendRequest', 'accept() with contact %s ready()', this.contact.name()) log.verbose('PuppeteerFriendRequest', 'accept() with contact %s ready()', this.contact().name())
return return
} }
throw new Error('FriendRequest.accept() content.ready() not ready') throw new Error('FriendRequest.accept() content.ready() not ready')
...@@ -157,6 +161,22 @@ export class PuppeteerFriendRequest extends FriendRequest { ...@@ -157,6 +161,22 @@ export class PuppeteerFriendRequest extends FriendRequest {
} }
public hello(): string {
return this._hello
}
public contact(): PuppeteerContact {
return this._contact
}
public async reject(): Promise<void> {
log.warn('PuppeteerFriendRequest', 'reject() not necessary, NOP.')
return
}
public type(): FriendRequestType {
return this._type
}
} }
export default PuppeteerFriendRequest export default PuppeteerFriendRequest
...@@ -214,9 +214,12 @@ test('mentioned()', async t => { ...@@ -214,9 +214,12 @@ test('mentioned()', async t => {
// puppet1 = { getContact: mockContactGetter } // puppet1 = { getContact: mockContactGetter }
// config.puppetInstance(puppet1) // config.puppetInstance(puppet1)
// } // }
MyContact.puppet = MyRoom.puppet = MyMessage.puppet = { const puppet = MyRoom.puppet = MyMessage.puppet = {
getContact: mockContactGetter, getContact: mockContactGetter,
} as any } as PuppetPuppeteer
MyContact.puppet = puppet
const msg11 = new MyMessage(rawObj11) const msg11 = new MyMessage(rawObj11)
const room11 = msg11.room() const room11 = msg11.room()
if (room11) { if (room11) {
......
...@@ -75,7 +75,7 @@ export class PuppeteerMessage extends Message { ...@@ -75,7 +75,7 @@ export class PuppeteerMessage extends Message {
* @private * @private
*/ */
constructor( constructor(
fileOrObj?: string | MsgRawPayload, fileOrPayload?: string | MsgRawPayload,
) { ) {
super() super()
log.silly('PuppeteerMessage', 'constructor()') log.silly('PuppeteerMessage', 'constructor()')
...@@ -83,14 +83,14 @@ export class PuppeteerMessage extends Message { ...@@ -83,14 +83,14 @@ export class PuppeteerMessage extends Message {
this.obj = {} as MsgPayload this.obj = {} as MsgPayload
// this.rawObj = {} as MsgRawObj // this.rawObj = {} as MsgRawObj
if (!fileOrObj) { if (!fileOrPayload) {
return return
} }
if (typeof fileOrObj === 'string') { if (typeof fileOrPayload === 'string') {
this.parsedPath = path.parse(fileOrObj) this.parsedPath = path.parse(fileOrPayload)
} else if (typeof fileOrObj === 'object') { } else if (typeof fileOrPayload === 'object') {
this.rawObj = fileOrObj this.rawObj = fileOrPayload
this.obj = this.parse(this.rawObj) this.obj = this.parse(this.rawObj)
this.id = this.obj.id this.id = this.obj.id
} else { } else {
...@@ -246,7 +246,7 @@ export class PuppeteerMessage extends Message { ...@@ -246,7 +246,7 @@ export class PuppeteerMessage extends Message {
} }
if (this.obj.room) { if (this.obj.room) {
const r = PuppeteerRoom.load(this.obj.room) as PuppeteerRoom const r = PuppeteerRoom.load(this.obj.room)
r.puppet = this.puppet r.puppet = this.puppet
return r return r
} }
...@@ -506,7 +506,7 @@ export class PuppeteerMessage extends Message { ...@@ -506,7 +506,7 @@ export class PuppeteerMessage extends Message {
) )
if (contactList.length === 0) { if (contactList.length === 0) {
log.warn(`Message`, `message.mentioned() can not found member using room.member() from mentionList, metion string: ${JSON.stringify(mentionList)}`) log.warn('PuppeteerMessage', `message.mentioned() can not found member using room.member() from mentionList, metion string: ${JSON.stringify(mentionList)}`)
} }
return contactList return contactList
} }
......
...@@ -101,7 +101,7 @@ export class PuppeteerRoom extends Room { ...@@ -101,7 +101,7 @@ export class PuppeteerRoom extends Room {
* @private * @private
*/ */
public async ready(): Promise<Room> { public async ready(): Promise<Room> {
log.silly('PuppeteerRoom', 'ready(%s)') log.silly('PuppeteerRoom', 'ready()')
if (!this.id) { if (!this.id) {
const e = new Error('ready() on a un-inited Room') const e = new Error('ready() on a un-inited Room')
log.warn('PuppeteerRoom', e.message) log.warn('PuppeteerRoom', e.message)
...@@ -614,7 +614,7 @@ export class PuppeteerRoom extends Room { ...@@ -614,7 +614,7 @@ export class PuppeteerRoom extends Room {
} }
const filterMap = this.obj[filterMapName] as Map<string, string> const filterMap = this.obj[filterMapName] as Map<string, string>
const idList = Object.keys(filterMap) const idList = Array.from(filterMap.keys())
.filter(id => filterMap.get(id) === filterValue) .filter(id => filterMap.get(id) === filterValue)
log.silly('PuppeteerRoom', 'memberAll() check %s from %s: %s', filterValue, filterKey, JSON.stringify(filterMap)) log.silly('PuppeteerRoom', 'memberAll() check %s from %s: %s', filterValue, filterKey, JSON.stringify(filterMap))
......
...@@ -22,6 +22,12 @@ import PuppetAccessory from '../puppet-accessory' ...@@ -22,6 +22,12 @@ import PuppetAccessory from '../puppet-accessory'
import Contact from './contact' import Contact from './contact'
export enum FriendRequestType {
SEND,
RECEIVE,
CONFIRM,
}
/** /**
* Send, receive friend request, and friend confirmation events. * Send, receive friend request, and friend confirmation events.
* *
...@@ -33,12 +39,14 @@ import Contact from './contact' ...@@ -33,12 +39,14 @@ import Contact from './contact'
*/ */
export abstract class FriendRequest extends PuppetAccessory { export abstract class FriendRequest extends PuppetAccessory {
public contact: Contact
public hello: string
public type: 'send' | 'receive' | 'confirm'
public abstract send(contact: Contact, hello: string): Promise<void> public abstract send(contact: Contact, hello: string): Promise<void>
public abstract accept(): Promise<void> public abstract accept(): Promise<void>
public abstract reject(): Promise<void>
public abstract contact() : Contact
public abstract hello() : string
public abstract type() : FriendRequestType
} }
......
...@@ -5,18 +5,20 @@ export { ...@@ -5,18 +5,20 @@ export {
} from './contact' } from './contact'
export { export {
FriendRequest, FriendRequest,
FriendRequestType,
} from './friend-request' } from './friend-request'
export { export {
Message, Message,
} from './message' } from './message'
export {
Room,
RoomMemberQueryFilter,
RoomQueryFilter,
} from './room'
export { export {
Puppet, Puppet,
PuppetEventName, PuppetEventName,
PuppetOptions, PuppetOptions,
ScanData, ScanData,
} from './puppet' } from './puppet'
export {
Room,
RoomMemberQueryFilter,
RoomQueryFilter,
} from './room'
...@@ -78,11 +78,11 @@ export abstract class Message extends PuppetAccessory implements Sayable { ...@@ -78,11 +78,11 @@ export abstract class Message extends PuppetAccessory implements Sayable {
* @private * @private
*/ */
constructor( constructor(
readonly fileOrObj?: string | Object, readonly fileOrPayload?: string | Object,
) { ) {
super() super()
log.silly('Message', 'constructor(%s) for class %s', log.silly('Message', 'constructor(%s) for child class %s',
fileOrObj || '', fileOrPayload || '',
this.constructor.name, this.constructor.name,
) )
} }
...@@ -197,7 +197,7 @@ export abstract class Message extends PuppetAccessory implements Sayable { ...@@ -197,7 +197,7 @@ export abstract class Message extends PuppetAccessory implements Sayable {
* if (/^ding$/i.test(m.text())) { * if (/^ding$/i.test(m.text())) {
* await m.say('hello world') * await m.say('hello world')
* console.log('Bot REPLY: hello world') * console.log('Bot REPLY: hello world')
* await m.say(new bot.Message.create(__dirname + '/wechaty.png')) * await m.say(new bot.Message(__dirname + '/wechaty.png'))
* console.log('Bot REPLY: Image') * console.log('Bot REPLY: Image')
* } * }
* }) * })
......
...@@ -158,7 +158,7 @@ export abstract class Puppet extends EventEmitter implements Sayable { ...@@ -158,7 +158,7 @@ export abstract class Puppet extends EventEmitter implements Sayable {
public emit(event: 'room-topic', room: Room, topic: string, oldTopic: string, changer: Contact) : boolean public emit(event: 'room-topic', room: Room, topic: string, oldTopic: string, changer: Contact) : boolean
public emit(event: 'scan', url: string, code: number) : boolean public emit(event: 'scan', url: string, code: number) : boolean
public emit(event: 'watchdog', food: WatchdogFood) : boolean public emit(event: 'watchdog', food: WatchdogFood) : boolean
public emit(event: never, ...args: never[]) : never public emit(event: never, ...args: never[]): never
public emit( public emit(
event: PuppetEventName, event: PuppetEventName,
...@@ -216,7 +216,7 @@ export abstract class Puppet extends EventEmitter implements Sayable { ...@@ -216,7 +216,7 @@ export abstract class Puppet extends EventEmitter implements Sayable {
* Login / Logout * Login / Logout
*/ */
public abstract logonoff() : boolean public abstract logonoff() : boolean
public abstract login(user: Contact): Promise<void> // public abstract login(user: Contact): Promise<void>
public abstract logout() : Promise<void> public abstract logout() : Promise<void>
/** /**
......
...@@ -128,6 +128,7 @@ export abstract class Room extends PuppetAccessory implements Sayable { ...@@ -128,6 +128,7 @@ export abstract class Room extends PuppetAccessory implements Sayable {
* @param {RoomQueryFilter} query * @param {RoomQueryFilter} query
* @returns {Promise<Room | null>} If can find the room, return Room, or return null * @returns {Promise<Room | null>} If can find the room, return Room, or return null
*/ */
public static async find<T extends typeof Room>( public static async find<T extends typeof Room>(
this : T, this : T,
query : RoomQueryFilter, query : RoomQueryFilter,
......
...@@ -29,12 +29,12 @@ ...@@ -29,12 +29,12 @@
, "dist/" , "dist/"
] ]
, "include": [ , "include": [
"bin/*.ts" "app/**/*.ts"
, "scripts/**/*.ts" , "bin/*.ts"
, "bot/**/*.ts"
, "examples/**/*.ts" , "examples/**/*.ts"
, "scripts/**/*.ts"
, "src/**/*.ts" , "src/**/*.ts"
, "tests/**/*.spec.ts" , "tests/**/*.spec.ts"
, "bot/**/*.ts"
, "app/**/*.ts"
] ]
} }
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册