wechaty-puppet-wechat4u
Version:
Wechat4u Puppet for Wechaty
1,173 lines (996 loc) • 36.6 kB
text/typescript
/**
* Wechaty - https://github.com/chatie/wechaty
*
* @copyright 2016-2018 Huan LI <zixia@zixia.net>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import Wechat4u from 'wechat4u'
import QuickLru from '@alloc/quick-lru'
import * as PUPPET from 'wechaty-puppet'
import { log } from 'wechaty-puppet'
import type { FileBoxInterface } from 'file-box'
import { FileBox } from 'file-box'
import { GError } from 'gerror'
import {
qrCodeForChatie,
retry,
VERSION,
NAME,
} from './config.js'
import {
WebContactRawPayload,
WebMessageRawPayload,
WebMessageType,
WebRoomRawMember,
WebRoomRawPayload,
} from './web-schemas.js'
import { parseEvent, EventType } from './wechat4u/events/mod.js'
import { webMessageToWechaty } from './wechat4u/schema-mapper/message.js'
// 解析小程序数据格式
import { parseMiniProgramMessagePayload } from './wechat4u/messages/message-miniprogram.js'
// 解析appmsg 数据格式
import { parseAppmsgMessagePayload } from './wechat4u/messages/message-appmsg.js'
// 解析表情数据格式
import { parseEmotionMessagePayload } from './wechat4u/messages/message-emotion.js'
import { wechat4uContactToWechaty } from './wechat4u/schema-mapper/contact.js'
import { wechat4uRoomMemberToWechaty, wechat4uRoomToWechaty } from './wechat4u/schema-mapper/room.js'
import { isRoomId } from './wechat4u/utils/is-type.js'
const MEMORY_SLOT_NAME = 'PUPPET-WECHAT4U'
export class PuppetWechat4u extends PUPPET.Puppet {
static override readonly VERSION = VERSION
/**
* Wecaht4u
*
* Code from:
* https://github.com/nodeWechat/wechat4u/blob/46931e78bcb56899b8d2a42a37b919e7feaebbef/run-core.js
*
*/
private wechat4u?: any
private scanQrCode?: string
// 启动时间 为了处理历史消息
private startTime: number = 0
private unknownContactId: string[][] = []
private getContactInterval: undefined | NodeJS.Timeout
private _heartBeatTimer?: ReturnType<typeof setTimeout>
private readonly cacheMessageRawPayload: QuickLru<string, WebMessageRawPayload>
constructor (
override options: PUPPET.PuppetOptions = {},
) {
super(options)
const lruOptions: QuickLru.Options<string, any> = {
maxAge: 1000 * 60 * 60,
maxSize: 10000,
onEviction (key: string, val: object) {
log.silly('PuppetWechat4u', 'constructor() lruOptions.dispose(%s, %s)', key, JSON.stringify(val))
},
}
this.cacheMessageRawPayload = new QuickLru<string, WebMessageRawPayload>(lruOptions)
}
override version () { return `${VERSION}<${super.version()}>` }
override name () { return `${NAME}<${super.name()}>` }
override async onStart (): Promise<void> {
log.verbose('PuppetWechat4u', 'onStart() with %s', this.memory.name || 'NONAME')
if (this.wechat4u) {
log.warn('PuppetWechat4u', 'onStart() wechat4u exist, will be overwrited')
}
this.startTime = parseInt(String(new Date().getTime() / 1000))
/**
* Huan(202110): rename `onStart()` to `tryStart()`
* then we will be able to use `MemoryMixin`
* to init MemoryCard for the child puppet
*/
try {
await this.memory.load()
} catch (_) {}
// console.info('faint 1')
const syncData = await this.memory.get(MEMORY_SLOT_NAME)
// console.info('faint 2')
if (syncData) {
this.wechat4u = new Wechat4u(syncData)
} else {
this.wechat4u = new Wechat4u()
}
this.monkeyPatch(this.wechat4u)
this.initHookEvents(this.wechat4u)
/**
* Should not `await` onStart/restart for wechat4u
* because it will blocks...
*/
if (this.wechat4u.PROP.uin) {
// 存在登录数据时,可以随时调用restart进行重启
this.wechat4u.restart()
} else {
this.wechat4u.start()
}
}
/**
* At present, if a user information that does not exist is found, it will be called once.
* If it is a group message, it will send a lot of information requests, and finally most of the interface requests will fail.
* At present, the method of timer is used to regularly obtain user information
* 1、A timer is started when the search request is triggered for the first time
* 2、All requested unknown user ids will be stored in unknownContactId
* 3、The timer will be executed once every 500ms, each time fetching 50 pieces of data in unknownContactId
* 4、If the data of unknownContactId is empty, the timer will be cleared and wait for the next establishment
* @private
*/
private getContactsInfo () {
const tempArray: string[][] = this.unknownContactId.splice(0, 40)
if (tempArray.length === 0 && this.getContactInterval) {
clearInterval(this.getContactInterval)
this.getContactInterval = undefined
}
if (tempArray.length) {
const userDataList = tempArray.map(contact => {
return {
EncryChatRoomId : contact[1],
UserName : contact[0],
}
})
this.wechat4u.batchGetContact(userDataList).then((result: any[]) => {
result.forEach((item) => {
if (isRoomId(item.UserName)) {
const membersList = item.MemberList.map((mItem: any) => {
return {
...mItem,
EncryChatRoomId: item.UserName,
}
})
this.wechat4u.updateContacts(membersList)
}
})
this.wechat4u.updateContacts(result)
return null
}).catch((e: any) => {
log.warn('PuppetWechat4u', 'contactRawPayload(%s) wechat4u.batchGetContact() exception: %s', e)
})
}
}
private monkeyPatch (wechat4u: any) {
log.silly('PuppetWechat4u', 'monkeyPatch()')
// fake wechat4u to think as we had logined.)
this.monkeyPatchOffState(wechat4u, 'checkLogin', Promise.resolve({ code: 200 }))
this.monkeyPatchOffState(wechat4u, 'login', Promise.resolve())
this.monkeyPatchOffState(wechat4u, '_init', Promise.resolve())
this._startPuppetHeart(true, wechat4u)
/**
* Disable Wechat4u for Sending Message to Filehelper when Heartbeat.
*/
// tslint:disable-next-line
wechat4u.setPollingTargetGetter(() => {
return ''
})
wechat4u.setPollingMessageGetter(() => {
return ''
})
// 自定义心跳间隔(以毫秒为单位)
// 25 days: https://stackoverflow.com/a/12633556/1123955
// this.wechat4u.setPollingIntervalGetter(() => Math.pow(2,31) - 1)
}
// 开始监听心跳
private _startPuppetHeart (firstTime: boolean = true, wechat4u: any) {
if (firstTime && this._heartBeatTimer) {
return
}
let status: string|undefined = wechat4u.state
if (status === wechat4u.CONF.STATE.login) {
status = 'normal'
} else if (status === wechat4u.CONF.STATE.logout) {
status = 'logout'
} else if (status === wechat4u.CONF.STATE.init) {
status = 'init'
} else if (status === wechat4u.CONF.STATE.uuid) {
status = 'uuid'
}
this.emit('heartbeat', { data: `heartbeat@puppet-wechat4u:${status}` })
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this._heartBeatTimer = setTimeout(async (): Promise<void> => {
await this._startPuppetHeart(false, wechat4u)
return undefined
}, 15 * 1000) // 15s
}
/**
* Monkey Patch for Wechat4u
* - https://www.audero.it/blog/2016/12/05/monkey-patching-javascript/#what-is-monkey-patching
*
* What is Monkey patching?
* Monkey patching is a technique to add, modify, or suppress
* the default behavior of a piece of code at runtime
* without changing its original source code.
*/
private monkeyPatchOffState (wechat4u: any, func: string, valueWhenLogouted: any): void {
log.verbose('PuppetWechat4u', 'monkeyPatchOffState(wechat4u, %s)', func)
const puppetThis = this
const funcOrig = wechat4u[func]
function funcNew (this: any) {
log.verbose('PuppetWechat4u', 'monkeyPatchOffState(%s) funcNew()', func)
if (puppetThis.state.inactive()) {
log.verbose('PuppetWechat4u', 'monkeyPatchOffState(%s) funcNew() state.off() is true, return', func)
return valueWhenLogouted
}
return funcOrig.call(this)
}
wechat4u[func] = funcNew
}
/**
* @private
* For issue https://github.com/wechaty/puppet-wechat/issues/107
*/
private async waitStable (): Promise<void> {
log.verbose('PuppetWeChat', 'waitStable()')
let maxNum = 0
let curNum = 0
let unchangedNum = 0
const SLEEP_SECOND = 4
const STABLE_CHECK_NUM = 5
while (unchangedNum < STABLE_CHECK_NUM) {
// wait 1 second
await new Promise(resolve => setTimeout(resolve, SLEEP_SECOND * 1000))
const contactList = await this.contactList()
curNum = contactList.length
if (curNum > 0 && curNum === maxNum) {
unchangedNum++
} else /* curNum < maxNum */ {
unchangedNum = 0
}
if (curNum > maxNum) {
maxNum = curNum
}
log.silly('PuppetWeChat', 'readyStable() while() curNum=%s, maxNum=%s, unchangedNum=%s',
curNum, maxNum, unchangedNum,
)
}
log.verbose('PuppetWeChat', 'readyStable() emit(ready)')
this.emit('ready', { data: 'stable' })
}
private initHookEvents (wechat4u: any) {
log.verbose('PuppetWechat4u', 'initHookEvents()')
/**
* uuid事件,参数为uuid,根据uuid生成二维码
*/
this.wechat4u.on('uuid', (uuid: string) => {
log.silly('PuppetWechat4u', 'initHookEvents() wechat4u.on(uuid)')
this.scanQrCode = 'https://login.weixin.qq.com/l/' + uuid
this.emit('scan', { qrcode: this.scanQrCode, status: PUPPET.types.ScanStatus.Waiting })
})
/**
* 登录用户头像事件,手机扫描后可以得到登录用户头像的Data URL
*/
wechat4u.on('user-avatar', (avatarDataUrl: string) => {
this.emit('scan', {
data: avatarDataUrl,
qrcode: this.scanQrCode || '',
status: PUPPET.types.ScanStatus.Scanned,
})
})
/**
* 登录成功事件
*/
wechat4u.on('login', async () => {
// 由于初始好友列表没有存储当前bot的基础信息,所以在登录后直接更新当前机器人的信息
this.wechat4u.updateContacts([ this.wechat4u.user ])
this.startTime = parseInt(String(new Date().getTime() / 1000))
// FIXME: where's the logined user id?
const userId = this.wechat4u.user.UserName
if (!userId) {
this.emit('error', {
data: GError.stringify(
new Error('login event can not found selfId'),
),
})
return
}
// we do not wait `ready` before emit `login`
this.waitStable().catch(e => {
log.error('PuppetWeChatEvent', 'onLogin() this.waitStable() rejection: %s', e && (e as Error).message)
})
await this.login(userId)
// 保存数据,将数据序列化之后保存到任意位置
await this.memory.set(MEMORY_SLOT_NAME, wechat4u.botData)
await this.memory.save()
})
/**
* 登出成功事件
*/
wechat4u.on('logout', async () => {
log.silly('PuppetWechat4u', 'initHookEvents() wechat4u.on(logout)')
if (this.isLoggedIn) {
await this.logout()
}
// 清除数据
await this.memory.delete(MEMORY_SLOT_NAME)
await this.memory.save()
this.wechat4u.start()
})
/**
* 联系人更新事件,参数为被更新的联系人列表
*/
wechat4u.on('contacts-updated', (contacts: WebContactRawPayload[]) => {
log.silly('PuppetWechat4u', 'initHookEvents() wechat4u.on(contacts-updated) new/total contacts.length=%d/%d',
contacts.length,
Object.keys(wechat4u.contacts).length,
)
contacts.forEach((item: any) => {
if (isRoomId(item.UserName)) {
const membersList = item.MemberList.map((mItem: any) => {
this.unknownContactId.push([ mItem.UserName, item.UserName ])
return {
...mItem,
EncryChatRoomId: item.UserName,
}
})
this.wechat4u.updateContacts(membersList)
}
})
if (!this.getContactInterval) {
this.getContactsInfo()
this.getContactInterval = setInterval(() => {
this.getContactsInfo()
}, 2000)
}
})
/**
* 错误事件,参数一般为Error对象
*/
wechat4u.on('error', (err: Error) => {
this.emit('error', {
data: GError.stringify(err),
})
})
/**
* 如何处理会话消息
*/
wechat4u.on('message', async (msg: WebMessageRawPayload) => {
if (!msg.MsgId) {
log.warn('PuppetWechat4u', 'initHookEvents() wechat4u.on(message) no message id: %s', JSON.stringify(msg))
throw new Error('no id')
}
// 如果是消息的创建时间小于机器人启动的时间 直接丢弃
if (msg.CreateTime < this.startTime) {
// log.warn('PuppetWechat4u', 'initHookEvents() wechat4u.on(message) is history message: %s', JSON.stringify(msg))
return
}
this.cacheMessageRawPayload.set(msg.MsgId, msg)
const event = await parseEvent(this, msg)
switch (event.type) {
case EventType.Message:
this.emit('message', { messageId: msg.MsgId })
break
case EventType.Friendship: {
this.emit('friendship', {
friendshipId: msg.MsgId,
})
break
}
case EventType.RoomInvite: {
this.emit('room-invite', {
roomInvitationId: msg.MsgId,
})
break
}
case EventType.RoomJoin: {
const roomJoin: PUPPET.payloads.EventRoomJoin = event.payload
this.emit('room-join', roomJoin)
break
}
case EventType.RoomLeave: {
const roomLeave: PUPPET.payloads.EventRoomLeave = event.payload
this.emit('room-leave', roomLeave)
break
}
case EventType.RoomTopic: {
const roomTopic: PUPPET.payloads.EventRoomTopic = event.payload
this.emit('room-topic', roomTopic)
break
}
}
})
}
override async onStop (): Promise<void> {
log.verbose('PuppetWechat4u', 'onStop()')
this.wechat4u.stop()
this.wechat4u = undefined
if (this._heartBeatTimer) {
clearTimeout(this._heartBeatTimer)
this._heartBeatTimer = undefined
}
}
override async ding (data: string): Promise<void> {
log.silly('PuppetWechat4u', 'ding(%s)', data || '')
this.emit('dong', { data })
}
/**
*
* ContactSelf
*
*
*/
override async contactSelfQRCode (): Promise<string> {
return PUPPET.throwUnsupportedError()
}
override async contactSelfName (name: string): Promise<void> {
return PUPPET.throwUnsupportedError(name)
}
override async contactSelfSignature (signature: string): Promise<void> {
return PUPPET.throwUnsupportedError(signature)
}
/**
*
* Contact
*
*/
override contactAlias (contactId: string) : Promise<string>
override contactAlias (contactId: string, alias: null | string): Promise<void>
override async contactAlias (contactId: string, alias?: null | string): Promise<void | string> {
log.verbose('PuppetWechat4u', 'contactAlias(%s, %s)', contactId, alias)
if (typeof alias === 'undefined') {
const payload = await this.contactPayload(contactId)
return payload.alias
}
await this.wechat4u.updateRemarkName(contactId, alias)
}
override async contactList (): Promise<string[]> {
log.verbose('PuppetWechat4u', 'contactList()')
const idList = Object.keys(this.wechat4u.contacts)
.filter((contact: any) => !this.wechat4u.Contact.isRoomContact(this.wechat4u.contacts[contact]))
return idList
}
// override async contactQrCode (contactId: string): Promise<string> {
// return PUPPET.throwUnsupportedError(contactId)
// }
override async contactAvatar (contactId: string) : Promise<FileBoxInterface>
override async contactAvatar (contactId: string, file: FileBoxInterface) : Promise<void>
override async contactAvatar (contactId: string, file?: FileBoxInterface): Promise<void | FileBoxInterface> {
log.verbose('PuppetWechat4u', 'contactAvatar(%s)', contactId)
if (file) {
return PUPPET.throwUnsupportedError()
}
const rawPayload = await this.contactRawPayload(contactId)
const payload = await this.contactPayload(contactId)
const name = payload.name
// add '&type=big' to get big image
if (rawPayload.HeadImgUrl) {
const res = await this.wechat4u.getHeadImg(rawPayload.HeadImgUrl + '&type=big')
/**
* 如何获取联系人头像
*/
return FileBox.fromBuffer(
res.data,
`wechaty-contact-avatar-${name}.jpg`, // FIXME
)
}
}
override async contactRawPayload (contactId: string): Promise<WebContactRawPayload> {
log.verbose('PuppetWechat4u', 'contactRawPayload(%s) with contacts.length=%d',
contactId,
Object.keys(this.wechat4u.contacts).length,
)
if (!(contactId in this.wechat4u.contacts)) {
this.unknownContactId.push([ contactId, '' ])
if (!this.getContactInterval) {
this.getContactsInfo()
this.getContactInterval = setInterval(() => {
this.getContactsInfo()
}, 2000)
}
}
const rawPayload: WebContactRawPayload = await retry<WebContactRawPayload>(async (retryException, attempt) => {
log.verbose('PuppetWechat4u', 'contactRawPayload(%s) retry() attempt=%d', contactId, attempt)
if (contactId in this.wechat4u.contacts) {
return this.wechat4u.contacts[contactId]
}
retryException(new Error('no this.wechat4u.contacts[' + contactId + ']'))
})
return rawPayload
}
override async contactRawPayloadParser (rawPayload: WebContactRawPayload): Promise<PUPPET.payloads.Contact> {
return wechat4uContactToWechaty(rawPayload)
}
/**
*
* Message
*
*/
override async messageContact (
messageId: string,
): Promise<string> {
log.verbose('PuppetWechat4u', 'messageContact(%s)', messageId)
return PUPPET.throwUnsupportedError()
}
// web支持撤回消息 https://github.com/nodeWechat/wechat4u/blob/8e20b34507dbe783ada8c769b72ef1792f33c94a/src/core.js#L1219
override async messageRecall (
messageId: string,
): Promise<boolean> {
log.verbose('PuppetWechat4u', 'messageRecall(%s)', messageId)
const rawPayload = await this.messageRawPayload(messageId)
this.wechat4u.revokeMsg(messageId, rawPayload.ToUserName)
return true
}
override async messageImage (
messageId: string,
imageType: PUPPET.types.Image,
) : Promise<FileBoxInterface> {
log.verbose('PuppetWechat4u', 'messageImage(%s, %s[%s])',
messageId,
imageType,
PUPPET.types.Image[imageType],
)
/**
* 图片消息
*/
// console.log('图片消息,保存到本地')
const filename = `${messageId}.jpg`
const msg = await this.wechat4u.getMsgImg(messageId)
const file = FileBox.fromStream(
msg.data,
filename,
)
return file
}
override async messageFile (id: string): Promise<FileBoxInterface> {
log.verbose('PuppetWechat4u', 'messageFile(%s)', id)
const payload = await this.messagePayload(id)
const rawPayload = await this.messageRawPayload(id)
let filename = payload.filename || 'unknown.txt'
/**
* 判断消息类型
*/
switch (payload.type) {
case PUPPET.types.Message.Text:
/**
* 文本消息
*/
throw new Error('msg type is text')
case PUPPET.types.Message.Emoticon: {
/**
* 表情消息
*/
const emotionPayload = await parseEmotionMessagePayload(rawPayload)
const emoticonBox = FileBox.fromUrl(emotionPayload.cdnurl, { name: `message-${id}-emoticon.jpg` })
emoticonBox.metadata = {
payload: emotionPayload,
type: 'emoticon',
}
return emoticonBox
}
// eslint-disable-next-lint no-fallthrough
case PUPPET.types.Message.Image:{
/**
* 图片消息
*/
// console.log('图片消息,保存到本地')
filename = `${rawPayload.MsgId}.jpg`
const msg = await this.wechat4u.getMsgImg(rawPayload.MsgId)
const file = FileBox.fromBuffer(
msg.data,
filename,
)
return file
}
case PUPPET.types.Message.Audio: {
/**
* 语音消息
*/
const audioFileBox = FileBox.fromBuffer(
(await this.wechat4u.getVoice(rawPayload.MsgId)).data,
`message-${id}-audio.sil`,
)
const voiceLength = rawPayload.VoiceLength
audioFileBox.metadata = {
voiceLength,
}
// console.log('语音消息,保存到本地')
return audioFileBox
}
case PUPPET.types.Message.Video:
/**
* 视频消息
*/
// console.log('视频消息,保存到本地')
return FileBox.fromBuffer(
(await this.wechat4u.getVideo(rawPayload.MsgId)).data,
`message-${id}-video.mp4`,
)
case PUPPET.types.Message.Attachment:
if (rawPayload.AppMsgType === 6) {
/**
* 文件消息
*/
// console.log('文件消息,保存到本地')
filename = rawPayload.FileName
return FileBox.fromBuffer(
(await this.wechat4u.getDoc(rawPayload.FromUserName, rawPayload.MediaId, rawPayload.FileName)).data,
filename,
)
}
break
default:
break
}
throw new Error('unsupported message. id: ' + id)
}
override async messageUrl (messageId: string) : Promise<PUPPET.payloads.UrlLink> {
log.verbose('PuppetWechat4u', 'messageUrl(%s)', messageId)
const rawPayload: WebMessageRawPayload | undefined = this.cacheMessageRawPayload.get(messageId)
if (!rawPayload) {
throw new Error('id not found')
}
const message = await this.messageRawPayloadParser(rawPayload)
if (message.type !== PUPPET.types.Message.Url) {
throw new Error('Can not get url from non url payload')
}
const appPayload = await parseAppmsgMessagePayload(rawPayload.Content)
return {
description: appPayload.des,
thumbnailUrl: appPayload.thumburl,
title: appPayload.title,
url: appPayload.url,
}
}
override async messageMiniProgram (messageId: string): Promise<PUPPET.payloads.MiniProgram> {
log.verbose('PuppetWechat4u', 'messageMiniProgram(%s)', messageId)
const rawPayload: WebMessageRawPayload | undefined = this.cacheMessageRawPayload.get(messageId)
if (!rawPayload) {
throw new Error('id not found')
}
const message = await this.messageRawPayloadParser(rawPayload)
if (message.type !== PUPPET.types.Message.MiniProgram) {
throw new Error('message is not mini program, can not get MiniProgramPayload')
}
return parseMiniProgramMessagePayload(rawPayload)
}
override async messageRawPayload (id: string): Promise<WebMessageRawPayload> {
log.verbose('PuppetWechat4u', 'messageRawPayload(%s)', id)
const rawPayload = this.cacheMessageRawPayload.get(id)
if (!rawPayload) {
throw new Error('id not found')
}
return rawPayload
}
override async messageRawPayloadParser (
rawPayload: WebMessageRawPayload,
): Promise<PUPPET.payloads.Message> {
log.verbose('PuppetWechat4u', 'messageRawPayloadParser(%s) @ %s', rawPayload, this)
// console.log(rawPayload)
const payload = webMessageToWechaty(this, rawPayload)
return payload
}
override async messageSendText (
conversationId : string,
text : string,
): Promise<void> {
log.verbose('PuppetWechat4u', 'messageSend(%s, %s)', conversationId, text)
/**
* 发送文本消息,可以包含emoji(😒)和QQ表情([坏笑])
*/
await this.wechat4u.sendMsg(text, conversationId)
/**
* { BaseResponse: { Ret: 0, ErrMsg: '' },
* MsgID: '830582407297708303',
* LocalID: '15279119663740094' }
*/
}
override async messageSendFile (
conversationId : string,
file : FileBox,
): Promise<void> {
log.verbose('PuppetWechat4u', 'messageSend(%s, %s)', conversationId, file)
/**
* 通过表情MD5发送表情
*/
// wechat4u.sendMsg({
// emoticonMd5: '00c801cdf69127550d93ca52c3f853ff'
// }, ToUserName)
// .catch(err => {
// bot.emit('error', err)
// })
/**
* 以下通过上传文件发送图片,视频,附件等
* 通用方法为入下
* file为多种类型
* filename必填,主要为了判断文件类型
*/
await this.wechat4u.sendMsg({
file : await file.toStream(),
filename : file.name,
}, conversationId)
}
override async messageSendContact (
conversationId : string,
contactId : string,
): Promise<void> {
log.verbose('PuppetWechat4u', 'messageSend("%s", %s)', conversationId, contactId)
PUPPET.throwUnsupportedError()
}
override async messageSendUrl (conversationId: string, urlLinkPayload: PUPPET.payloads.UrlLink) : Promise<void> {
PUPPET.throwUnsupportedError(conversationId, urlLinkPayload)
}
override async messageSendMiniProgram (conversationId: string, miniProgramPayload: PUPPET.payloads.MiniProgram): Promise<void> {
log.verbose('PuppetWechat4u', 'messageSendMiniProgram("%s", %s)',
JSON.stringify(conversationId),
JSON.stringify(miniProgramPayload),
)
PUPPET.throwUnsupportedError(conversationId, miniProgramPayload)
}
override async messageForward (
conversationid : string,
messageId : string,
): Promise<void> {
log.verbose('PuppetWechat4u', 'messageForward(%s, %s)',
conversationid,
messageId,
)
const rawPayload = await this.messageRawPayload(messageId)
/**
* 如何直接转发消息
*/
await this.wechat4u.forwardMsg(rawPayload, conversationid)
}
override async conversationReadMark (
conversationId: string,
hasRead?: boolean,
) : Promise<void | boolean> {
return PUPPET.throwUnsupportedError(conversationId, hasRead)
}
/**
*
* Room Invitation
*
*/
override async roomInvitationAccept (roomInvitationId: string): Promise<void> {
return PUPPET.throwUnsupportedError(roomInvitationId)
}
override async roomInvitationRawPayload (roomInvitationId: string): Promise<any> {
return PUPPET.throwUnsupportedError(roomInvitationId)
}
override async roomInvitationRawPayloadParser (rawPayload: any): Promise<PUPPET.payloads.RoomInvitation> {
return PUPPET.throwUnsupportedError(rawPayload)
}
/**
*
* Room
*
*/
override async roomRawPayload (
id: string,
): Promise<WebRoomRawPayload> {
log.verbose('PuppetWechat4u', 'roomRawPayload(%s)', id)
const rawPayload: WebRoomRawPayload = await retry<WebRoomRawPayload>((retryException, attempt) => {
log.verbose('PuppetWechat4u', 'contactRawPayload(%s) retry() attempt=%d', id, attempt)
if (!this.wechat4u.contacts[id]) {
retryException(new Error('no this.wechat4u.contacts[' + id + ']'))
}
return this.wechat4u.contacts[id]
})
return rawPayload
}
override async roomRawPayloadParser (
rawPayload: WebRoomRawPayload,
): Promise<PUPPET.payloads.Room> {
return wechat4uRoomToWechaty(rawPayload)
}
override async roomList (): Promise<string[]> {
log.verbose('PuppetWechat4u', 'roomList()')
const idList = Object.keys(this.wechat4u.contacts)
.filter((contact: any) => this.wechat4u.Contact.isRoomContact(this.wechat4u.contacts[contact]))
return idList
}
override async roomDel (
roomId : string,
contactId : string,
): Promise<void> {
log.verbose('PuppetWechat4u', 'roomDel(%s, %s)', roomId, contactId)
const type = 'delmember'
// XXX: [contactId] or [{ UserName: id }, ...] ?
await this.wechat4u.updateChatroom(roomId, [ contactId ], type)
}
override async roomAvatar (roomId: string): Promise<FileBoxInterface> {
log.verbose('PuppetWechat4u', 'roomAvatar(%s)', roomId)
const payload = await this.roomPayload(roomId)
if (payload.avatar) {
// FIXME: set http headers with cookies
return FileBox.fromUrl(payload.avatar)
}
log.warn('PuppetWechat4u', 'roomAvatar() avatar not found, use the chatie default.')
return qrCodeForChatie()
}
override async roomAdd (
roomId : string,
contactId : string,
): Promise<void> {
log.verbose('PuppetWechat4u', 'roomAdd(%s, %s)', roomId, contactId)
const roomPayload = await this.roomPayload(roomId)
// TODO: if the room owner enabled "invite only?"
let type = 'addmember' // invitemember ???
if (roomPayload.memberIdList.length > 40) {
type = 'invitemember'
}
// https://github.com/nodeWechat/wechat4u/tree/46931e78bcb56899b8d2a42a37b919e7feaebbef#botupdatechatroomchatroomusername-memberlist-fun
const ret = await this.wechat4u.updateChatroom(roomId, [ contactId ], type)
log.verbose('PuppetWechat4u', 'roomAdd(%s, %s) ret: %s', roomId, contactId, JSON.stringify(ret))
}
override async roomTopic (roomId: string) : Promise<string>
override async roomTopic (roomId: string, topic: string) : Promise<void>
override async roomTopic (
roomId: string,
topic?: string,
): Promise<void | string> {
log.verbose('PuppetWechat4u', 'roomTopic(%s, %s)', roomId, topic)
const roomPayload = await this.roomPayload(roomId)
if (typeof topic === 'undefined') {
return roomPayload.topic
}
await this.wechat4u.updateChatRoomName(roomId, topic)
}
override async roomCreate (
contactIdList : string[],
topic : string,
): Promise<string> {
log.verbose('PuppetWechat4u', 'roomCreate(%s, %s)', contactIdList, topic)
const memberList = contactIdList.map(id => ({ UserName: id }))
const roomId = await this.wechat4u.createChatroom(topic, memberList)
return roomId
}
override async roomAnnounce (roomId: string) : Promise<string>
override async roomAnnounce (roomId: string, text: string) : Promise<void>
override async roomAnnounce (roomId: string, text?: string) : Promise<void | string> {
return PUPPET.throwUnsupportedError(roomId, text)
}
override async roomQuit (roomId: string): Promise<void> {
return PUPPET.throwUnsupportedError(roomId)
}
override async roomQRCode (roomId: string): Promise<string> {
return PUPPET.throwUnsupportedError(roomId)
}
override async roomMemberList (roomId: string) : Promise<string[]> {
log.verbose('PuppetWechat4u', 'roommemberList(%s)', roomId)
const rawPayload = await this.roomRawPayload(roomId)
const memberIdList = (rawPayload.MemberList || [])
.map(member => member.UserName)
return memberIdList
}
override async roomMemberRawPayload (roomId: string, contactId: string): Promise<WebRoomRawMember> {
log.verbose('PuppetWechat4u', 'roomMemberRawPayload(%s, %s)', roomId, contactId)
const rawPayload = await this.roomRawPayload(roomId)
const memberPayloadList = rawPayload.MemberList || []
const memberPayloadResult = memberPayloadList.filter(payload => payload.UserName === contactId)
if (memberPayloadResult.length > 0) {
return memberPayloadResult[0]!
} else {
throw new Error('not found')
}
}
override async roomMemberRawPayloadParser (rawPayload: WebRoomRawMember): Promise<PUPPET.payloads.RoomMember> {
return wechat4uRoomMemberToWechaty(rawPayload)
}
/**
*
* Friendship
*
*/
override async friendshipSearchPhone (
phone: string,
): Promise<null | string> {
log.verbose('PuppetWechat4u', 'friendshipSearchPhone(%s)', phone)
return PUPPET.throwUnsupportedError()
}
override async friendshipSearchWeixin (
weixin: string,
): Promise<null | string> {
log.verbose('PuppetWechat4u', 'friendshipSearchWeixin(%s)', weixin)
return PUPPET.throwUnsupportedError()
}
override async friendshipAdd (
contactId : string,
hello : string,
): Promise<void> {
log.verbose('PuppetWechat4u', 'friendshipAdd(%s, %s)', contactId, hello)
return PUPPET.throwUnsupportedError()
// await this.wechat4u.addFriend(contactId, hello)
}
override async friendshipAccept (
friendshipId : string,
): Promise<void> {
log.verbose('PuppetWechat4u', 'friendshipAccept(%s)', friendshipId)
const payload = await this.friendshipPayload(friendshipId) as any as PUPPET.payloads.FriendshipReceive
await this.wechat4u.verifyUser(payload.contactId, payload.ticket)
}
override async friendshipRawPayload (id: string): Promise<any> {
log.verbose('PuppetWechat4u', 'friendshipRawPayload(%s)', id)
const rawPayload = this.cacheMessageRawPayload.get(id)
if (!rawPayload) {
throw new Error('no rawPayload')
}
return rawPayload
}
override async friendshipRawPayloadParser (rawPayload: any) : Promise<PUPPET.payloads.Friendship> {
log.verbose('PuppetWechat4u', 'friendshipRawPayloadParser(%s)', rawPayload)
const timestamp = Math.floor(Date.now() / 1000) // in seconds
switch (rawPayload.MsgType) {
case WebMessageType.VERIFYMSG: {
const recommendInfo = rawPayload.RecommendInfo
if (!recommendInfo) {
throw new Error('no recommendInfo')
}
const payloadReceive: PUPPET.payloads.FriendshipReceive = {
contactId : recommendInfo.UserName,
hello : recommendInfo.Content,
id : rawPayload.MsgId,
ticket : recommendInfo.Ticket,
timestamp,
type : PUPPET.types.Friendship.Receive,
}
return payloadReceive
}
case WebMessageType.SYS: {
const payloadConfirm: PUPPET.payloads.FriendshipConfirm = {
contactId : rawPayload.FromUserName,
id : rawPayload.MsgId,
timestamp,
type : PUPPET.types.Friendship.Confirm,
}
return payloadConfirm
}
default:
throw new Error('not supported friend request message raw payload')
}
}
/**
*
* Tag
*
*/
override async tagContactAdd (
tagId: string,
contactId: string,
): Promise<void> {
log.verbose('PuppetWechat4u', 'tagContactAdd(%s)', tagId, contactId)
}
override async tagContactRemove (
tagId: string,
contactId: string,
): Promise<void> {
log.verbose('PuppetWechat4u', 'tagContactRemove(%s)', tagId, contactId)
}
override async tagContactDelete (
tagId: string,
): Promise<void> {
log.verbose('PuppetWechat4u', 'tagContactDelete(%s)', tagId)
}
override async tagContactList (
contactId?: string,
): Promise<string[]> {
log.verbose('PuppetWechat4u', 'tagContactList(%s)', contactId)
return []
}
override contactCorporationRemark (..._: any[]) {
return PUPPET.throwUnsupportedError()
}
override contactDescription (..._: any[]) {
return PUPPET.throwUnsupportedError()
}
override contactPhone (..._: any[]) {
return PUPPET.throwUnsupportedError()
}
override async messageLocation (messageId: string): Promise<PUPPET.payloads.Location> {
return PUPPET.throwUnsupportedError(messageId)
}
override async messageSendLocation (
conversationId: string,
locationPayload: PUPPET.payloads.Location,
): Promise<void | string> {
return PUPPET.throwUnsupportedError(conversationId, locationPayload)
}
}
export default PuppetWechat4u