firer.ts 14.9 KB
Newer Older
1
/**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
2
 *   Wechaty - https://github.com/chatie/wechaty
3
 *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
4
 *   @copyright 2016-2018 Huan LI <zixia@zixia.net>
5
 *
6 7 8
 *   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
9
 *
10
 *       http://www.apache.org/licenses/LICENSE-2.0
11
 *
12 13 14 15 16
 *   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.
17
 *
18 19 20
 */

/* tslint:disable:no-var-requires */
21
const retryPromise  = require('retry-promise').default
22

23
import {
L
lijiarui 已提交
24
  log,
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
25
}         from '../config'
26

27 28
import PuppeteerContact       from './puppeteer-contact'
import PuppeteerMessage       from './puppeteer-message'
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
29

30
import PuppeteerFriendRequest from './puppeteer-friend-request'
31 32

/* tslint:disable:variable-name */
33
export const Firer = {
L
lijiarui 已提交
34 35
  checkFriendConfirm,
  checkFriendRequest,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
36

L
lijiarui 已提交
37 38 39
  checkRoomJoin,
  checkRoomLeave,
  checkRoomTopic,
40

L
lijiarui 已提交
41 42 43 44
  parseFriendConfirm,
  parseRoomJoin,
  parseRoomLeave,
  parseRoomTopic,
45

46 47 48
}

const regexConfig = {
49
  friendConfirm: [
L
lijiarui 已提交
50 51
    /^You have added (.+) as your WeChat contact. Start chatting!$/,
    /^你已添加了(.+),现在可以开始聊天了。$/,
52 53
    /^(.+) just added you to his\/her contacts list. Send a message to him\/her now!$/,
    /^(.+)刚刚把你添加到通讯录,现在可以开始聊天了。$/,
L
lijiarui 已提交
54 55 56
  ],

  roomJoinInvite: [
L
lijiarui 已提交
57 58 59 60 61 62 63 64 65 66 67
    // 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: "管理员"邀请"宁锐锋"加入了群聊
    /^"(.+?)"邀请"(.+)"加入了群聊$/,
L
lijiarui 已提交
68 69 70
  ],

  roomJoinQrcode: [
L
lijiarui 已提交
71 72 73 74 75 76 77
    // 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 "管理员".
L
lijiarui 已提交
78
    /^"(.+)" joined the group chat via the QR Code shared by "?(.+?)".$/,
L
lijiarui 已提交
79 80 81 82 83 84

    // 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+(.+)"通过扫描"(.+?)"分享的二维码加入群聊$/,
L
lijiarui 已提交
85 86
  ],

87 88
  // no list
  roomLeaveByBot: [
L
lijiarui 已提交
89 90 91
    /^You removed "(.+)" from the group chat$/,
    /^你将"(.+)"移出了群聊$/,
  ],
L
lijiarui 已提交
92

93 94 95 96 97
  roomLeaveByOther: [
    /^You were removed from the group chat by "(.+)"$/,
    /^你被"(.+)"移出群聊$/,
  ],

L
lijiarui 已提交
98 99 100 101
  roomTopic: [
    /^"?(.+?)"? changed the group name to "(.+)"$/,
    /^"?(.+?)"?修改群名为“(.+)”$/,
  ],
102 103
}

104
async function checkFriendRequest(m: PuppeteerMessage) {
105 106 107
  if (!m.rawObj) {
    throw new Error('message empty')
  }
108
  const info = m.rawObj.RecommendInfo
109
  log.verbose('PuppetPuppeteerFirer', 'fireFriendRequest(%s)', info)
110

111 112 113
  if (!info) {
    throw new Error('no info')
  }
114

115
  const request = new PuppeteerFriendRequest()
116 117
  request.puppet = m.puppet

118
  request.receive(info)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
119 120

  await request.contact.ready()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
121
  if (!request.contact.isReady()) {
122
    log.warn('PuppetPuppeteerFirer', 'fireFriendConfirm() contact still not ready after `ready()` call')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
123 124
  }

125 126 127 128 129 130
  this.emit('friend', request.contact, request)
}

/**
 * try to find FriendRequest Confirmation Message
 */
131
function parseFriendConfirm(content: string): boolean {
132 133
  const reList = regexConfig.friendConfirm
  let found = false
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
134

135 136
  reList.some(re => !!(found = re.test(content)))
  if (found) {
137 138 139 140 141 142
    return true
  } else {
    return false
  }
}

