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

Refactoring with ContactList & RoomList in Puppet

上级 384317b2
......@@ -37,7 +37,7 @@ export abstract class Accessory extends EventEmitter {
)
if (this._puppet) {
throw new Error('puppet can not be set twice!')
throw new Error('puppet can not be set twice')
}
this._puppet = puppet
}
......@@ -61,6 +61,9 @@ export abstract class Accessory extends EventEmitter {
this.name,
wechaty,
)
if (this._wechaty) {
throw new Error('wechaty can not be set twice')
}
this._wechaty = wechaty
}
......
......@@ -32,7 +32,7 @@ import {
// import Message from './message'
import {
ContantGender,
ContactGender,
ContactPayload,
ContactQueryFilter,
ContactType,
......@@ -51,7 +51,7 @@ export class Contact extends Accessory implements Sayable {
// tslint:disable-next-line:variable-name
public static Type = ContactType
// tslint:disable-next-line:variable-name
public static Gender = ContantGender
public static Gender = ContactGender
protected static [POOL]: Map<string, Contact>
protected static get pool() {
......@@ -450,14 +450,14 @@ export class Contact extends Accessory implements Sayable {
/**
* Contact gender
*
* @returns {ContantGender.Male(2)|Gender.Female(1)|Gender.Unknown(0)}
* @returns {ContactGender.Male(2)|Gender.Female(1)|Gender.Unknown(0)}
* @example
* const gender = contact.gender()
*/
public gender(): ContantGender {
public gender(): ContactGender {
return this.payload
? this.payload.gender
: ContantGender.Unknown
: ContactGender.Unknown
}
/**
......
......@@ -327,11 +327,11 @@ export class Bridge extends EventEmitter {
}
}
public async contactFind(filterFunc: string): Promise<string[]> {
public async contactList(): Promise<string[]> {
try {
return await this.proxyWechaty('contactFind', filterFunc)
return await this.proxyWechaty('contactList')
} catch (e) {
log.error('PuppetPuppeteerBridge', 'contactFind() exception: %s', e.message)
log.error('PuppetPuppeteerBridge', 'contactList() exception: %s', e.message)
throw e
}
}
......
......@@ -577,16 +577,9 @@
: null
}
function contactFind(filterFunction) {
function contactList() {
var contactFactory = WechatyBro.glue.contactFactory
var match
if (!filterFunction) {
match = () => true
} else {
match = eval(filterFunction)
}
return new Promise(resolve => retryFind(0, resolve))
// return
......@@ -595,13 +588,12 @@
function retryFind(attempt, callback) {
attempt = attempt || 0
var contactList = contactFactory
var contactIdList = contactFactory
.getAllFriendContact()
.filter(c => match(c))
.map(c => c.UserName)
if (contactList && contactList.length) {
callback(contactList)
if (contactIdList && contactIdList.length) {
callback(contactIdList)
} else if (attempt > 3) {
callback([])
} else {
......@@ -857,7 +849,7 @@
getCheckUploadUrl,
// for Wechaty Contact Class
contactFind,
contactList,
contactRemark,
// for Wechaty Room Class
......
#!/usr/bin/env ts-node
// tslint:disable:no-shadowed-variable
import * as test from 'blue-tape'
import {
FileBox,
} from 'file-box'
import {
MemoryCard,
} from 'memory-card'
import {
ContactPayload,
ContactType,
ContactGender,
ContactPayloadFilterFactory,
} from '../puppet/schemas/contact'
import {
// FriendRequestPayload,
} from '../puppet/schemas/friend-request'
import {
MessagePayload,
} from '../puppet/schemas/message'
import {
RoomPayload,
RoomPayloadFilterFactory,
} from '../puppet/schemas/room'
import {
Receiver,
} from '../puppet/schemas/puppet'
import {
Puppet,
} from '../puppet/puppet'
class Fixture extends Puppet {
public async start() : Promise<void> { return {} as any }
public async stop() : Promise<void> { return {} as any }
public async ding(data?: any) : Promise<string> { return {} as any }
public async logout(): Promise<void> { return {} as any }
/**
*
* Contact
*
*/
public async contactAlias(contactId: string) : Promise<string>
public async contactAlias(contactId: string, alias: string | null) : Promise<void>
public async contactAlias(contactId: string, alias?: string|null) : Promise<string | void> { return {} as any }
public async contactAvatar(contactId: string) : Promise<FileBox> { return {} as any }
public async contactList() : Promise<string[]> { return {} as any }
public async contactRawPayload(id: string) : Promise<any> { return {} as any }
public async contactRawPayloadParser(rawPayload: any) : Promise<ContactPayload> { return {} as any }
/**
*
* FriendRequest
*
*/
public async friendRequestSend(contactId: string, hello?: string) : Promise<void> { return {} as any }
public async friendRequestAccept(contactId: string, ticket: string) : Promise<void> { return {} as any }
/**
*
* Message
*
*/
public async messageFile(messageId: string) : Promise<FileBox> { return {} as any }
public async messageForward(to: Receiver, messageId: string) : Promise<void> { return {} as any }
public async messageSendText(to: Receiver, text: string) : Promise<void> { return {} as any }
public async messageSendFile(to: Receiver, file: FileBox) : Promise<void> { return {} as any }
public async messageRawPayload(id: string) : Promise<any> { return {} as any }
public async messageRawPayloadParser(rawPayload: any) : Promise<MessagePayload> { return {} as any }
/**
*
* Room
*
*/
public async roomAdd(roomId: string, contactId: string) : Promise<void> { return {} as any }
public async roomCreate(contactIdList: string[], topic?: string) : Promise<string> { return {} as any }
public async roomDel(roomId: string, contactId: string) : Promise<void> { return {} as any }
public async roomQuit(roomId: string) : Promise<void> { return {} as any }
public async roomTopic(roomId: string, topic?: string) : Promise<string | void> { return {} as any }
public async roomList() : Promise<string[]> { return {} as any }
public async roomRawPayload(id: string) : Promise<any> { return {} as any }
public async roomRawPayloadParser(rawPayload: any) : Promise<RoomPayload> { return {} as any }
}
test('contactQueryFilterFunction()', async t => {
const TEXT_REGEX = 'query by regex'
const TEXT_TEXT = 'query by text'
const PAYLOAD_LIST: ContactPayload[] = [
{
id : 'id1',
gender : ContactGender.Unknown,
type : ContactType.Personal,
name : TEXT_REGEX,
alias : TEXT_TEXT,
},
{
id : 'id2',
gender : ContactGender.Unknown,
type : ContactType.Personal,
name : TEXT_TEXT,
alias : TEXT_REGEX,
},
{
id : 'id3',
gender : ContactGender.Unknown,
type : ContactType.Personal,
name : TEXT_REGEX,
alias : TEXT_TEXT,
},
{
id : 'id4',
gender : ContactGender.Unknown,
type : ContactType.Personal,
name : TEXT_TEXT,
alias : TEXT_REGEX,
},
]
const REGEX_VALUE = new RegExp(TEXT_REGEX)
const TEXT_VALUE = TEXT_TEXT
const puppet = new Fixture({ memory: new MemoryCard })
const filterFactory: ContactPayloadFilterFactory = (puppet as any).contactQueryFilterFactory
t.test('filter name by regex', async t => {
const QUERY = { name: REGEX_VALUE }
const ID_LIST = ['id1', 'id3']
const func = filterFactory(QUERY)
const idList = PAYLOAD_LIST.filter(func).map(payload => payload.id)
t.deepEqual(idList, ID_LIST, 'should filter the query to id list')
})
t.test('filter name by text', async t => {
const QUERY = { name: TEXT_VALUE }
const ID_LIST = ['id2', 'id4']
const func = filterFactory(QUERY)
const idList = PAYLOAD_LIST.filter(func).map(payload => payload.id)
t.deepEqual(idList, ID_LIST, 'should filter query to id list')
})
t.test('filter alias by regex', async t => {
const QUERY = { alias: REGEX_VALUE }
const ID_LIST = ['id2', 'id4']
const func = filterFactory(QUERY)
const idList = PAYLOAD_LIST.filter(func).map(payload => payload.id)
t.deepEqual(idList, ID_LIST, 'should filter query to id list')
})
t.test('filter alias by text', async t => {
const QUERY = { alias: TEXT_VALUE }
const ID_LIST = ['id1', 'id3']
const func = filterFactory(QUERY)
const idList = PAYLOAD_LIST.filter(func).map(payload => payload.id)
t.deepEqual(idList, ID_LIST, 'should filter query to id list')
})
t.test('throw if filter key unknown', async t => {
t.throws(() => filterFactory({ xxxx: 'test' } as any), 'should throw')
})
t.test('throw if filter key are more than one', async t => {
t.throws(() => filterFactory({
name: 'test',
alias: 'test',
}), 'should throw')
})
})
test('roomQueryFilterFunction()', async t => {
const TEXT_REGEX = 'query by regex'
const TEXT_TEXT = 'query by text'
const DUMMY = {
memberIdList : {} as any,
nameMap : {} as any,
roomAliasMap : {} as any,
contactAliasMap : {} as any,
}
const PAYLOAD_LIST: RoomPayload[] = [
{
id : 'id1',
topic : TEXT_TEXT,
...DUMMY,
},
{
id : 'id2',
topic : TEXT_REGEX,
...DUMMY,
},
{
id : 'id3',
topic : TEXT_TEXT,
...DUMMY,
},
{
id : 'id4',
topic : TEXT_REGEX,
...DUMMY,
},
]
const REGEX_VALUE = new RegExp(TEXT_REGEX)
const TEXT_VALUE = TEXT_TEXT
const puppet = new Fixture({ memory: new MemoryCard() })
const filterFactory: RoomPayloadFilterFactory = (puppet as any).roomQueryFilterFactory
t.test('filter name by regex', async t => {
const QUERY = { topic: REGEX_VALUE }
const ID_LIST = ['id1', 'id3']
const func = filterFactory(QUERY)
const idList = PAYLOAD_LIST.filter(func).map(payload => payload.id)
t.deepEqual(idList, ID_LIST, 'should filter the query to id list')
})
t.test('filter name by text', async t => {
const QUERY = { topic: TEXT_VALUE }
const ID_LIST = ['id2', 'id4']
const func = filterFactory(QUERY)
const idList = PAYLOAD_LIST.filter(func).map(payload => payload.id)
t.deepEqual(idList, ID_LIST, 'should filter query to id list')
})
t.test('throw if filter key unknown', async t => {
t.throws(() => filterFactory({ xxxx: 'test' } as any), 'should throw')
})
t.test('throw if filter key are more than one', async t => {
t.throws(() => filterFactory({
topic: 'test',
alias: 'test',
} as any), 'should throw')
})
})
......@@ -36,57 +36,36 @@ import {
WatchdogFood,
} from 'watchdog'
import {
WECHATY_EVENT_DICT,
// Wechaty,
} from '../wechaty'
// import {
// PUPPET_EVENT_DICT,
// } from './schemas/puppet'
import {
Sayable,
log,
} from '../config'
import Profile from '../profile'
import {
// Contact,
ContactPayload,
ContactQueryFilter,
} from '../contact'
ContactPayloadFilterFunction,
} from './schemas/contact'
import {
// FriendRequestType,
FriendRequestPayload,
} from '../friend-request'
} from './schemas/friend-request'
import {
// Message,
MessagePayload,
} from '../message'
} from './schemas/message'
import {
// Room,
RoomPayload,
RoomQueryFilter,
} from '../room'
export interface ScanPayload {
code : number, // Code
data? : string, // Image Data URL
url : string, // QR Code URL
}
export const PUPPET_EVENT_DICT = {
...WECHATY_EVENT_DICT,
watchdog: 'tbw',
}
export type PuppetEventName = keyof typeof PUPPET_EVENT_DICT
export interface PuppetOptions {
profile : Profile,
// wechaty : Wechaty,
}
RoomPayloadFilterFunction,
} from './schemas/room'
export interface Receiver {
contactId? : string,
roomId? : string,
}
import {
PuppetEventName,
PuppetOptions,
Receiver,
} from './schemas/puppet'
let PUPPET_COUNTER = 0
......@@ -182,7 +161,7 @@ export abstract class Puppet extends EventEmitter implements Sayable {
}
public toString() {
return `Puppet#${this.counter}<${this.constructor.name}>(${this.options.profile.name})`
return `Puppet#${this.counter}<${this.constructor.name}>(${this.options.memory.name})`
}
// private abstract async emitError(err: string) : Promise<void>
......@@ -211,7 +190,7 @@ export abstract class Puppet extends EventEmitter implements Sayable {
public emit(event: 'error', error: string) : boolean
public emit(event: 'friend', requestId: string) : boolean
public emit(event: 'heartbeat', data: string) : boolean
// public emit(event: 'heartbeat', data: string) : boolean
public emit(event: 'login', contactId: string) : boolean
public emit(event: 'logout', contactId: string) : boolean
public emit(event: 'message', messageId: string) : boolean
......@@ -254,7 +233,7 @@ export abstract class Puppet extends EventEmitter implements Sayable {
public on(event: 'error', listener: (error: string) => void) : this
public on(event: 'friend', listener: (requestId: string) => void) : this
public on(event: 'heartbeat', listener: (data: string) => void) : this
// public on(event: 'heartbeat', listener: (data: string) => void) : this
public on(event: 'login', listener: (contactId: string) => void) : this
public on(event: 'logout', listener: (contactId: string) => void) : this
public on(event: 'message', listener: (messageId: string) => void) : this
......@@ -346,8 +325,12 @@ export abstract class Puppet extends EventEmitter implements Sayable {
}
/**
*
* Login / Logout
*
*/
public abstract async logout(): Promise<void>
public logonoff(): boolean {
if (this.id) {
return true
......@@ -356,7 +339,6 @@ export abstract class Puppet extends EventEmitter implements Sayable {
}
}
public abstract async logout(): Promise<void>
protected async login(userId: string): Promise<void> {
log.verbose('Puppet', 'login(%s)', userId)
......@@ -378,11 +360,68 @@ export abstract class Puppet extends EventEmitter implements Sayable {
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 contactList() : Promise<string[]>
public abstract async contactRawPayload(id: string) : Promise<any>
public abstract async contactRawPayloadParser(rawPayload: any) : Promise<ContactPayload>
public async contactFindAll(query?: ContactQueryFilter): Promise<string[]> {
log.verbose('Puppet', 'contactFindAll(%s)', JSON.stringify(query))
const allContactIdList = await this.contactList()
const allContactPayloadList = await Promise.all(
allContactIdList.map(
id => this.contactPayload(id),
),
)
if (!query) {
return allContactIdList
}
const filterFuncion = this.contactQueryFilterFactory(query)
const idList = allContactPayloadList
.filter(filterFuncion)
.map(payload => payload.id)
return idList
}
private contactQueryFilterFactory(
query: ContactQueryFilter,
): ContactPayloadFilterFunction {
log.verbose('Puppet', 'contactQueryFilterFactory({ %s })',
Object.keys(query)
.map(k => `${k}: ${query[k as keyof ContactQueryFilter]}`)
.join(', '),
)
if (Object.keys(query).length !== 1) {
throw new Error('query only support one key. multi key support is not availble now.')
}
// TypeScript bug: have to set `undefined | string | RegExp` at here, or the later code type check will get error
const filterKey: undefined | string | RegExp = Object.keys(query)[0] as keyof ContactQueryFilter
const filterValue = query[filterKey]
if (!filterValue) {
throw new Error('filterValue not found for filterKey: ' + filterKey)
}
let filterFunction
if (filterValue instanceof RegExp) {
filterFunction = (payload: ContactPayload) => filterValue.test(payload[filterKey])
} else if (typeof filterValue === 'string') {
filterFunction = (payload: ContactPayload) => filterValue === payload[filterKey]
} else {
throw new Error('unsupport filterValue type: ' + typeof filterValue)
}
return filterFunction
}
public async contactPayload(
id: string,
noCache = false,
......@@ -494,13 +533,77 @@ export abstract class Puppet extends EventEmitter implements Sayable {
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 roomList() : Promise<string[]>
public abstract async roomRawPayload(id: string) : Promise<any>
public abstract async roomRawPayloadParser(rawPayload: any) : Promise<RoomPayload>
public async roomMember(name: string): Promise<string[]> {
log.verbose('Puppet', 'roomMember(%s)', name)
return []
}
public async roomFindAll(query?: RoomQueryFilter): Promise<string[]> {
log.verbose('Puppet', 'roomFindAll(%s)', JSON.stringify(query))
const allRoomIdList = await this.roomList()
if (!query) {
return allRoomIdList
}
const roomPayloadList = await Promise.all(
allRoomIdList.map(
id => this.roomPayload(id),
),
)
const filterFunction = this.roomQueryFilterFactory(query)
const roomIdList = roomPayloadList
.filter(filterFunction)
.map(payload => payload.id)
return roomIdList
}
private roomQueryFilterFactory(
query: RoomQueryFilter,
): RoomPayloadFilterFunction {
log.verbose('Puppet', 'roomQueryFilterFactory({ %s })',
Object.keys(query)
.map(k => `${k}: ${query[k as keyof ContactQueryFilter]}`)
.join(', '),
)
if (Object.keys(query).length !== 1) {
throw new Error('query only support one key. multi key support is not availble now.')
}
// TypeScript bug: have to set `undefined | string | RegExp` at here, or the later code type check will get error
const filterKey: undefined | string | RegExp = Object.keys(query)[0] as keyof ContactQueryFilter
const filterValue = query[filterKey]
if (!filterValue) {
throw new Error('filterValue not found for filterKey: ' + filterKey)
}
let filterFunction: RoomPayloadFilterFunction
if (filterValue instanceof RegExp) {
filterFunction = (payload: RoomPayload) => filterValue.test(payload[filterKey])
} else if (typeof filterValue === 'string') {
filterFunction = (payload: RoomPayload) => filterValue === payload[filterKey]
} else {
throw new Error('unsupport filterValue: ' + typeof filterValue)
}
return filterFunction
}
public async roomPayload(
id: string,
noCache = false,
......
export enum ContantGender {
export enum ContactGender {
Unknown = 0,
Male = 1,
Female = 2,
......@@ -17,7 +17,7 @@ export interface ContactQueryFilter {
export interface ContactPayload {
id: string,
gender: ContantGender,
gender: ContactGender,
type: ContactType,
address?: string,
......
......@@ -18,11 +18,12 @@ export const CHAT_EVENT_DICT = {
'room-topic': 'document can be writen at here',
scan : 'document can be writen at here',
}
export type ChatEventName = keyof typeof CHAT_EVENT_DICT
export const PUPPET_EVENT_DICT = {
...CHAT_EVENT_DICT,
error : 'document can be writen at here',
heartbeat : 'document can be writen at here',
// heartbeat : 'document can be writen at here',
start : 'document can be writen at here',
stop : 'document can be writen at here',
watchdog : 'document can be writen at here',
......
......@@ -30,7 +30,12 @@ import {
callerResolve,
hotImport,
} from 'hot-import'
import StateSwitch from 'state-switch'
import {
StateSwitch,
} from 'state-switch'
import {
MemoryCard,
} from 'memory-card'
import {
Accessory,
......@@ -42,14 +47,14 @@ import {
Raven,
Sayable,
} from './config'
import Profile from './profile'
import {
PUPPET_DICT,
PuppetName,
} from './puppet-config'
import {
Puppet, PuppetOptions,
Puppet,
PuppetOptions,
} from './puppet/'
import {
......@@ -65,26 +70,32 @@ import {
Room,
} from './room'
export const WECHAT_EVENT_DICT = {
friend : 'tbw',
login : 'tbw',
logout : 'tbw',
message : 'tbw',
'room-join' : 'tbw',
'room-leave': 'tbw',
'room-topic': 'tbw',
scan : 'tbw',
}
import {
CHAT_EVENT_DICT,
PUPPET_EVENT_DICT,
PuppetEventName,
// ChatEventName,
} from './puppet/schemas/puppet'
// export const WECHAT_EVENT_DICT = {
// friend : 'tbw',
// login : 'tbw',
// logout : 'tbw',
// message : 'tbw',
// 'room-join' : 'tbw',
// 'room-leave': 'tbw',
// 'room-topic': 'tbw',
// scan : 'tbw',
// }
export const WECHATY_EVENT_DICT = {
...WECHAT_EVENT_DICT,
...CHAT_EVENT_DICT,
error : 'tbw',
heartbeat : 'tbw',
start : 'tbw',
stop : 'tbw',
}
export type WechatEventName = keyof typeof WECHAT_EVENT_DICT
export type WechatyEventName = keyof typeof WECHATY_EVENT_DICT
export interface WechatyOptions {
......@@ -109,7 +120,7 @@ export class Wechaty extends Accessory implements Sayable {
*/
private static globalInstance: Wechaty
private profile: Profile
private memory: MemoryCard
/**
* the state
......@@ -169,7 +180,7 @@ export class Wechaty extends Accessory implements Sayable {
? null
: (options.profile || config.default.DEFAULT_PROFILE)
this.profile = new Profile(options.profile)
this.memory = new MemoryCard(options.profile)
this.id = cuid()
......@@ -199,7 +210,7 @@ export class Wechaty extends Accessory implements Sayable {
'Wechaty#',
this.id,
`<${this.options && this.options.puppet || ''}>`,
`(${this.profile && this.profile.name || ''})`,
`(${this.memory && this.memory.name || ''})`,
].join('')
}
......@@ -485,8 +496,7 @@ export class Wechaty extends Accessory implements Sayable {
}
const options: PuppetOptions = {
profile : this.profile,
// wechaty: this,
memory : this.memory,
}
return new MyPuppet(options)
......@@ -510,7 +520,7 @@ export class Wechaty extends Accessory implements Sayable {
}
private initPuppetEventBridge(puppet: Puppet) {
const eventNameList: WechatyEventName[] = Object.keys(WECHATY_EVENT_DICT) as any
const eventNameList: PuppetEventName[] = Object.keys(PUPPET_EVENT_DICT) as any
for (const eventName of eventNameList) {
log.verbose('Wechaty', 'initPuppetEventBridge() puppet.on(%s) registered', eventName)
// /// e as any ??? Maybe this is a bug of TypeScript v2.5.3
......@@ -526,16 +536,21 @@ export class Wechaty extends Accessory implements Sayable {
})
break
case 'heartbeat':
case 'watchdog':
puppet.removeAllListeners('heartbeat')
puppet.on('heartbeat', data => {
puppet.on('watchdog', data => {
/**
* Use `watchdog` event from Puppet to `heartbeat` Wechaty.
*/
this.emit('heartbeat', data)
})
break
case 'start':
case 'stop':
// do not emit 'start'/'stop' again for wechaty
// do not emit 'start'/'stop' again for wechaty:
// because both puppet & wechaty should have their own
// `start`/`stop` event seprately
break
// case 'start':
......@@ -641,6 +656,9 @@ export class Wechaty extends Accessory implements Sayable {
})
break
case 'watchdog':
break
default:
throw new Error('eventName ' + eventName + 'unsupported!')
......@@ -698,7 +716,7 @@ export class Wechaty extends Accessory implements Sayable {
this.state.on('pending')
try {
await this.profile.load()
await this.memory.load()
await this.initPuppet()
await this.puppet.start()
......@@ -736,17 +754,10 @@ export class Wechaty extends Accessory implements Sayable {
}
this.state.off('pending')
let puppet: Puppet
try {
puppet = this.puppet
} catch (e) {
log.warn('Wechaty', 'stop() without this.puppet')
return
}
await this.memory.save()
try {
await puppet.stop()
await this.puppet.stop()
} catch (e) {
log.error('Wechaty', 'stop() exception: %s', e.message)
Raven.captureException(e)
......@@ -757,7 +768,7 @@ export class Wechaty extends Accessory implements Sayable {
// MUST use setImmediate at here(the end of this function),
// because we need to run the micro task registered by the `emit` method
setImmediate(() => puppet.removeAllListeners())
setImmediate(() => this.puppet.removeAllListeners())
}
return
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册