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

store puppet to the class static properti and add guard check (#1217)

上级 fbdc8005
#!/usr/bin/env ts-node
import * as test from 'blue-tape'
// import * as sinon from 'sinon'
import { cloneClass } from 'clone-class'
import { Contact } from './contact'
test('Should not be able to instanciate directly', async t => {
t.throws(() => {
const c = new Contact('xxx')
t.fail(c.name())
}, 'should throw when `new Contact()`')
t.throws(() => {
const c = Contact.load('xxx')
t.fail(c.name())
}, 'should throw when `Contact.load()`')
})
test('Should not be able to instanciate through cloneClass without puppet', async t => {
// tslint:disable-next-line:variable-name
const MyContact = cloneClass(Contact)
t.throws(() => {
const c = new MyContact('xxx')
t.fail(c.name())
}, 'should throw when `new MyContact()` without puppet')
t.throws(() => {
const c = MyContact.load('xxx')
t.fail(c.name())
}, 'should throw when `MyContact.load()` without puppet')
})
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()`')
t.doesNotThrow(() => {
const c = MyContact.load('xxx')
t.ok(c, 'should get contact instance from `MyContact.load()`')
}, 'should not throw when `MyContact.load()`')
})
......@@ -18,6 +18,7 @@
* @ignore
*/
import { FileBox } from 'file-box'
import { instanceToClass } from 'clone-class'
import {
log,
......@@ -199,6 +200,17 @@ export class Contact extends PuppetAccessory implements Sayable {
) {
super()
log.silly('Contact', `constructor(${id})`)
// tslint:disable-next-line:variable-name
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')
}
if (!this.puppet) {
throw new Error('Contact class can not be instanciated without a puppet!')
}
}
/**
......@@ -255,8 +267,6 @@ export class Contact extends PuppetAccessory implements Sayable {
throw new Error('unsupported')
}
msg.puppet = this.puppet
log.silly('Contact', 'say() from: %s to: %s content: %s',
this.puppet.userSelf(),
this,
......
......@@ -21,9 +21,11 @@
/* tslint:disable:no-var-requires */
const retryPromise = require('retry-promise').default
import PuppetAccessory from './puppet-accessory'
import { instanceToClass } from 'clone-class'
import Contact from './contact'
import { PuppetAccessory } from './puppet-accessory'
import { Contact } from './contact'
import {
log,
......@@ -121,6 +123,17 @@ export class FriendRequest extends PuppetAccessory {
) {
super()
log.verbose('PuppeteerFriendRequest', 'constructor(%s)', payload)
// tslint:disable-next-line:variable-name
const MyClass = instanceToClass(this, FriendRequest)
if (MyClass === FriendRequest) {
throw new Error('FriendRequest class can not be instanciated directly! See: https://github.com/Chatie/wechaty/issues/1217')
}
if (!this.puppet) {
throw new Error('FriendRequest class can not be instanciated without a puppet!')
}
}
public async send(): Promise<void> {
......
......@@ -21,7 +21,10 @@ import * as cuid from 'cuid'
import {
FileBox,
} from 'file-box'
} from 'file-box'
import {
instanceToClass,
} from 'clone-class'
import {
log,
......@@ -207,6 +210,17 @@ export class Message extends PuppetAccessory implements Sayable {
)
log.silly('Message', 'constructor()')
// tslint:disable-next-line:variable-name
const MyClass = instanceToClass(this, Message)
if (MyClass === Message) {
throw new Error('Message class can not be instanciated directly! See: https://github.com/Chatie/wechaty/issues/1217')
}
if (!this.puppet) {
throw new Error('Message class can not be instanciated without a puppet!')
}
// default set to MT because there's a id param
this.direction = MessageDirection.MT
}
......@@ -408,7 +422,7 @@ export class Message extends PuppetAccessory implements Sayable {
/**
* File Message
*/
const msg = Message.createMO({
const msg = this.puppet.Message.createMO({
file : textOrFile,
to : from,
room,
......@@ -429,7 +443,7 @@ export class Message extends PuppetAccessory implements Sayable {
/**
* 1. to Individual
*/
msg = Message.createMO({
msg = this.puppet.Message.createMO({
to,
text,
})
......@@ -443,7 +457,7 @@ export class Message extends PuppetAccessory implements Sayable {
*/
const mentionContact = mentionList[0]
const textMentionList = mentionList.map(c => '@' + c.name()).join(' ')
msg = Message.createMO({
msg = this.puppet.Message.createMO({
to: mentionContact,
room,
text: textMentionList + ' ' + text,
......@@ -452,14 +466,13 @@ export class Message extends PuppetAccessory implements Sayable {
/**
* 2.2 did not mention anyone
*/
msg = Message.createMO({
msg = this.puppet.Message.createMO({
to,
room,
text,
})
}
}
msg.puppet = this.puppet
await this.puppet.messageSend(msg)
}
......
......@@ -25,7 +25,6 @@ import {
import {
Message,
MessagePayload,
MessageDirection,
} from '../message'
import {
Contact,
......@@ -93,8 +92,8 @@ export class PuppetMock extends Puppet {
// await some tasks...
this.state.on(true)
const user = Contact.load('logined_user_id')
const msg = Message.createMT('mock_id')
const user = this.Contact.load('logined_user_id')
const msg = this.Message.createMT('mock_id')
this.user = user
this.emit('login', user)
......@@ -200,11 +199,11 @@ export class PuppetMock extends Puppet {
log.verbose('PuppetMock', 'messagePayload(%s)', rawPayload)
const payload: MessagePayload = {
date : new Date(),
direction : MessageDirection.MT,
from : Contact.load('xxx'),
direction : this.Message.Direction.MT,
from : this.Contact.load('xxx'),
text : 'mock message text',
to : this.userSelf(),
type : Message.Type.Text,
type : this.Message.Type.Text,
}
return payload
}
......@@ -287,8 +286,7 @@ export class PuppetMock extends Puppet {
if (!contactList || ! contactList.map) {
throw new Error('contactList not found')
}
const r = Room.load('mock room id') as Room
r.puppet = this
const r = this.Room.load('mock room id') as Room
return r
}
......
......@@ -141,8 +141,7 @@ async function onLogin(
log.silly('PuppetPuppeteerEvent', 'bridge.getUserName: %s', userId)
const user = Contact.load(userId)
user.puppet = this
const user = this.Contact.load(userId)
await user.ready()
log.silly('PuppetPuppeteerEvent', `onLogin() user ${user.name()} logined`)
......@@ -191,8 +190,7 @@ async function onMessage(
this : PuppetPuppeteer,
rawPayload : WebMessageRawPayload,
): Promise<void> {
let msg = Message.createMT(rawPayload.MsgId)
msg.puppet = this
let msg = this.Message.createMT(rawPayload.MsgId)
try {
await msg.ready()
......@@ -234,14 +232,13 @@ async function onMessage(
case WebMsgType.MICROVIDEO:
case WebMsgType.APP:
log.verbose('PuppetPuppeteerEvent', 'onMessage() EMOTICON/IMAGE/VIDEO/VOICE/MICROVIDEO message')
msg = Message.createMT(rawPayload.MsgId)
msg.puppet = this
msg = this.Message.createMT(rawPayload.MsgId)
break
case WebMsgType.TEXT:
if (rawPayload.SubMsgType === WebMsgType.LOCATION) {
log.verbose('PuppetPuppeteerEvent', 'onMessage() (TEXT&LOCATION) message')
msg = Message.createMT(rawPayload.MsgId)
msg = this.Message.createMT(rawPayload.MsgId)
}
break
}
......
......@@ -127,7 +127,7 @@ async function checkFriendRequest(
throw new Error('no recommendInfo')
}
const contact = Contact.load(recommendInfo.UserName)
const contact = this.Contact.load(recommendInfo.UserName)
contact.puppet = this
const hello = recommendInfo.Content
......@@ -138,12 +138,11 @@ async function checkFriendRequest(
log.warn('PuppetPuppeteerFirer', 'fireFriendConfirm() contact still not ready after `ready()` call')
}
const receivedRequest = FriendRequest.createReceive(
const receivedRequest = this.FriendRequest.createReceive(
contact,
hello,
ticket,
)
receivedRequest.puppet = this
this.emit('friend', receivedRequest)
}
......@@ -179,10 +178,9 @@ async function checkFriendConfirm(
const contact = m.from()
const confirmedRequest = FriendRequest.createConfirm(
const confirmedRequest = this.FriendRequest.createConfirm(
contact,
)
confirmedRequest.puppet = m.puppet
await contact.ready()
if (!contact.isReady()) {
......@@ -416,8 +414,7 @@ async function checkRoomLeave(
// }
} else {
removerContact = Contact.load(this.userSelf().id)
removerContact.puppet = m.puppet
removerContact = this.userSelf()
// not sure which is better
// leaverContact = room.member({contactAlias: remover}) || room.member({name: leaver})
......@@ -483,7 +480,6 @@ async function checkRoomTopic(
let changerContact: Contact | null
if (/^You$/.test(changer) || /^你$/.test(changer)) {
changerContact = this.userSelf()
changerContact.puppet = m.puppet
} else {
changerContact = room.member(changer)
}
......
......@@ -81,7 +81,7 @@ test('login/logout events', sinonTest(async function (t: test.Test) {
})
t.ok(pw, 'should instantiated a PuppetPuppeteer')
// config.puppetInstance(pw)
// FIXME: do not modify global instance
Contact.puppet = pw
await pw.start()
......
......@@ -309,7 +309,7 @@ export class PuppetPuppeteer extends Puppet {
public async messageRawPayloadParser(
rawPayload: WebMessageRawPayload,
): Promise<MessagePayload> {
const from: Contact = Contact.load(rawPayload.MMActualSender) // MMPeerUserName
const from: Contact = this.Contact.load(rawPayload.MMActualSender) // MMPeerUserName
const text: string = rawPayload.MMActualContent // Content has @id prefix added by wx
const date: Date = new Date(rawPayload.MMDisplayTime) // Javascript timestamp of milliseconds
......@@ -319,9 +319,9 @@ export class PuppetPuppeteer extends Puppet {
// FIXME: has there any better method to know the room ID?
if (rawPayload.MMIsChatRoom) {
if (/^@@/.test(rawPayload.FromUserName)) {
room = Room.load(rawPayload.FromUserName) // MMPeerUserName always eq FromUserName ?
room = this.Room.load(rawPayload.FromUserName) // MMPeerUserName always eq FromUserName ?
} else if (/^@@/.test(rawPayload.ToUserName)) {
room = Room.load(rawPayload.ToUserName)
room = this.Room.load(rawPayload.ToUserName)
} else {
throw new Error('parse found a room message, but neither FromUserName nor ToUserName is a room(/^@@/)')
}
......@@ -330,7 +330,7 @@ export class PuppetPuppeteer extends Puppet {
if (rawPayload.ToUserName) {
if (!/^@@/.test(rawPayload.ToUserName)) { // if a message in room without any specific receiver, then it will set to be `undefined`
to = Contact.load(rawPayload.ToUserName)
to = this.Contact.load(rawPayload.ToUserName)
}
}
......@@ -737,8 +737,7 @@ export class PuppetPuppeteer extends Puppet {
try {
const idList = await this.bridge.contactFind(filterFunc)
return idList.map(id => {
const c = Contact.load(id) as Contact
c.puppet = this
const c = this.Contact.load(id)
return c
})
} catch (e) {
......@@ -801,11 +800,7 @@ export class PuppetPuppeteer extends Puppet {
// console.log(rawPayload)
const memberList = (rawPayload.MemberList || [])
.map(m => {
const c = Contact.load(m.UserName)
c.puppet = this
return c
})
.map(m => this.Contact.load(m.UserName))
await Promise.all(memberList.map(c => c.ready()))
const nameMap = this.roomParseMap('name' , rawPayload.MemberList)
......@@ -841,8 +836,7 @@ export class PuppetPuppeteer extends Puppet {
memberList.forEach(member => {
let tmpName: string
// console.log(member)
const contact = Contact.load(member.UserName)
contact.puppet = this
const contact = this.Contact.load(member.UserName)
// contact.ready().then(() => console.log('###############', contact.name()))
// console.log(contact)
// log.silly('PuppetPuppeteer', 'roomParseMap() memberList.forEach(contact=%s)', contact)
......@@ -897,8 +891,7 @@ export class PuppetPuppeteer extends Puppet {
try {
const idList = await this.bridge.roomFind(filterFunction)
return idList.map(id => {
const r = Room.load(id) as Room
r.puppet = this
const r = this.Room.load(id) as Room
return r
})
} catch (e) {
......@@ -959,8 +952,7 @@ export class PuppetPuppeteer extends Puppet {
if (!roomId) {
throw new Error('PuppetPuppeteer.roomCreate() roomId "' + roomId + '" not found')
}
const r = Room.load(roomId) as Room
r.puppet = this
const r = this.Room.load(roomId) as Room
return r
} catch (e) {
......@@ -1013,14 +1005,10 @@ export class PuppetPuppeteer extends Puppet {
log.verbose('PuppetPuppeteer', 'readyStable()')
let counter = -1
// tslint:disable-next-line:variable-name
const MyContact = cloneClass(Contact)
MyContact.puppet = this
async function stable(done: Function): Promise<void> {
const stable = async (done: Function): Promise<void> => {
log.silly('PuppetPuppeteer', 'readyStable() stable() counter=%d', counter)
const contactList = await MyContact.findAll()
const contactList = await this.Contact.findAll()
if (counter === contactList.length) {
log.verbose('PuppetPuppeteer', 'readyStable() stable() READY counter=%d', counter)
return done()
......
......@@ -80,18 +80,6 @@ export const PUPPET_EVENT_DICT = {
export type PuppetEventName = keyof typeof PUPPET_EVENT_DICT
// export type PuppetContact = typeof Contact & Constructor<Contact>
// export type PuppetFriendRequest = typeof FriendRequest & Constructor<FriendRequest>
// export type PuppetMessage = typeof Message & Constructor<Message>
// export type PuppetRoom = typeof Room & Constructor<Room>
// export interface PuppetClasses {
// Contact: PuppetContact,
// FriendRequest: PuppetFriendRequest,
// Message: PuppetMessage,
// Room: PuppetRoom,
// }
export interface PuppetOptions {
profile: Profile,
wechaty: Wechaty,
......@@ -108,6 +96,15 @@ export abstract class Puppet extends EventEmitter implements Sayable {
protected user?: Contact
/* tslint:disable:variable-name */
public readonly Contact : typeof Contact
/* tslint:disable:variable-name */
public readonly FriendRequest : typeof FriendRequest
/* tslint:disable:variable-name */
public readonly Message : typeof Message
/* tslint:disable:variable-name */
public readonly Room : typeof Room
/**
* childPkg stores the `package.json` that the NPM module who extends the `Puppet`
*/
......@@ -124,23 +121,10 @@ export abstract class Puppet extends EventEmitter implements Sayable {
this.state = new StateSwitch(this.constructor.name, log)
this.watchdog = new Watchdog(WATCHDOG_TIMEOUT, 'Puppet')
/**
* 1. Check Classes for inherience correctly
*/
// if (!classes) {
// throw new Error('no classes found')
// }
// https://stackoverflow.com/questions/14486110/how-to-check-if-a-javascript-class-inherits-another-without-creating-an-obj
// const check = classes.Contact.prototype instanceof Contact
// && classes.FriendRequest.prototype instanceof FriendRequest
// && classes.Message.prototype instanceof Message
// && classes.Room.prototype instanceof Room
// if (!check) {
// throw new Error('Puppet must set classes right! https://github.com/Chatie/wechaty/issues/1167')
// }
// this.classes = classes
this.Contact = this.options.wechaty.Contact
this.FriendRequest = this.options.wechaty.FriendRequest
this.Message = this.options.wechaty.Message
this.Room = this.options.wechaty.Room
/**
* 2. Load the package.json for Puppet Plugin version range matching
......@@ -257,12 +241,12 @@ export abstract class Puppet extends EventEmitter implements Sayable {
let msg: Message
if (typeof textOrFile === 'string') {
msg = Message.createMO({
msg = this.Message.createMO({
text : textOrFile,
to : this.userSelf(),
})
} else if (textOrFile instanceof FileBox) {
msg = Message.createMO({
msg = this.Message.createMO({
file: textOrFile,
to: this.userSelf(),
})
......@@ -270,7 +254,6 @@ export abstract class Puppet extends EventEmitter implements Sayable {
throw new Error('say() arg unknown')
}
msg.puppet = this
await this.messageSend(msg)
}
......
......@@ -19,7 +19,12 @@
*/
import * as util from 'util'
import { FileBox } from 'file-box'
import {
FileBox,
} from 'file-box'
import {
instanceToClass,
} from 'clone-class'
import {
// config,
......@@ -197,6 +202,18 @@ export class Room extends PuppetAccessory implements Sayable {
) {
super()
log.silly('Room', `constructor(${id})`)
// tslint:disable-next-line:variable-name
const MyClass = instanceToClass(this, Room)
if (MyClass === Room) {
throw new Error('Room class can not be instanciated directly! See: https://github.com/Chatie/wechaty/issues/1217')
}
if (!this.puppet) {
throw new Error('Room class can not be instanciated without a puppet!')
}
}
/**
......@@ -295,13 +312,13 @@ export class Room extends PuppetAccessory implements Sayable {
} else {
text = textOrFile
}
msg = Message.createMO({
msg = this.puppet.Message.createMO({
room : this,
to : replyToList[0], // FIXME: is this right?
text,
})
} else if (textOrFile instanceof FileBox) {
msg = Message.createMO({
msg = this.puppet.Message.createMO({
room: this,
to: replyToList[0],
file: textOrFile,
......@@ -310,7 +327,6 @@ export class Room extends PuppetAccessory implements Sayable {
throw new Error('arg unsupported')
}
msg.puppet = this.puppet
await this.puppet.messageSend(msg)
}
......@@ -652,11 +668,7 @@ export class Room extends PuppetAccessory implements Sayable {
log.silly('Room', 'memberAll() check %s from %s: %s', filterValue, filterKey, JSON.stringify(filterMap))
if (idList.length) {
return idList.map(id => {
const c = Contact.load(id)
c.puppet = this.puppet
return c
})
return idList.map(id => this.puppet.Contact.load(id))
} else {
return []
}
......
......@@ -85,8 +85,8 @@ export type WechatEventName = keyof typeof WECHAT_EVENT_DICT
export type WechatyEventName = keyof typeof WECHATY_EVENT_DICT
export interface WechatyOptions {
puppet?: PuppetName | Puppet,
profile?: string | null,
puppet : PuppetName | Puppet,
profile : null | string,
}
/**
......@@ -118,16 +118,16 @@ export class Wechaty extends PuppetAccessory implements Sayable {
* the cuid
* @private
*/
public readonly cuid: string
public readonly cuid : string
// tslint:disable-next-line:variable-name
public Contact : typeof Contact
public readonly Contact : typeof Contact
// tslint:disable-next-line:variable-name
public FriendRequest : typeof FriendRequest
public readonly FriendRequest : typeof FriendRequest
// tslint:disable-next-line:variable-name
public Message : typeof Message
public readonly Message : typeof Message
// tslint:disable-next-line:variable-name
public Room : typeof Room
public readonly Room : typeof Room
/**
* get the singleton instance of Wechaty
......@@ -157,7 +157,7 @@ export class Wechaty extends PuppetAccessory implements Sayable {
* @public
*/
constructor(
private options: WechatyOptions = {},
private options: WechatyOptions = {} as any,
) {
super()
log.verbose('Wechaty', 'contructor()')
......@@ -171,6 +171,19 @@ export class Wechaty extends PuppetAccessory implements Sayable {
this.profile = new Profile(options.profile)
this.cuid = cuid()
/**
* Clone Classes for this bot and attach the `puppet` to the Class
*
* https://stackoverflow.com/questions/36886082/abstract-constructor-type-in-typescript
* https://github.com/Microsoft/TypeScript/issues/5843#issuecomment-290972055
* https://github.com/Microsoft/TypeScript/issues/19197
*/
// TODO: make Message & Room constructor private???
this.Contact = cloneClass(Contact)
this.FriendRequest = cloneClass(FriendRequest)
this.Message = cloneClass(Message)
this.Room = cloneClass(Room)
}
/**
......@@ -375,83 +388,72 @@ export class Wechaty extends PuppetAccessory implements Sayable {
*/
public initPuppet(): void {
log.verbose('Wechaty', 'initPuppet()')
let puppet: Puppet
/**
* 1. Init the Puppet
*/
if (typeof this.options.puppet === 'string') {
const puppet = this.initPuppetResolver(this.options.puppet)
if (!this.initPuppetSemverSatisfy(
puppet.wechatyVersionRange(),
)) {
throw new Error(`The Puppet Plugin(${puppet.constructor.name}) `
+ `requires a version range(${puppet.wechatyVersionRange()}) `
+ `that is not satisfying the Wechaty version: ${this.version()}.`,
)
}
this.initPuppetEventBridge(puppet)
this.initPuppetAccessory(puppet)
}
/**
* Init the Puppet
*/
private initPuppetResolver(puppet: PuppetName | Puppet): Puppet {
log.verbose('Wechaty', 'initPuppetResolver(%s)', puppet)
if (typeof puppet === 'string') {
// tslint:disable-next-line:variable-name
const MyPuppet = PUPPET_DICT[this.options.puppet]
const MyPuppet = PUPPET_DICT[puppet]
if (!MyPuppet) {
throw new Error('no such puppet: ' + puppet)
}
const options = {
profile: this.profile,
wechaty: this,
}
puppet = new MyPuppet(options)
return new MyPuppet(options)
} else if (this.options.puppet instanceof Puppet) {
puppet = this.options.puppet
} else if (puppet instanceof Puppet) {
return puppet
} else {
throw new Error('unsupported options.puppet!')
}
}
/**
* 2. Plugin Version Range Check
*/
if (!semver.satisfies(
/**
* Plugin Version Range Check
*/
private initPuppetSemverSatisfy(versionRange: string) {
log.verbose('Wechaty', 'initPuppet(%s)', versionRange)
return semver.satisfies(
this.version(true),
puppet.wechatyVersionRange(),
)) {
throw new Error(`The Puppet Plugin(${puppet.constructor.name}) `
+ `requires a version range(${puppet.wechatyVersionRange()}) `
+ `that is not satisfying the Wechaty version: ${this.version()}.`)
}
versionRange,
)
}
private initPuppetEventBridge(puppet: Puppet) {
for (const event of Object.keys(WECHATY_EVENT_DICT)) {
log.verbose('Wechaty', 'initPuppet() puppet.on(%s) registered', event)
log.verbose('Wechaty', 'initPuppetEventBridge() puppet.on(%s) registered', event)
/// e as any ??? Maybe this is a bug of TypeScript v2.5.3
puppet.on(event as any, (...args: any[]) => {
this.emit(event, ...args)
})
}
}
/**
* When `this` is the global instance of Wechaty (`Wechaty.instance()`)
* so we can keep using `Contact.find()` and `Room.find()`
*
* This workaround should be removed after v0.18
*
* See: fix the breaking changes for #518
* https://github.com/Chatie/wechaty/issues/518
*/
if (this === Wechaty.globalInstance) {
Contact.puppet = puppet
FriendRequest.puppet = puppet
Message.puppet = puppet
Room.puppet = puppet
instanceToClass(this, PuppetAccessory).puppet = puppet
}
/**
* Clone Classes for this bot and attach the `puppet` to the Class
*
* Fixme:
* https://stackoverflow.com/questions/36886082/abstract-constructor-type-in-typescript
* https://github.com/Microsoft/TypeScript/issues/5843#issuecomment-290972055
* https://github.com/Microsoft/TypeScript/issues/19197
*/
// this.Contact = cloneClass(puppet.classes.Contact)
// this.FriendRequest = cloneClass(puppet.classes.FriendRequest)
// this.Message = cloneClass(puppet.classes.Message)
// this.Room = cloneClass(puppet.classes.Room)
// TODO: make Message & Room constructor private???
this.Contact = cloneClass(Contact)
this.FriendRequest = cloneClass(FriendRequest)
this.Message = cloneClass(Message)
this.Room = cloneClass(Room)
private initPuppetAccessory(puppet: Puppet) {
log.verbose('Wechaty', 'initPuppetAccessory(%s)', puppet)
this.Contact.puppet = puppet
this.FriendRequest.puppet = puppet
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册