UNPKG

wechaty-puppet-wechat

Version:
551 lines (457 loc) 16.2 kB
/** * 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 { log, } from './config.js' import type { PuppetWeChat } from './puppet-wechat.js' import type { // WebRecomendInfo, WebMessageRawPayload, } from './web-schemas.js' // import { // // FriendRequestPayload, // FriendRequestType, // FriendRequestPayloadReceive, // FriendRequestPayloadConfirm, // } from 'wechaty-puppet' const REGEX_CONFIG = { friendConfirm: [ /^You have added (.+) as your WeChat contact. Start chatting!$/, /^你已添加了(.+),现在可以开始聊天了。$/, /^(.+) just added you to his\/her contacts list. Send a message to him\/her now!$/, /^(.+)刚刚把你添加到通讯录,现在可以开始聊天了。$/, ], roomJoinInvite: [ // There are 3 blank(charCode is 32) here. eg: You invited 管理员 to the group chat. /^(.+?) invited (.+) to the group chat.\s+$/, // There no no blank or punctuation here. eg: 管理员 invited 小桔建群助手 to the group chat /^(.+?) invited (.+) to the group chat$/, // There are 2 blank(charCode is 32) here. eg: 你邀请"管理员"加入了群聊 /^(.+?)邀请"(.+)"加入了群聊\s+$/, // There no no blank or punctuation here. eg: "管理员"邀请"宁锐锋"加入了群聊 /^"(.+?)"邀请"(.+)"加入了群聊$/, ], roomJoinQrcode: [ // Wechat change this, should desperate. See more in pr#651 // /^" (.+)" joined the group chat via the QR Code shared by "?(.+?)".$/, // There are 2 blank(charCode is 32) here. Qrcode is shared by bot. // eg: "管理员" joined group chat via the QR code you shared. /^"(.+)" joined group chat via the QR code "?(.+?)"? shared.\s+$/, // There are no blank(charCode is 32) here. Qrcode isn't shared by bot. // eg: "宁锐锋" joined the group chat via the QR Code shared by "管理员". /^"(.+)" joined the group chat via the QR Code shared by "?(.+?)".$/, // There are 2 blank(charCode is 32) here. Qrcode is shared by bot. eg: "管理员"通过扫描你分享的二维码加入群聊 /^"(.+)"通过扫描(.+?)分享的二维码加入群聊\s+$/, // There are 1 blank(charCode is 32) here. Qrode isn't shared by bot. eg: " 苏轼"通过扫描"管理员"分享的二维码加入群聊 /^"\s+(.+)"通过扫描"(.+?)"分享的二维码加入群聊$/, ], // no list roomLeaveIKickOther: [ /^(You) removed "(.+)" from the group chat$/, /^(你)将"(.+)"移出了群聊$/, ], roomLeaveOtherKickMe: [ /^(You) were removed from the group chat by "(.+)"$/, /^(你)被"(.+)"移出群聊$/, ], roomTopic: [ /^"?(.+?)"? changed the group name to "(.+)"$/, /^"?(.+?)"?修改群名为“(.+)”$/, ], } export class Firer { constructor ( public puppet: PuppetWeChat, ) { // } // public async checkFriendRequest( // rawPayload : WebMessageRawPayload, // ): Promise<void> { // if (!rawPayload.RecommendInfo) { // throw new Error('no RecommendInfo') // } // const recommendInfo: WebRecomendInfo = rawPayload.RecommendInfo // log.verbose('PuppetWeChatFirer', 'fireFriendRequest(%s)', recommendInfo) // if (!recommendInfo) { // throw new Error('no recommendInfo') // } // const contactId = recommendInfo.UserName // const hello = recommendInfo.Content // const ticket = recommendInfo.Ticket // const type = FriendRequestType.Receive // const id = cuid() // const payloadReceive: FriendRequestPayloadReceive = { // id, // contactId, // hello, // ticket, // type, // } // this.puppet.cacheFriendRequestPayload.set(id, payloadReceive) // this.puppet.emit('friend', id) // } public async checkFriendConfirm ( rawPayload : WebMessageRawPayload, ) { const content = rawPayload.Content log.silly('PuppetWeChatFirer', 'fireFriendConfirm(%s)', content) if (!this.parseFriendConfirm(content)) { return } // const contactId = rawPayload.FromUserName // const type = FriendRequestType.Confirm // const id = cuid() // const payloadConfirm: FriendRequestPayloadConfirm = { // id, // contactId, // type, // } // this.puppet.cacheFriendRequestPayload.set(id, payloadConfirm) this.puppet.emit('friendship', { friendshipId: rawPayload.MsgId }) } public async checkRoomJoin ( rawPayload : WebMessageRawPayload, ): Promise<boolean> { const text = rawPayload.Content const roomId = rawPayload.FromUserName /** * Get the display names of invitee & inviter */ let inviteeNameList : string[] let inviterName : string try { [inviteeNameList, inviterName] = this.parseRoomJoin(text) } catch (e) { log.silly('PuppetWeChatFirer', 'checkRoomJoin() "%s" is not a join message', text) return false // not a room join message } log.silly('PuppetWeChatFirer', 'checkRoomJoin() inviteeList: %s, inviter: %s', inviteeNameList.join(','), inviterName, ) /** * Convert the display name to Contact ID */ let inviterContactId: undefined | string const inviteeContactIdList: string[] = [] if (/^You|你$/i.test(inviterName)) { // === 'You' || inviter === '你' || inviter === 'you' inviterContactId = this.puppet.currentUserId } const sleep = 1000 const timeout = 60 * 1000 let ttl = timeout / sleep let ready = true while (ttl-- > 0) { log.silly('PuppetWeChatFirer', 'fireRoomJoin() retry() ttl %d', ttl) if (!ready) { await new Promise(resolve => setTimeout(resolve, timeout)) ready = true } /** * loop inviteeNameList * set inviteeContactIdList */ for (let i = 0; i < inviteeNameList.length; i++) { const inviteeName = inviteeNameList[i]! const inviteeContactId = inviteeContactIdList[i] if (inviteeContactId) { /** * had already got ContactId for Room Member * try to resolve the ContactPayload */ try { await this.puppet.contactPayload(inviteeContactId) } catch (e) { log.warn('PuppetWeChatFirer', 'fireRoomJoin() contactPayload(%s) exception: %s', inviteeContactId, (e as Error).message, ) ready = false } } else { /** * only had Name of RoomMember * try to resolve the ContactId & ContactPayload */ const memberIdList = await this.puppet.roomMemberSearch(roomId, inviteeName) if (memberIdList.length <= 0) { ready = false } const contactId = memberIdList[0] // XXX: Take out the first one if we have matched many contact. inviteeContactIdList[i] = contactId! if (!contactId) { ready = false } else { try { await this.puppet.contactPayload(contactId) } catch (e) { ready = false } } } } if (!inviterContactId) { const contactIdList = await this.puppet.roomMemberSearch(roomId, inviterName) if (contactIdList.length > 0) { inviterContactId = contactIdList[0] } else { ready = false } } if (ready) { log.silly('PuppetWeChatFirer', 'fireRoomJoin() resolve() inviteeContactIdList: %s, inviterContactId: %s', inviteeContactIdList.join(','), inviterContactId, ) /** * Resolve All Payload again to make sure the data is ready. */ await Promise.all( inviteeContactIdList .filter(id => !!id) .map( id => this.puppet.contactPayload(id!), ), ) if (!inviterContactId) { throw new Error('no inviterContactId') } await this.puppet.contactPayload(inviterContactId) await this.puppet.roomPayload(roomId) const timestamp = Math.floor(Date.now() / 1000) // in seconds this.puppet.emit('room-join', { inviteeIdList : inviteeContactIdList, inviterId : inviterContactId, roomId, timestamp, }) return true } } log.warn('PuppetWeChatFier', 'fireRoomJoin() resolve payload fail.') return false } /** * You removed "Bruce LEE" from the group chat */ public async checkRoomLeave ( rawPayload : WebMessageRawPayload, ): Promise<boolean> { log.verbose('PuppetWeChatFirer', 'fireRoomLeave(%s)', rawPayload.Content) const roomId = rawPayload.FromUserName let leaverName : string let removerName : string try { [leaverName, removerName] = this.parseRoomLeave(rawPayload.Content) } catch (e) { log.silly('PuppetWeChatFirer', 'fireRoomLeave() %s', (e as Error).message) return false } log.silly('PuppetWeChatFirer', 'fireRoomLeave() got leaverName: %s', leaverName) /** * FIXME: leaver maybe is a list * @lijiarui: I have checked, leaver will never be a list. * If the bot remove 2 leavers at the same time, * it will be 2 sys message, instead of 1 sys message contains 2 leavers. */ let leaverContactId : undefined | string let removerContactId : undefined | string if (/^(You|你)$/i.test(leaverName)) { leaverContactId = this.puppet.currentUserId } else if (/^(You|你)$/i.test(removerName)) { removerContactId = this.puppet.currentUserId } if (!leaverContactId) { const idList = await this.puppet.roomMemberSearch(roomId, leaverName) leaverContactId = idList[0] } if (!removerContactId) { const idList = await this.puppet.roomMemberSearch(roomId, removerName) removerContactId = idList[0] } if (!leaverContactId || !removerContactId) { throw new Error('no id') } /** * FIXME: leaver maybe is a list * @lijiarui 2017: I have checked, leaver will never be a list. If the bot remove 2 leavers at the same time, * it will be 2 sys message, instead of 1 sys message contains 2 leavers. * @huan 2018 May: we need to generilize the pattern for future usage. */ const timestamp = Math.floor(Date.now() / 1000) // in seconds this.puppet.emit('room-leave', { removeeIdList : [leaverContactId], removerId : removerContactId, roomId, timestamp, }) setTimeout(() => { this.puppet.roomPayloadDirty(roomId) .then(() => this.puppet.roomPayload(roomId)) .catch(console.error) }, 10 * 1000) // reload the room data, especially for memberList return true } public async checkRoomTopic ( rawPayload : WebMessageRawPayload, ): Promise<boolean> { let topic : string let changer : string try { [topic, changer] = this.parseRoomTopic(rawPayload.Content) } catch (e) { // not found return false } const roomId = rawPayload.FromUserName const roomPayload = await this.puppet.roomPayload(roomId) const oldTopic = roomPayload.topic let changerContactId: undefined | string if (/^(You|你)$/.test(changer)) { changerContactId = this.puppet.currentUserId } else { changerContactId = (await this.puppet.roomMemberSearch(roomId, changer))[0] } if (!changerContactId) { log.error('PuppetWeChatFirer', 'fireRoomTopic() changer contact not found for %s', changer) return false } try { const timestamp = Math.floor(Date.now() / 1000) // in seconds this.puppet.emit('room-topic', { changerId : changerContactId, newTopic : topic, oldTopic, roomId, timestamp, }) return true } catch (e) { log.error('PuppetWeChatFirer', 'fireRoomTopic() co exception: %s', (e as Error).stack) return false } } /** * try to find FriendRequest Confirmation Message */ private parseFriendConfirm ( content: string, ): boolean { const reList = REGEX_CONFIG.friendConfirm const found = reList.some(re => re.test(content)) if (found) { return true } else { return false } } /** * try to find 'join' event for Room * * 1. * You invited 管理员 to the group chat. * You invited 李卓桓.PreAngel、Bruce LEE to the group chat. * 2. * 管理员 invited 小桔建群助手 to the group chat * 管理员 invited 庆次、小桔妹 to the group chat */ private parseRoomJoin ( content: string, ): [string[], string] { log.verbose('PuppetWeChatFirer', 'parseRoomJoin(%s)', content) const reListInvite = REGEX_CONFIG.roomJoinInvite const reListQrcode = REGEX_CONFIG.roomJoinQrcode let foundInvite: null | string[] = null for (const re of reListInvite) { foundInvite = content.match(re) if (foundInvite) { break } } // reListInvite.some(re => !!(foundInvite = content.match(re))) let foundQrcode: null | string[] = [] for (const re of reListQrcode) { foundQrcode = content.match(re) if (foundQrcode) { break } } // reListQrcode.some(re => !!(foundQrcode = content.match(re))) if ((!foundInvite || !foundInvite.length) && (!foundQrcode || !foundQrcode.length)) { throw new Error('parseRoomJoin() not found matched re of ' + content) } /** * 管理员 invited 庆次、小桔妹 to the group chat * "管理员"通过扫描你分享的二维码加入群聊 */ const [inviter, inviteeStr] = foundInvite ? [foundInvite[1], foundInvite[2]] : [foundQrcode![2], foundQrcode![1]] // FIXME: should also compatible english split const inviteeList = inviteeStr?.split(/、/) || [] return [inviteeList, inviter!] // put invitee at first place } private parseRoomLeave ( content: string, ): [string, string] { let matchIKickOther: null | string[] = null for (const re of REGEX_CONFIG.roomLeaveIKickOther) { matchIKickOther = content.match(re) if (matchIKickOther) { break } } let matchOtherKickMe: null | string[] = null for (const re of REGEX_CONFIG.roomLeaveOtherKickMe) { matchOtherKickMe = content.match(re) if (matchOtherKickMe) { break } } let leaverName : undefined | string let removerName : undefined | string if (matchIKickOther && matchIKickOther.length) { leaverName = matchIKickOther[2] removerName = matchIKickOther[1] } else if (matchOtherKickMe && matchOtherKickMe.length) { leaverName = matchOtherKickMe[1] removerName = matchOtherKickMe[2] } else { throw new Error('no match') } return [leaverName!, removerName!] } private parseRoomTopic ( content: string, ): [string, string] { const reList = REGEX_CONFIG.roomTopic let found: null | string[] = null for (const re of reList) { found = content.match(re) if (found) { break } } if (!found || !found.length) { throw new Error('checkRoomTopic() not found') } const [, changer, topic] = found return [topic!, changer!] } } export default Firer