143
async function checkFriendConfirm(m: PuppeteerMessage) {
144
  const content = m.text()
145
  log.silly('PuppetPuppeteerFirer', 'fireFriendConfirm(%s)', content)
146

147
  if (!parseFriendConfirm(content)) {
148 149
    return
  }
150
  const request = new PuppeteerFriendRequest()
151 152
  request.puppet = m.puppet

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
153
  const contact = m.from()
154 155
  request.confirm(contact)

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
156
  await contact.ready()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
157
  if (!contact.isReady()) {
158
    log.warn('PuppetPuppeteerFirer', 'fireFriendConfirm() contact still not ready after `ready()` call')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
159
  }
160 161 162 163 164 165
  this.emit('friend', contact)
}

/**
 * try to find 'join' event for Room
 *
166
 * 1.
L
lijiarui 已提交
167 168
 *  You invited 管理员 to the group chat.
 *  You invited 李卓桓.PreAngel、Bruce LEE to the group chat.
169
 * 2.
L
lijiarui 已提交
170 171
 *  管理员 invited 小桔建群助手 to the group chat
 *  管理员 invited 庆次、小桔妹 to the group chat
172
 */
173
function parseRoomJoin(content: string): [string[], string] {
174
  log.verbose('PuppetPuppeteerFirer', 'checkRoomJoin(%s)', content)
175

ruiruibupt's avatar
#155  
ruiruibupt 已提交
176 177
  const reListInvite = regexConfig.roomJoinInvite
  const reListQrcode = regexConfig.roomJoinQrcode
178

ruiruibupt's avatar
#155  
ruiruibupt 已提交
179 180 181 182 183
  let foundInvite: string[]|null = []
  reListInvite.some(re => !!(foundInvite = content.match(re)))
  let foundQrcode: string[]|null = []
  reListQrcode.some(re => !!(foundQrcode = content.match(re)))
  if ((!foundInvite || !foundInvite.length) && (!foundQrcode || !foundQrcode.length)) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
184
    throw new Error('checkRoomJoin() not found matched re of ' + content)
185
  }
186
  /**
L
lijiarui 已提交
187 188
   * 管理员 invited 庆次、小桔妹 to the group chat
   * "管理员"通过扫描你分享的二维码加入群聊
ruiruibupt's avatar
ruiruibupt 已提交
189
   */
ruiruibupt's avatar
#155  
ruiruibupt 已提交
190
  const [inviter, inviteeStr] = foundInvite ? [ foundInvite[1], foundInvite[2] ] : [ foundQrcode[2], foundQrcode[1] ]
191 192 193
  const inviteeList = inviteeStr.split(/、/)

  return [inviteeList, inviter] // put invitee at first place
194 195
}

196
async function checkRoomJoin(m: PuppeteerMessage): Promise<boolean> {
197

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
198
  const room = m.room()
199
  if (!room) {
200
    log.warn('PuppetPuppeteerFirer', 'fireRoomJoin() `room` not found')
L
lijiarui 已提交
201
    return false
202 203
  }

204
  const text = m.text()
205

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
206 207
  let inviteeList: string[], inviter: string
  try {
208
    [inviteeList, inviter] = parseRoomJoin(text)
209
  } catch (e) {
210
    log.silly('PuppetPuppeteerFirer', 'fireRoomJoin() "%s" is not a join message', text)
L
lijiarui 已提交
211
    return false // not a room join message
212
  }
213
  log.silly('PuppetPuppeteerFirer', 'fireRoomJoin() inviteeList: %s, inviter: %s',
L
lijiarui 已提交
214 215
                              inviteeList.join(','),
                              inviter,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
216
          )
217

218 219
  let inviterContact: PuppeteerContact | null = null
  let inviteeContactList: PuppeteerContact[] = []
220

221
  try {
L
lijiarui 已提交
222
    if (inviter === 'You' || inviter === '' || inviter === 'you') {
223
      inviterContact = PuppeteerContact.load(this.userId) as PuppeteerContact
224
      inviterContact.puppet = m.puppet
225 226
    }

227 228 229 230
    const max = 20
    const backoff = 300
    const timeout = max * (backoff * max) / 2
    // 20 / 300 => 63,000
231 232 233
    // max = (2*totalTime/backoff) ^ (1/2)
    // timeout = 11,250 for {max: 15, backoff: 100}

234
    await retryPromise({ max: max, backoff: backoff }, async (attempt: number) => {
235
      log.silly('PuppetPuppeteerFirer', 'fireRoomJoin() retryPromise() attempt %d with timeout %d', attempt, timeout)
236

237
      await room.refresh()
238
      let inviteeListAllDone = true
239

240
      for (const i in inviteeList) {
241
        const loaded = inviteeContactList[i] instanceof PuppeteerContact
242 243

        if (!loaded) {
244
          const c = room.member(inviteeList[i])
245 246 247 248 249 250 251 252 253 254
          if (!c) {
            inviteeListAllDone = false
            continue
          }

          inviteeContactList[i] = await c.ready()
          const isReady = c.isReady()
          if (!isReady) {
            inviteeListAllDone = false
            continue
255 256
          }
        }
257

258
        if (inviteeContactList[i] instanceof PuppeteerContact) {
259 260
          const isReady = inviteeContactList[i].isReady()
          if (!isReady) {
261
            log.warn('PuppetPuppeteerFirer', 'fireRoomJoin() retryPromise() isReady false for contact %s', inviteeContactList[i].id)
262 263 264 265 266 267
            inviteeListAllDone = false
            await inviteeContactList[i].refresh()
            continue
          }
        }

268 269 270 271 272 273
      }

      if (!inviterContact) {
        inviterContact = room.member(inviter)
      }

274
      if (inviteeListAllDone && inviterContact) {
275 276
        log.silly('PuppetPuppeteerFirer', 'fireRoomJoin() resolve() inviteeContactList: %s, inviterContact: %s',
                                    inviteeContactList.map((c: PuppeteerContact) => c.name()).join(','),
L
lijiarui 已提交
277
                                    inviterContact.name(),
278
                )
L
lijiarui 已提交
279
        return true
280 281
      }

282
      log.error('PuppetPuppeteerFirer', 'fireRoomJoin() not found(yet)')
L
lijiarui 已提交
283 284
      return false
      // throw new Error('not found(yet)')
285 286

    }).catch(e => {
287 288
      log.warn('PuppetPuppeteerFirer', 'fireRoomJoin() reject() inviteeContactList: %s, inviterContact: %s',
                                 inviteeContactList.map((c: PuppeteerContact) => c.name()).join(','),
L
lijiarui 已提交
289
                                 inviter,
290
      )
291
    })
292

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
293
    if (!inviterContact) {
294
      log.error('PuppetPuppeteerFirer', 'firmRoomJoin() inivter not found for %s , `room-join` & `join` event will not fired', inviter)
L
lijiarui 已提交
295
      return false
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
296
    }
297 298
    if (!inviteeContactList.every(c => c instanceof PuppeteerContact)) {
      log.error('PuppetPuppeteerFirer', 'firmRoomJoin() inviteeList not all found for %s , only part of them will in the `room-join` or `join` event',
L
lijiarui 已提交
299
                                  inviteeContactList.join(','),
300
              )
301
      inviteeContactList = inviteeContactList.filter(c => (c instanceof PuppeteerContact))
302
      if (inviteeContactList.length < 1) {
303
        log.error('PuppetPuppeteerFirer', 'firmRoomJoin() inviteeList empty.  `room-join` & `join` event will not fired')
L
lijiarui 已提交
304
        return false
305
      }
306
    }
307

308 309
    await Promise.all(inviteeContactList.map(c => c.ready()))
    await inviterContact.ready()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
310
    await room.ready()
311

312 313
    this.emit('room-join', room , inviteeContactList, inviterContact)
    room.emit('join'            , inviteeContactList, inviterContact)
314

L
lijiarui 已提交
315
    return true
316
  } catch (e) {
317
    log.error('PuppetPuppeteerFirer', 'exception: %s', e.stack)
L
lijiarui 已提交
318
    return false
319 320
  }

321 322
}

323 324 325 326 327 328 329 330
function parseRoomLeave(content: string): [string, string] {
  const reListByBot = regexConfig.roomLeaveByBot
  const reListByOther = regexConfig.roomLeaveByOther
  let foundByBot: string[]|null = []
  reListByBot.some(re => !!(foundByBot = content.match(re)))
  let foundByOther: string[]|null = []
  reListByOther.some(re => !!(foundByOther = content.match(re)))
  if ((!foundByBot || !foundByBot.length) && (!foundByOther || !foundByOther.length)) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
331
    throw new Error('checkRoomLeave() no matched re for ' + content)
332
  }
333 334
  const [leaver, remover] = foundByBot ? [ foundByBot[1], this.userId ] : [ this.userId, foundByOther[1] ]
  return [leaver, remover]
335 336 337 338 339
}

/**
 * You removed "Bruce LEE" from the group chat
 */
340 341
async function checkRoomLeave(m: PuppeteerMessage): Promise<boolean> {
  log.verbose('PuppetPuppeteerFirer', 'fireRoomLeave(%s)', m.text())
342

343
  let leaver: string, remover: string
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
344
  try {
345
    [leaver, remover] = parseRoomLeave(m.text())
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
346
  } catch (e) {
L
lijiarui 已提交
347
    return false
348
  }
349
  log.silly('PuppetPuppeteerFirer', 'fireRoomLeave() got leaver: %s', leaver)
350

351
  const room = m.room()
352
  if (!room) {
353
    log.warn('PuppetPuppeteerFirer', 'fireRoomLeave() room not found')
L
lijiarui 已提交
354
    return false
355
  }
356 357
  /**
   * FIXME: leaver maybe is a list
358
   * @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.
359
   */
360
  let leaverContact: PuppeteerContact | null, removerContact: PuppeteerContact | null
361
  if (leaver === this.userId) {
362
    leaverContact = PuppeteerContact.load(this.userId) as PuppeteerContact
363 364
    leaverContact.puppet = m.puppet

365 366 367 368
    // not sure which is better
    // removerContact = room.member({contactAlias: remover}) || room.member({name: remover})
    removerContact = room.member(remover)
    if (!removerContact) {
369
      log.error('PuppetPuppeteerFirer', 'fireRoomLeave() bot is removed from the room, but remover %s not found, event `room-leave` & `leave` will not be fired', remover)
L
lijiarui 已提交
370
      return false
371
    }
372

373
  } else {
374
    removerContact = PuppeteerContact.load(this.userId) as PuppeteerContact
375 376
    removerContact.puppet = m.puppet

377 378 379 380
    // not sure which is better
    // leaverContact = room.member({contactAlias: remover}) || room.member({name: leaver})
    leaverContact = room.member(remover)
    if (!leaverContact) {
381
      log.error('PuppetPuppeteerFirer', 'fireRoomLeave() bot removed someone from the room, but leaver %s not found, event `room-leave` & `leave` will not be fired', leaver)
L
lijiarui 已提交
382
      return false
383
    }
384
  }
385

386
  await removerContact.ready()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
387 388
  await leaverContact.ready()
  await room.ready()
389 390 391

  /**
   * FIXME: leaver maybe is a list
392
   * @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.
393
   */
394 395
  this.emit('room-leave', room, leaverContact, removerContact)
  room.emit('leave'           , leaverContact, removerContact)
396 397

  setTimeout(_ => { room.refresh() }, 10000) // reload the room data, especially for memberList
L
lijiarui 已提交
398
  return true
399 400
}

401
function parseRoomTopic(content: string): [string, string] {
402
  const reList = regexConfig.roomTopic
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
403

404 405 406
  let found: string[]|null = []
  reList.some(re => !!(found = content.match(re)))
  if (!found || !found.length) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
407
    throw new Error('checkRoomTopic() not found')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
408
  }
409
  const [, changer, topic] = found
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
410 411 412
  return [topic, changer]
}

413
async function checkRoomTopic(m: PuppeteerMessage): Promise<boolean> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
414 415
  let  topic, changer
  try {
416
    [topic, changer] = parseRoomTopic(m.text())
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
417
  } catch (e) { // not found
L
lijiarui 已提交
418
    return false
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
419 420 421
  }

  const room = m.room()
422
  if (!room) {
423
    log.warn('PuppetPuppeteerFirer', 'fireRoomLeave() room not found')
L
lijiarui 已提交
424
    return false
425 426
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
427 428
  const oldTopic = room.topic()

429
  let changerContact: PuppeteerContact | null
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
430
  if (/^You$/.test(changer) || /^你$/.test(changer)) {
431
    changerContact = PuppeteerContact.load(this.userId) as PuppeteerContact
432
    changerContact.puppet = m.puppet
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
433 434 435
  } else {
    changerContact = room.member(changer)
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
436 437

  if (!changerContact) {
438
    log.error('PuppetPuppeteerFirer', 'fireRoomTopic() changer contact not found for %s', changer)
L
lijiarui 已提交
439
    return false
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
440 441
  }

442 443
  try {
    await changerContact.ready()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
444
    await room.ready()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
445
    this.emit('room-topic', room, topic, oldTopic, changerContact)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
446
    room.emit('topic'           , topic, oldTopic, changerContact)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
447
    room.refresh()
L
lijiarui 已提交
448
    return true
449
  } catch (e) {
450
    log.error('PuppetPuppeteerFirer', 'fireRoomTopic() co exception: %s', e.stack)
L
lijiarui 已提交
451
    return false
452
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
453
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
454 455

export default Firer