wechaty-puppet-matrix 0.0.21 → 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1674 @@
1
+ import { log } from '@juzi/wechaty-puppet'
2
+ import * as PUPPET from '@juzi/wechaty-puppet'
3
+ import type { FileBoxInterface } from 'file-box'
4
+ import {
5
+ FileBox,
6
+ FileBoxType,
7
+ } from 'file-box'
8
+ import type { ContactPayload, MessagePayload } from './engine-schema.js'
9
+ import type { FileBoxMetadataMessage } from './matrix/types.js'
10
+ import Client from './matrix/service/request.js'
11
+ import { getUnixTime } from 'date-fns'
12
+
13
+ import { delay } from './matrix/utils/index.js'
14
+ import path from 'path'
15
+ // 参考
16
+ import { CacheManager, RoomMemberMap } from './matrix/cache-manager.js'
17
+ import { isIMContactId, isRoomId } from './matrix/utils/is-type.js'
18
+ import { AppMessagePayload, parseAppmsgMessagePayload } from './matrix/messages/message-appmsg.js'
19
+ import { parseMiniProgramMessagePayload } from './matrix/messages/message-miniprogram.js'
20
+ import { parseEmotionMessagePayload } from './matrix/messages/message-emotion.js'
21
+ import { ImageMessagePayload, parseImageMessagePayload } from './matrix/messages/message-image.js'
22
+ import { parseAudioMessagePayload, AudioMessagePayload } from './matrix/messages/message-audio.js'
23
+ import { parseVideoMessagePayload, VideoMessagePayload } from './matrix/messages/message-video.js'
24
+ import { parseLocationMessagePayload } from './matrix/messages/message-location.js'
25
+ import { CachedPromiseFunc } from './matrix/utils/cached-promise.js'
26
+ import { engineMessageToWechaty } from './matrix/schema-mapper/message.js'
27
+ import { engineContactToWechaty } from './matrix/schema-mapper/contact.js'
28
+ import {
29
+ chatRoomMemberToContact,
30
+ engineRoomMemberToWechaty,
31
+ engineRoomToWechaty,
32
+ } from './matrix/schema-mapper/room.js'
33
+ import { parseEvent, EventType } from './matrix/events/mod.js'
34
+ import { addRunningPuppet, removeRunningPuppet } from './cleanup.js'
35
+ import { packageJson } from './package-json.js'
36
+ import { JsonToXml } from './matrix/utils/xml-to-json.js'
37
+
38
+ const VERSION = packageJson.version || '0.0.0'
39
+ const PRE = '[PuppetMatrix]'
40
+ const SEARCH_CONTACT_PREFIX = '$search$-'
41
+ const STRANGER_SUFFIX = '@stranger'
42
+
43
+ export type PuppetEngineOptions = PUPPET.PuppetOptions & {
44
+ proxyId?: string,
45
+ token?: string,
46
+ engine?: any
47
+ maxGetQrcoderTimes: number
48
+ }
49
+
50
+ class PuppetMatrix extends PUPPET.Puppet {
51
+
52
+ private _cacheMgr?: CacheManager
53
+ private _client?: Client
54
+ private _self?: ContactPayload | undefined
55
+ private _qrcodeTicket: string | null | undefined
56
+ private _printVersion: boolean = true
57
+ private _heartBeatTimer?: ReturnType<typeof setTimeout>
58
+ private _qrcodeInterval?: ReturnType<typeof setInterval> | null
59
+ private _verifyInterval?: ReturnType<typeof setInterval> | null
60
+ private _qrcodeStatuasInterval?: ReturnType<typeof setInterval> | null
61
+ private _getQrcodeTimes: number
62
+ public static override readonly VERSION = VERSION
63
+ /**
64
+ * UUIDify:
65
+ * We need to clone a FileBox
66
+ * to set uuid loader/saver with this grpc client
67
+ */
68
+
69
+ constructor (public override options: PuppetEngineOptions = {} as PuppetEngineOptions) {
70
+ super(options)
71
+
72
+ this.options.engine = Client
73
+ this._getQrcodeTimes = 0
74
+ this.options.maxGetQrcoderTimes = 20
75
+
76
+ if (!this.options.token) {
77
+ const token = process.env['WECHATY_PUPPET_MATRIX_TOKEN'] || ''
78
+ if (token) {
79
+ this.options.token = token
80
+ }
81
+ }
82
+
83
+ if (!this.options.proxyId) {
84
+ const proxyId = process.env['WECHATY_PUPPET_MATRIX_PROXYID'] || ''
85
+ if (proxyId) {
86
+ this.options.proxyId = proxyId
87
+ }
88
+ }
89
+ }
90
+
91
+ public get client () {
92
+ return this._client
93
+ }
94
+
95
+ override async onStart (): Promise<void> {
96
+ log.verbose(PRE, 'onStart()')
97
+ await this._startClient()
98
+ }
99
+
100
+ /**
101
+ * 启动监听
102
+ * @private
103
+ */
104
+ private async _startClient () {
105
+ if (!this._client) {
106
+ this._client = await this.options.engine.create(this.options)
107
+ }
108
+
109
+ await this._startPuppetHeart(true)
110
+ if (this._client) {
111
+ this._client.on('login', this.wrapAsync(async ({ robotInfo } : { robotInfo: ContactPayload }) => {
112
+ log.info(PRE, `login success: ${robotInfo.name}`)
113
+ await this.onLogin(robotInfo)
114
+ }))
115
+ this._client.on('message', this.wrapAsync(async (message:MessagePayload) => {
116
+ await this._onPushMessage(message)
117
+ }))
118
+ this._client.on('contact', this.wrapAsync(async ({ contactInfo }:{ friendShip:PUPPET.payloads.FriendshipReceive, contactInfo: ContactPayload, type: PUPPET.types.Friendship }) => {
119
+ await this._cacheMgr!.setContact(contactInfo.wxid, contactInfo)
120
+ await this._cacheMgr?.setFriendshipRawPayload(contactInfo.wxid, {
121
+ id: contactInfo.wxid,
122
+ contactId: contactInfo.wxid,
123
+ timestamp: getUnixTime(Date.now()),
124
+ type: PUPPET.types.Friendship.Confirm,
125
+ })
126
+
127
+ this.emit('friendship', {
128
+ friendshipId: contactInfo.wxid,
129
+ type: PUPPET.types.Friendship.Confirm,
130
+ })
131
+ }))
132
+
133
+ this._client.on('update-contacts', this.wrapAsync(async (contactList: ContactPayload[]) => {
134
+ for (const contact of contactList) {
135
+ await this._onPushContact(contact)
136
+ }
137
+ }))
138
+ this._client.on('room-leave', this.wrapAsync(async ({ roomId, leaveIds }: { roomId: string, leaveIds: string[] }) => {
139
+ const payload = {
140
+ roomId,
141
+ removeeIdList: leaveIds,
142
+ removerId: this._self?.wxid,
143
+ timestamp: getUnixTime(Date.now()),
144
+ }
145
+ for (const leaveId of leaveIds) {
146
+ await this._cacheMgr?.deleteSingleRoomMember(roomId, leaveId)
147
+ const roomInfo = await this._cacheMgr?.getRoom(roomId)
148
+ if (roomInfo && roomInfo.chatroommemberList) {
149
+ roomInfo.chatroommemberList = roomInfo.chatroommemberList.filter(member => member !== leaveId)
150
+ await this._cacheMgr?.setRoom(roomId, roomInfo)
151
+ }
152
+ }
153
+
154
+ this.emit('room-leave', payload)
155
+ }))
156
+ this._client.on('room-join', this.wrapAsync(async ({ roomId, inviterId, joinIds, joinUsers }: { roomId: string, inviterId: string, joinIds: string[], joinUsers: ContactPayload[] }) => {
157
+ const payload = {
158
+ inviteeIdList : joinIds,
159
+ inviterId,
160
+ roomId,
161
+ timestamp : getUnixTime(Date.now()),
162
+ }
163
+ for (const joinUser of joinUsers) {
164
+ await this._cacheMgr?.updateSingleRoomMember(roomId, joinUser)
165
+ const roomInfo = await this._cacheMgr?.getRoom(roomId)
166
+ if (roomInfo && roomInfo.chatroommemberList) {
167
+ roomInfo.chatroommemberList.push(joinUser.wxid)
168
+ await this._cacheMgr?.setRoom(roomId, roomInfo)
169
+ }
170
+ }
171
+ this.emit('room-join', payload)
172
+ }))
173
+ this._client.on('logout', this.wrapAsync(async (message: string) => {
174
+ log.info(PRE, 'Logout event: %s', message)
175
+ await this.logout(message)
176
+ }))
177
+
178
+ this._client.on('verify-code', this.wrapAsync(async ({ ticket, uuid } : {ticket: string, uuid: string, status: number}) => {
179
+ log.info(PRE, 'verify-code: %s, %s', ticket, uuid)
180
+ this._qrcodeTicket = ticket
181
+ if (!this._verifyInterval) {
182
+ this._verifyInterval = setInterval(() => {
183
+ this.emit('verify-code', {
184
+ id: uuid,
185
+ scene: 1,
186
+ status: 1,
187
+ message: '',
188
+ } as any)
189
+ }, 5000)
190
+ }
191
+
192
+ }))
193
+ }
194
+ addRunningPuppet(this)
195
+ void this.checkIsLogin()
196
+
197
+ if (this._printVersion) {
198
+ // only print once
199
+ this._printVersion = false
200
+ log.info(`
201
+ ============================================================
202
+ Welcome to Wechaty Matrix puppet!
203
+
204
+ - puppet-Matrix version: ${VERSION}
205
+ ============================================================
206
+ `)
207
+ }
208
+ }
209
+
210
+ private async checkIsLogin () {
211
+ const info = await this._client?.getSelfInfo() as ContactPayload
212
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
213
+ if (info) {
214
+ log.info(PRE, `login success: ${info.name}`)
215
+ this.options.proxyId = ''
216
+ this._getQrcodeTimes = 0
217
+ await this._client?.setBridgeId('')
218
+ await this.onLogin(info)
219
+ } else {
220
+ await this._client?.setBridgeId(this.options.proxyId)
221
+ if (this._qrcodeInterval) {
222
+ clearInterval(this._qrcodeInterval)
223
+ this._qrcodeInterval = null
224
+ }
225
+ await this._getQrcode()
226
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
227
+ this._qrcodeInterval = setInterval(async () => {
228
+ await this._getQrcode()
229
+ }, 200000)
230
+ }
231
+ }
232
+
233
+ private getQrcodeTicket (authurl: string):string {
234
+ if (!authurl || !authurl.startsWith('http')) return ''
235
+ const url = new URL(authurl)
236
+ const searchParams = new URLSearchParams(url.search.slice(1))
237
+ return searchParams.get('ticket') || ''
238
+ }
239
+
240
+ private async _getQrcode () {
241
+ const qrcode = await this._client?.getQrcode()
242
+ if (qrcode === 'stop') {
243
+ this.options.proxyId = ''
244
+
245
+ if (this._qrcodeInterval) {
246
+ clearInterval(this._qrcodeInterval)
247
+ this._qrcodeInterval = null
248
+ }
249
+ if (this._qrcodeStatuasInterval) {
250
+ clearInterval(this._qrcodeStatuasInterval)
251
+ this._qrcodeStatuasInterval = null
252
+ }
253
+ return
254
+ }
255
+
256
+ if (qrcode) {
257
+ if (qrcode.isLogin) {
258
+ await this.checkIsLogin()
259
+ return
260
+ }
261
+ if (this._qrcodeStatuasInterval) {
262
+ clearInterval(this._qrcodeStatuasInterval)
263
+ this._qrcodeStatuasInterval = null
264
+ }
265
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
266
+ this._qrcodeStatuasInterval = setInterval(async () => {
267
+ const scanStatus = await this._client?.getQrcodeStatus() as { status: number, uuid: string, authUrl?: string }
268
+ if (scanStatus.status === 4 || scanStatus.status === 3) {
269
+ await this.checkIsLogin()
270
+ }
271
+ if (scanStatus.uuid === qrcode.uuid) {
272
+
273
+ if (scanStatus.authUrl) {
274
+ this._qrcodeTicket = this.getQrcodeTicket(scanStatus.authUrl)
275
+ if (!this._verifyInterval) {
276
+ this._verifyInterval = setInterval(() => {
277
+ this.emit('verify-code', {
278
+ id: scanStatus.uuid,
279
+ scene: 1,
280
+ status: 1,
281
+ message: '',
282
+ } as any)
283
+ }, 5000)
284
+ }
285
+ }
286
+ await this.onQrCodeEvent(scanStatus.status, qrcode.qrcodeUrl)
287
+ }
288
+
289
+ }, 8000)
290
+ } else {
291
+ if (this._getQrcodeTimes && this._getQrcodeTimes > 5) {
292
+ log.info(PRE, '由于多次请求二维码失败,已停止请求,请检查你的网络环境,重启容器后再试。')
293
+ return
294
+ }
295
+ this._getQrcodeTimes = this._getQrcodeTimes + 1
296
+
297
+ let tempTimer: any = setTimeout(() => {
298
+ void this._getQrcode()
299
+ tempTimer && clearTimeout(tempTimer)
300
+ tempTimer = null
301
+ }, 5000)
302
+ }
303
+ }
304
+
305
+ override async enterVerifyCode (id: string, code: string): Promise<void> {
306
+ log.verbose(PRE, 'enterVerifyCode(%s, %s)', id, code)
307
+ if (this._qrcodeTicket) {
308
+ await this._client?.verifyLoginUrl(this._qrcodeTicket, code)
309
+ return
310
+ }
311
+ log.error(PRE, 'enterVerifyCode(%s, %s) error, no qrcodeTicket', id, code)
312
+ }
313
+
314
+ private async onQrCodeEvent (status: number, url: string) {
315
+ const ScanStatusName = {
316
+ [PUPPET.types.ScanStatus.Unknown]: 'Unknown',
317
+ [PUPPET.types.ScanStatus.Cancel]: 'Cancel',
318
+ [PUPPET.types.ScanStatus.Waiting]: 'Waiting',
319
+ [PUPPET.types.ScanStatus.Scanned]: 'Scanned',
320
+ [PUPPET.types.ScanStatus.Confirmed]: 'Confirmed',
321
+ [PUPPET.types.ScanStatus.Timeout]: 'Timeout',
322
+ }
323
+ let scanStatus: PUPPET.types.ScanStatus = PUPPET.types.ScanStatus.Unknown
324
+
325
+ switch (status) {
326
+ case 0:
327
+ scanStatus = PUPPET.types.ScanStatus.Waiting
328
+ break
329
+ case 1:
330
+ scanStatus = PUPPET.types.ScanStatus.Scanned
331
+ break
332
+ case 2:
333
+ scanStatus = PUPPET.types.ScanStatus.Confirmed
334
+ break
335
+ case 3:
336
+ scanStatus = PUPPET.types.ScanStatus.Cancel
337
+ break
338
+ case 4:
339
+ scanStatus = PUPPET.types.ScanStatus.Timeout
340
+ break
341
+ }
342
+ log.silly(
343
+ PRE,
344
+ `scan event, status: ${ScanStatusName[scanStatus]}${url ? ', with qrcode: ' + url : ''}`,
345
+ )
346
+
347
+ this.emit('scan', {
348
+ qrcode: url,
349
+ status: scanStatus,
350
+ })
351
+ // 如果被取消 或者 超时 重新拉取二维码
352
+ if (status === 4 || status === 3) {
353
+ await this.checkIsLogin()
354
+ }
355
+ }
356
+
357
+ // 登录
358
+ private async onLogin (user: ContactPayload):Promise<void> {
359
+ try {
360
+ // create cache manager firstly
361
+ if (!this._client) {
362
+ this._client = await this.options.engine.create(this.options)
363
+ }
364
+ if (!this._cacheMgr) {
365
+ this._cacheMgr = new CacheManager(user.wxid)
366
+ await this._cacheMgr.init()
367
+ await this._cacheMgr!.setContact(user.wxid, user)
368
+ }
369
+ if (this._verifyInterval) {
370
+ clearInterval(this._verifyInterval)
371
+ this._verifyInterval = null
372
+ }
373
+ if (this._qrcodeInterval) {
374
+ clearInterval(this._qrcodeInterval)
375
+ this._qrcodeInterval = null
376
+ }
377
+ if (this._qrcodeStatuasInterval) {
378
+ clearInterval(this._qrcodeStatuasInterval)
379
+ this._qrcodeStatuasInterval = null
380
+ }
381
+ this._self = user
382
+
383
+ await super.login(user.wxid)
384
+ void this._client?.pushLoginNotify()
385
+ await this.ready()
386
+ } catch (e) {
387
+ log.error('error login', e)
388
+ }
389
+ }
390
+
391
+ public async ready (): Promise<void> {
392
+ try {
393
+ await this._client?.getContactList(1, 100)
394
+ await delay(2000)
395
+ await this._client?.getGroupList(1, 100)
396
+ await delay(2000)
397
+ await this._client?.getOfficeList(1, 100)
398
+
399
+ log.silly(PRE, 'on ready')
400
+
401
+ setTimeout(() => {
402
+ this.emit('ready', {
403
+ data: 'ready',
404
+ })
405
+ }, 10000)
406
+ } catch (e) {
407
+ log.error('ready error', e)
408
+ }
409
+ }
410
+
411
+ public async onStop (): Promise<void> {
412
+ await this._stopClient()
413
+ }
414
+
415
+ private async _stopClient (): Promise<void> {
416
+ this.__currentUserId = undefined
417
+ this.__currentUserId = undefined
418
+ if (this._cacheMgr) {
419
+ log.info(PRE, 'colse cache')
420
+ await this._cacheMgr.close()
421
+ this._cacheMgr = undefined
422
+ }
423
+ removeRunningPuppet(this)
424
+ this._stopPuppetHeart()
425
+ }
426
+
427
+ // 登出
428
+ override async logout (reason?: string): Promise<void> {
429
+ if (!this.isLoggedIn) {
430
+ return
431
+ }
432
+ await this._client?.logout()
433
+
434
+ await super.logout(reason)
435
+ void this.checkIsLogin()
436
+ }
437
+
438
+ override ding (data?: string): void {
439
+ const eventDongPayload = {
440
+ data: data ? data! : 'ding-dong',
441
+ }
442
+ this.emit('dong', eventDongPayload)
443
+ }
444
+
445
+ override async refreshQRCode (): Promise<void> {
446
+ log.verbose(PRE, 'refreshQRCode(%s)')
447
+ await this.checkIsLogin()
448
+ }
449
+
450
+ public async stopRefreshQRCode (): Promise<void> {
451
+ log.verbose(PRE, 'stopRefreshQRCode(%s)')
452
+ if (this._qrcodeInterval) {
453
+ clearInterval(this._qrcodeInterval)
454
+ this._qrcodeInterval = null
455
+ }
456
+ if (this._qrcodeStatuasInterval) {
457
+ clearInterval(this._qrcodeStatuasInterval)
458
+ this._qrcodeStatuasInterval = null
459
+ }
460
+ }
461
+
462
+ /****************************************************************************
463
+ * contact
464
+ ***************************************************************************/
465
+
466
+ /**
467
+ *
468
+ * ContactSelf
469
+ *
470
+ *
471
+ */
472
+ // 设置自己的昵称 暂不支持
473
+ override async contactSelfName (name: string): Promise<void> {
474
+ return PUPPET.throwUnsupportedError(name)
475
+ }
476
+
477
+ // 获取自己的二维码
478
+ override async contactSelfQRCode (): Promise<string> {
479
+ return this._client?.getUserQrcode() || ''
480
+ }
481
+
482
+ // 设置自己的签名 暂不支持
483
+ override async contactSelfSignature (signature: string): Promise<void> {
484
+ return PUPPET.throwUnsupportedError(signature)
485
+ }
486
+
487
+ // 获取用户的手机号 暂不支持
488
+ override async contactPhone (contactId: string, phoneList: string[]): Promise<void> {
489
+ return PUPPET.throwUnsupportedError(contactId, phoneList)
490
+ }
491
+
492
+ // 查询或设置用户备注
493
+ override contactAlias (contactId: string) : Promise<string>
494
+ override contactAlias (contactId: string, alias: string | null): Promise<void>
495
+
496
+ override async contactAlias (contactId : string, alias? : string | null): Promise<string | void> {
497
+ const contact = await this.contactRawPayload(contactId)
498
+ if (alias) {
499
+ // contact is stranger, set alias in cache, to update after user is added
500
+ if (contact) {
501
+ if (contact.wxid.indexOf(STRANGER_SUFFIX) !== -1) {
502
+ await this._cacheMgr!.setContactStrangerAlias(contact.wxid, alias)
503
+
504
+ // to suppress warning: 15:31:06 WARN Contact alias(asd3) sync with server fail: set(asd3) is not equal to get()
505
+ if (contactId.startsWith(SEARCH_CONTACT_PREFIX)) {
506
+ const searchContact = await this._cacheMgr?.getContactSearch(contactId)
507
+ if (searchContact) {
508
+ searchContact.remark = alias
509
+ await this._cacheMgr!.setContactSearch(contactId, searchContact)
510
+ }
511
+ }
512
+ } else {
513
+ await this._client?.setContactAlias(contactId, alias)
514
+ contact.remark = alias
515
+ await this._updateContactCache(contact)
516
+ }
517
+ }
518
+ } else {
519
+ return contact && contact.remark
520
+ }
521
+ }
522
+
523
+ // 获取用户头像
524
+ override async contactAvatar (contactId: string) : Promise<FileBoxInterface>
525
+ override async contactAvatar (contactId: string, file: FileBoxInterface) : Promise<void>
526
+
527
+ override async contactAvatar (contactId: string, file?: FileBoxInterface) : Promise<void | FileBoxInterface> {
528
+ if (file) {
529
+ return PUPPET.throwUnsupportedError('set avatar is not unsupported')
530
+ }
531
+ const contact = await this.contactRawPayload(contactId)
532
+ if (contact && contact.avatar) {
533
+ return FileBox.fromUrl(contact.avatar, { name: `avatar-${contactId}.jpg` })
534
+ }
535
+ }
536
+
537
+ // 获取用户列表
538
+ override async contactList (): Promise<string[]> {
539
+ return this._cacheMgr!.getContactIds()
540
+ }
541
+
542
+ // 公司备注 暂不支持
543
+ override async contactCorporationRemark (contactId: string, corporationRemark: string | null) {
544
+ return PUPPET.throwUnsupportedError(contactId, corporationRemark)
545
+ }
546
+
547
+ // 其他备注 暂不支持
548
+ override async contactDescription (contactId: string, description: string | null) {
549
+ return PUPPET.throwUnsupportedError(contactId, description)
550
+ }
551
+
552
+ // 删除联系人
553
+ override async contactDelete (contactId: string): Promise<void> {
554
+ const contact = await this._refreshContact(contactId)
555
+ if (contact && contact.isFriend === 2) {
556
+ log.warn(`can not delete contact which is not a friend:: ${contactId}`)
557
+ return
558
+ }
559
+ await this._client?.removeContact(contactId)
560
+ await this._refreshContact(contactId, 2)
561
+ }
562
+
563
+ // 添加标签 暂不支持
564
+ override async tagContactTagAdd (tagIds: string[], contactIds: string[]): Promise<void> {
565
+ return PUPPET.throwUnsupportedError(tagIds, contactIds)
566
+ }
567
+
568
+ // 删除用户标签 暂不支持
569
+ override async tagContactTagRemove (tagIds: string[], contactIds: string[]): Promise<void> {
570
+ return PUPPET.throwUnsupportedError(tagIds, contactIds)
571
+ }
572
+
573
+ // 删除标签
574
+ override async tagTagDelete (tagIdList: string[]) : Promise<void> {
575
+ return PUPPET.throwUnsupportedError(tagIdList)
576
+ }
577
+
578
+ // 获取用户标签
579
+ override async tagTagContactList (contactId?: string) : Promise<string[]> {
580
+ return PUPPET.throwUnsupportedError(contactId)
581
+ }
582
+
583
+ /****************************************************************************
584
+ * friendship
585
+ ***************************************************************************/
586
+ /**
587
+ * 通过好友请求
588
+ * @param friendshipId
589
+ */
590
+ override async friendshipAccept (friendshipId : string): Promise<void> {
591
+ const friendship: PUPPET.payloads.FriendshipReceive = (await this.friendshipRawPayload(
592
+ friendshipId,
593
+ )) as PUPPET.payloads.FriendshipReceive
594
+ const userName = friendship.contactId
595
+
596
+ // FIXME: workaround to make accept enterprise account work. can be done in a better way
597
+ if (isIMContactId(userName)) {
598
+ await this._refreshContact(userName)
599
+ }
600
+ await this._client?.confirmFriendship(userName, friendship.scene, friendship.ticket)
601
+ }
602
+
603
+ /**
604
+ * 主动添加好友
605
+ * @param contactId
606
+ * @param option
607
+ */
608
+ override async friendshipAdd (contactId: string, option?: PUPPET.types.FriendshipAddOptions): Promise<void> {
609
+ let stranger: number | undefined
610
+ let ticket: string
611
+ let addContactScene
612
+ const cachedContactSearch = await this._cacheMgr!.getContactSearch(contactId)
613
+ // 通过陌生人查找用户 手机或者qq
614
+ if (cachedContactSearch) {
615
+ stranger = cachedContactSearch.isFriend || undefined
616
+ ticket = cachedContactSearch.ticket || ''
617
+ addContactScene = cachedContactSearch.scene
618
+ } else {
619
+ // 通过wxid 查找
620
+ const contactPayload = await this.contactRawPayload(contactId)
621
+ const contactAlias = contactPayload?.remark
622
+ if (!contactAlias) {
623
+ // add contact from room,
624
+ const roomIds = await this._findRoomIdForWxid(contactId)
625
+ if (!roomIds.length) {
626
+ throw new Error(`Can not find room for contact while adding friendship: ${contactId}`)
627
+ }
628
+ const res = await this._client?.searchContact([ contactId ]) as ContactPayload | undefined
629
+ if (res) {
630
+ await this._updateContactCache(res)
631
+ }
632
+ addContactScene = '14' // 1=qq 3=微信号 6=单向添加 10和13=通讯录 14=群聊 15=手机号 17=名片 30=扫一扫
633
+ }
634
+ const res = await this.contactRawPayload(contactId)
635
+ if (res?.isFriend === 1) {
636
+ throw new Error(`contact:${contactId} is already a friend`)
637
+ }
638
+ // 通过wxid加好友
639
+ ticket = ''
640
+ addContactScene = '6' // 1=qq 3=微信号 6=单向添加 10和13=通讯录 14=群聊 15=手机号 17=名片 30=扫一扫
641
+ }
642
+ let hello: string | undefined
643
+ if (stranger === 1) {
644
+ throw new Error(`contact:${contactId} is already a friend`)
645
+ }
646
+ if (option) {
647
+ if (typeof option === 'string') {
648
+ hello = option
649
+ } else {
650
+ hello = (option as any).hello
651
+ }
652
+ }
653
+ await this._client?.addFriendByWxid({ content: hello, ticket, scene: addContactScene, wxid: contactId })
654
+ }
655
+
656
+ /**
657
+ * 根据手机号查询好友
658
+ * @param phone
659
+ */
660
+ override async friendshipSearchPhone (phone: string): Promise<null | string> {
661
+ return this._friendshipSearch(phone, '15')
662
+ }
663
+
664
+ /**
665
+ * 根据qq号查询好友
666
+ * @param qq
667
+ */
668
+ override async friendshipSearchHandle (qq: string): Promise<null | string> {
669
+ return this._friendshipSearch(qq, '1')
670
+ }
671
+
672
+ /**
673
+ * 陌生人查询
674
+ * @param id
675
+ * @param scene 场景值 1=qq 3=微信号 6=单向添加 10和13=通讯录 14=群聊 15=手机号 17=名片 30=扫一扫
676
+ * @private
677
+ */
678
+ private async _friendshipSearch (id: string, scene?:string): Promise<null | string> {
679
+ const cachedContactSearch = await this._cacheMgr!.getContactSearch(id)
680
+ if (cachedContactSearch) {
681
+ return id
682
+ }
683
+
684
+ const res = await this._client?.searchStranger(id)
685
+ const searchId = `${SEARCH_CONTACT_PREFIX}${id}`
686
+ if (res) {
687
+ await this._cacheMgr!.setContactSearch(searchId, { ...res, scene })
688
+ }
689
+ return searchId
690
+ }
691
+
692
+ /**
693
+ * 根据wxid 查询群id
694
+ * @param wxid
695
+ * @private
696
+ */
697
+ private async _findRoomIdForWxid (wxid: string): Promise<string[]> {
698
+ const ret = []
699
+
700
+ const roomIds = (await this._cacheMgr?.getRoomIds()) || []
701
+ for (const roomId of roomIds) {
702
+ const roomMember = await this._cacheMgr?.getRoomMember(roomId)
703
+ if (!roomMember) {
704
+ continue
705
+ }
706
+
707
+ const roomMemberIds = Object.keys(roomMember)
708
+ if (roomMemberIds.indexOf(wxid) !== -1) {
709
+ ret.push(roomId)
710
+ }
711
+ }
712
+
713
+ return ret
714
+ }
715
+
716
+ /****************************************************************************
717
+ * get message payload
718
+ ***************************************************************************/
719
+ // 名片
720
+ override async messageContact (messageId: string): Promise<string> {
721
+ log.verbose('PuppetWeChat', 'messageContact(%s)', messageId)
722
+ return PUPPET.throwUnsupportedError(messageId)
723
+ }
724
+
725
+ override async messageChannel (
726
+ messageId: string,
727
+ ): Promise<PUPPET.payloads.Channel> {
728
+ log.verbose('PuppetService', 'messageChannel(%s)', messageId)
729
+
730
+ const rawPayload = await this.messageRawPayload(messageId)
731
+ const payload = await this.messageRawPayloadParser(rawPayload)
732
+
733
+ if (payload.type !== PUPPET.types.Message.Channel) {
734
+ throw new Error('Can not get channel from no Channel payload')
735
+ }
736
+
737
+ // FIXME: thumb may not in appPayload.thumburl, but in appPayload.appAttachPayload
738
+ const appPayload = await parseAppmsgMessagePayload(rawPayload.msg)
739
+ const channelPayload = {
740
+ avatar: appPayload.channel?.avatar || '',
741
+ coverUrl: appPayload.channel?.mediaList?.media?.coverUrl || '',
742
+ desc: appPayload.channel?.desc || '',
743
+ extras: '',
744
+ feedType: appPayload.channel?.feedType || PUPPET.types.ChannelFeed.VIDEO,
745
+ nickname: appPayload.channel?.nickname || '',
746
+ thumbUrl: appPayload.channel?.mediaList?.media?.thumbUrl || '',
747
+ url: appPayload.channel?.mediaList?.media?.url || '',
748
+ objectId: appPayload.channel?.objectId || '',
749
+ objectNonceId: appPayload.channel?.objectNonceId || '',
750
+ }
751
+
752
+ return channelPayload
753
+ }
754
+
755
+ // 文件消息
756
+ override async messageFile (messageId: string): Promise<FileBoxInterface> {
757
+ const messagePayload: MessagePayload = await this.messageRawPayload(messageId)
758
+ const message: PUPPET.payloads.Message = await this.messageRawPayloadParser(messagePayload)
759
+ switch (message.type) {
760
+ // 图片
761
+ case PUPPET.types.Message.Image:
762
+ return this._getMessageImageFileBox(messageId, messagePayload)
763
+ case PUPPET.types.Message.Audio:
764
+ return this._getMessageAudioFileBox(messageId, messagePayload)
765
+ case PUPPET.types.Message.Video: {
766
+ return this._getMessageVideoFileBox(messageId, messagePayload)
767
+ }
768
+ case PUPPET.types.Message.Attachment:
769
+ return this._getMessageFileFileBox(messageId, messagePayload)
770
+ case PUPPET.types.Message.Emoticon: {
771
+ const emotionPayload = await parseEmotionMessagePayload(messagePayload)
772
+ const emoticonBox = FileBox.fromUrl(emotionPayload.cdnurl, { name: `message-${messageId}-emoticon.jpg` })
773
+
774
+ emoticonBox.metadata = {
775
+ payload: emotionPayload,
776
+ type: 'emoticon',
777
+ }
778
+
779
+ return emoticonBox
780
+ }
781
+ case PUPPET.types.Message.MiniProgram:
782
+ return PUPPET.throwUnsupportedError(messageId)
783
+ case PUPPET.types.Message.Url:
784
+ return PUPPET.throwUnsupportedError(messageId)
785
+ default:
786
+ throw new Error(`Can not get file for message: ${messageId}`)
787
+ }
788
+ }
789
+
790
+ /**
791
+ * 解析图片消息
792
+ * @param messageId
793
+ */
794
+ override async messageImage (messageId: string): Promise<FileBoxInterface> {
795
+ const messagePayload: MessagePayload = await this.messageRawPayload(messageId)
796
+ return this._getMessageImageFileBox(messageId, messagePayload)
797
+ }
798
+
799
+ /**
800
+ * 解析小程序
801
+ * @param messageId
802
+ */
803
+ override async messageMiniProgram (messageId: string): Promise<PUPPET.payloads.MiniProgram> {
804
+ const messagePayload = await this.messageRawPayload(messageId)
805
+ const message = await this.messageRawPayloadParser(messagePayload)
806
+
807
+ if (message.type !== PUPPET.types.Message.MiniProgram) {
808
+ throw new Error('message is not mini program, can not get MiniProgramPayload')
809
+ }
810
+
811
+ return parseMiniProgramMessagePayload(messagePayload)
812
+ }
813
+
814
+ /**
815
+ * 解析h5链接
816
+ * @param messageId
817
+ */
818
+ override async messageUrl (messageId: string) : Promise<PUPPET.payloads.UrlLink> {
819
+ const rawPayload = await this.messageRawPayload(messageId)
820
+ const payload = await this.messageRawPayloadParser(rawPayload)
821
+
822
+ if (payload.type !== PUPPET.types.Message.Url) {
823
+ throw new Error('Can not get url from non url payload')
824
+ }
825
+
826
+ // FIXME: thumb may not in appPayload.thumburl, but in appPayload.appAttachPayload
827
+ const appPayload = await parseAppmsgMessagePayload(rawPayload.msg)
828
+ return {
829
+ description: appPayload.des,
830
+ thumbnailUrl: appPayload.thumburl,
831
+ title: appPayload.title,
832
+ url: appPayload.url,
833
+ }
834
+ }
835
+
836
+ /**
837
+ * 解析h5链接
838
+ * @param messageId
839
+ */
840
+ override async messageLocation (messageId: string) : Promise<PUPPET.payloads.Location> {
841
+ const rawPayload = await this.messageRawPayload(messageId)
842
+ const payload = await this.messageRawPayloadParser(rawPayload)
843
+
844
+ if (payload.type !== PUPPET.types.Message.Location) {
845
+ throw new Error('Can not get location from non location payload')
846
+ }
847
+
848
+ // FIXME: thumb may not in appPayload.thumburl, but in appPayload.appAttachPayload
849
+ const locationPayload = await parseLocationMessagePayload(rawPayload.msg) as PUPPET.payloads.Location
850
+ return locationPayload
851
+ }
852
+
853
+ /****************************************************************************
854
+ * send message
855
+ ***************************************************************************/
856
+ // 发送名片
857
+ override async messageSendContact (toUserId: string, contactId: string): Promise<void | string> {
858
+ log.verbose('PuppetWeChat', 'messageSend("%s", %s)', toUserId, contactId)
859
+ const contactPayload = await this.contactRawPayload(contactId)
860
+ const xmlObj = {
861
+ msg: {
862
+ antispamticket: '', // TODO 需要考虑怎么设置
863
+ bigheadimgurl: contactPayload?.avatar,
864
+ brandFlags: '0',
865
+ certflag: '0',
866
+ city: contactPayload?.city,
867
+ fullpy: contactPayload?.name,
868
+ imagestatus: '3',
869
+ nickname: contactPayload?.name,
870
+ province: contactPayload?.province,
871
+ regionCode: 'CN_Shanghai',
872
+ scene: '17',
873
+ sex: contactPayload?.sex,
874
+ smallheadimgurl: contactPayload?.avatar,
875
+ username: contactPayload?.ticket,
876
+ },
877
+ }
878
+ const xml = JsonToXml(xmlObj)
879
+ const res = await this._client?.sendContactCard(toUserId, xml, this._self?.wxid)
880
+ if (res) {
881
+ await this._cacheMgr?.setMessage(res.id, res)
882
+ return res.id
883
+ }
884
+ }
885
+
886
+ // 发送文件
887
+ override async messageSendFile (conversationId: string, fileBox: FileBoxInterface): Promise<string | void> {
888
+ const metadata: FileBoxMetadataMessage = fileBox.metadata as FileBoxMetadataMessage
889
+
890
+ const fileType
891
+ = fileBox.mediaType && fileBox.mediaType !== 'application/octet-stream'
892
+ ? fileBox.mediaType
893
+ : path.extname(fileBox.name)
894
+
895
+ // @ts-ignore
896
+ const fileUrl = fileBox.remoteUrl || ''
897
+ if (metadata.type === 'emoticon') {
898
+ PUPPET.throwUnsupportedError(conversationId, fileBox)
899
+ } else if (fileBox.mediaType.startsWith('image/')) {
900
+ if (fileUrl) {
901
+ const msgInfo = await this._client?.sendUrlImg(conversationId, fileUrl, this._self?.wxid)
902
+
903
+ if (msgInfo) {
904
+ await this._cacheMgr?.setMessage(msgInfo.id, msgInfo)
905
+ return msgInfo.id
906
+ }
907
+
908
+ }
909
+ } else if (fileBox.mediaType === 'audio/silk') {
910
+ PUPPET.throwUnsupportedError(conversationId, fileBox)
911
+ } else if (fileType.includes('video/mp4') || fileType.includes('.mp4')) {
912
+ const msgInfo = await this._client?.sendVideo(conversationId, fileUrl, this._self?.wxid)
913
+ if (msgInfo) {
914
+ await this._cacheMgr?.setMessage(msgInfo.id, msgInfo)
915
+ return msgInfo.id
916
+ }
917
+ } else {
918
+ if (fileUrl) {
919
+ const msgInfo = await this._client?.sendCloudFile(conversationId, fileUrl, this._self?.wxid)
920
+ if (msgInfo) {
921
+ await this._cacheMgr?.setMessage(msgInfo.id, msgInfo)
922
+ return msgInfo.id
923
+ }
924
+ }
925
+ }
926
+ }
927
+
928
+ // 发送小程序
929
+ override async messageSendMiniProgram (toUserName: string, mpPayload: PUPPET.payloads.MiniProgram): Promise<void | string> {
930
+ const miniProgram = {
931
+ appid: mpPayload.appid,
932
+ contactId: toUserName,
933
+ content: mpPayload.description,
934
+ username: mpPayload.username,
935
+ pagePath: mpPayload.pagePath,
936
+ thumbUrl: mpPayload.thumbUrl,
937
+ iconUrl: mpPayload.iconUrl,
938
+ title: mpPayload.title,
939
+ }
940
+
941
+ if (!mpPayload.thumbUrl) {
942
+ log.warn(PRE, 'no thumb image found while sending mimi program')
943
+ }
944
+
945
+ const res = await this._client?.sendMiniProgram(miniProgram, this._self?.wxid)
946
+ if (res) {
947
+ await this._cacheMgr?.setMessage(res.id, res)
948
+ return res.id
949
+ }
950
+ }
951
+
952
+ // 发送文字
953
+ override async messageSendText (conversationId: string, text: string, mentionIdList?: string[]): Promise<string | void> {
954
+ if (mentionIdList && mentionIdList.length) {
955
+ const mentionContact = await Promise.all(mentionIdList.map(async (mentionId) => {
956
+ const contact = await this.contactRawPayload(mentionId)
957
+ if (contact) {
958
+ return '@' + (contact.alias || contact.name)
959
+ }
960
+ return '@' + (mentionId === '@all' ? 'all' : '')
961
+ }))
962
+ const mentionText = mentionContact.join(String.fromCharCode(0x2005))
963
+ console.log('metionContact********', mentionText)
964
+ text = text.replaceAll(mentionText, '').trim()
965
+ const msgInfo = await this._client?.sendAtText(conversationId, text, mentionIdList, this._self?.wxid)
966
+ if (msgInfo) {
967
+ await this._cacheMgr?.setMessage(msgInfo.id, msgInfo)
968
+ return msgInfo.id
969
+ }
970
+ }
971
+
972
+ const msgInfo = await this._client?.sendText(conversationId, text, this._self?.wxid)
973
+
974
+ if (msgInfo) {
975
+ await this._cacheMgr?.setMessage(msgInfo.id, msgInfo)
976
+ return msgInfo.id
977
+ }
978
+ }
979
+
980
+ // 发送h5链接
981
+ override async messageSendUrl (conversationId: string, urlLinkPayload: PUPPET.payloads.UrlLink): Promise<string | void> {
982
+ const urlCard = {
983
+ contactId: conversationId,
984
+ content: urlLinkPayload.description,
985
+ jumpUrl: urlLinkPayload.url,
986
+ imageUrl: urlLinkPayload.thumbnailUrl,
987
+ title: urlLinkPayload.title,
988
+ }
989
+ if (!urlLinkPayload.thumbnailUrl) {
990
+ log.warn(PRE, 'no thumb image found while sending mimi program')
991
+ }
992
+ const msgInfo = await this._client?.sendShareCard(urlCard, this._self?.wxid)
993
+
994
+ if (msgInfo) {
995
+ await this._cacheMgr?.setMessage(msgInfo.id, msgInfo)
996
+ return msgInfo.id
997
+ }
998
+ }
999
+
1000
+ /**
1001
+ * 确认收款
1002
+ */
1003
+ override async messageSendPost (conversationId: string, postPayload: PUPPET.payloads.Post): Promise<void> {
1004
+ const msgType = postPayload.sayableList[0] as PUPPET.payloads.Sayable
1005
+ if (msgType.type !== 'Text') {
1006
+ throw new Error(`Wrong Post!!! please check your Post payload to make sure it right ${conversationId}`)
1007
+ }
1008
+ }
1009
+
1010
+ /**
1011
+ * 消息撤回 暂不支持
1012
+ * @param messageId
1013
+ */
1014
+ override async messageRecall (messageId: string): Promise<boolean> {
1015
+ return PUPPET.throwUnsupportedError(messageId)
1016
+ }
1017
+
1018
+ /**
1019
+ * 消息转发
1020
+ * @param toUserName
1021
+ * @param messageId
1022
+ */
1023
+ override async messageForward (toUserName: string, messageId: string): Promise<void> {
1024
+ const messagePayload = await this.messageRawPayload(messageId)
1025
+ const message = await this.messageRawPayloadParser(messagePayload)
1026
+
1027
+ switch (message.type) {
1028
+ case PUPPET.types.Message.Text:
1029
+ await this.messageSendText(toUserName, message.text!)
1030
+ break
1031
+
1032
+ case PUPPET.types.Message.Image: {
1033
+ const imageFileBox = await this.messageImage(messageId)
1034
+ await this.messageSendFile(toUserName, imageFileBox)
1035
+ break
1036
+ }
1037
+ case PUPPET.types.Message.Audio: {
1038
+ const audioFileBox = await this.messageFile(messageId)
1039
+ await this.messageSendFile(toUserName, audioFileBox)
1040
+ break
1041
+ }
1042
+ case PUPPET.types.Message.Video: {
1043
+ const videoFileBox = await this.messageFile(messageId)
1044
+ await this.messageSendFile(toUserName, videoFileBox)
1045
+ break
1046
+ }
1047
+ default:
1048
+ throw new Error(`Message forwarding is unsupported for messageId:${messageId}, type:${message.type}`)
1049
+ }
1050
+ }
1051
+
1052
+ /****************************************************************************
1053
+ * room
1054
+ ***************************************************************************/
1055
+
1056
+ // 拉人进群
1057
+ override async roomAdd (roomId : string, contactId : string): Promise<void> {
1058
+ let type:number = 1 // 1 直接拉 2 发送邀请链接 人数超过40需要对方同意
1059
+ if (roomId) {
1060
+ const ret = await this.roomRawPayload(roomId)
1061
+ if (ret && ret.memberNum && ret.memberNum > 38) {
1062
+ type = 2
1063
+ }
1064
+ }
1065
+ await this._client?.inviteToGroup(roomId, contactId, type)
1066
+ }
1067
+
1068
+ // 获取群头像
1069
+ override async roomAvatar (roomId: string): Promise<FileBoxInterface> {
1070
+ const chatroom = await this.roomRawPayload(roomId)
1071
+ if (chatroom && chatroom.avatar) {
1072
+ return FileBox.fromUrl(chatroom.avatar)
1073
+ } else {
1074
+ // return dummy FileBox object
1075
+ return FileBox.fromBuffer(Buffer.from(new ArrayBuffer(0)), 'room-avatar.jpg')
1076
+ }
1077
+ }
1078
+
1079
+ // 创建群聊
1080
+ override async roomCreate (
1081
+ contactIdList : string[],
1082
+ topic : string,
1083
+ ): Promise<string> {
1084
+ return this._client?.creatRoom(contactIdList, topic) || ''
1085
+ }
1086
+
1087
+ // 删除群聊 暂不支持
1088
+ override async roomDel (
1089
+ roomId : string,
1090
+ contactId : string,
1091
+ ): Promise<void> {
1092
+ return PUPPET.throwUnsupportedError(roomId, contactId)
1093
+ }
1094
+
1095
+ // 获取群聊列表
1096
+ override async roomList (): Promise<string[]> {
1097
+ return this._cacheMgr!.getRoomIds()
1098
+ }
1099
+
1100
+ // 获取群二维码 暂不支持
1101
+ override async roomQRCode (roomId: string): Promise<string> {
1102
+ return PUPPET.throwUnsupportedError(roomId)
1103
+ }
1104
+
1105
+ // 机器人退出群聊
1106
+ override async roomQuit (roomId: string): Promise<void> {
1107
+ return this._client?.roomQuit(roomId)
1108
+ }
1109
+
1110
+ override async roomTopic (roomId: string) : Promise<string>
1111
+ override async roomTopic (roomId: string, topic: string) : Promise<void>
1112
+
1113
+ // 修改群名称
1114
+ override async roomTopic (
1115
+ roomId : string,
1116
+ topic? : string,
1117
+ ): Promise<void | string> {
1118
+ await this._client?.setGroupName(roomId, topic)
1119
+ }
1120
+
1121
+ override async roomAnnounce (roomId: string) : Promise<string>
1122
+ override async roomAnnounce (roomId: string, text: string) : Promise<void>
1123
+
1124
+ // 修改群公告
1125
+ override async roomAnnounce (roomId: string, text?: string) : Promise<void | string> {
1126
+ if (text !== undefined) {
1127
+ await this._client?.sendAnnouncement(roomId, text)
1128
+ }
1129
+ return ''
1130
+ }
1131
+
1132
+ // 获取群成员列表
1133
+ override async roomMemberList (roomId: string) : Promise<string[]> {
1134
+ const roomMemberMap = await this._getRoomMemberList(roomId)
1135
+ return Object.values(roomMemberMap).map((m) => m.wxid)
1136
+ }
1137
+
1138
+ // 接受群邀请 暂不支持
1139
+ override async roomInvitationAccept (roomInvitationId: string): Promise<void> {
1140
+ return PUPPET.throwUnsupportedError(roomInvitationId)
1141
+ }
1142
+
1143
+ /****************************************************************************
1144
+ * RawPayload section
1145
+ ***************************************************************************/
1146
+
1147
+ // 解析联系人信息格式化为Wechaty 格式
1148
+ override async contactRawPayloadParser (payload: ContactPayload): Promise<PUPPET.payloads.Contact> {
1149
+ return engineContactToWechaty(payload)
1150
+ }
1151
+
1152
+ // 获取联系人信息 原格式
1153
+ override async contactRawPayload (id: string): Promise<ContactPayload | undefined> {
1154
+ log.silly(PRE, 'contactRawPayload(%s) @ %s', id, this)
1155
+ if (id.startsWith(SEARCH_CONTACT_PREFIX)) {
1156
+ const searchContact = await this._cacheMgr?.getContactSearch(id)
1157
+ return searchContact
1158
+ }
1159
+
1160
+ let ret = await this._cacheMgr!.getContact(id)
1161
+ if (!ret) {
1162
+ ret = await CachedPromiseFunc(`contactRawPayload-${id}`, async () => {
1163
+ const contact = await this._refreshContact(id)
1164
+ return contact
1165
+ })
1166
+ return ret
1167
+ }
1168
+ return ret
1169
+ }
1170
+
1171
+ /**
1172
+ * 解析原始消息体为Wechaty支持的格式
1173
+ * @param payload
1174
+ */
1175
+ override async messageRawPayloadParser (payload: MessagePayload): Promise<PUPPET.payloads.Message> {
1176
+ return engineMessageToWechaty(this, payload)
1177
+ }
1178
+
1179
+ /**
1180
+ * 根据消息id 获取消息
1181
+ * @param id
1182
+ */
1183
+ override async messageRawPayload (id: string): Promise<MessagePayload> {
1184
+ const ret = await this._cacheMgr!.getMessage(id)
1185
+ if (!ret) {
1186
+ throw new Error(`can not find message in cache for messageId: ${id}`)
1187
+ }
1188
+
1189
+ return ret
1190
+ }
1191
+
1192
+ /**
1193
+ * 群数据格式化为Wechaty 支持类型
1194
+ * @param rawPayload
1195
+ */
1196
+ override async roomRawPayloadParser (payload: ContactPayload): Promise<PUPPET.payloads.Room> {
1197
+ return engineRoomToWechaty(payload)
1198
+ }
1199
+
1200
+ /**
1201
+ * 查找群基础信息
1202
+ * @param id
1203
+ */
1204
+ override async roomRawPayload (id: string): Promise<ContactPayload|undefined> {
1205
+ let ret = await this._cacheMgr!.getRoom(id)
1206
+ if (!ret) {
1207
+ const contact = await this._refreshContact(id)
1208
+ ret = contact
1209
+ }
1210
+ return ret
1211
+ }
1212
+
1213
+ /**
1214
+ * 查找群成员信息
1215
+ * @param roomId
1216
+ * @param contactId
1217
+ */
1218
+ override async roomMemberRawPayload (roomId: string, contactId: string): Promise<ContactPayload> {
1219
+ const roomMemberMap = await this._getRoomMemberList(roomId)
1220
+ if (roomMemberMap[contactId]) {
1221
+ return roomMemberMap[contactId]!
1222
+ }
1223
+ const newMap = await this._getRoomMemberList(roomId, true)
1224
+
1225
+ return newMap[contactId]!
1226
+ }
1227
+
1228
+ /**
1229
+ * 解析群成员信息
1230
+ * @param rawPayload
1231
+ */
1232
+ override async roomMemberRawPayloadParser (rawPayload: ContactPayload): Promise<PUPPET.payloads.RoomMember> {
1233
+ return engineRoomMemberToWechaty(rawPayload)
1234
+ }
1235
+
1236
+ /**
1237
+ * 接收群邀请信息 暂不支持
1238
+ * @param roomInvitationId
1239
+ */
1240
+ override async roomInvitationRawPayload (roomInvitationId: string): Promise<any> {
1241
+ return PUPPET.throwUnsupportedError(roomInvitationId)
1242
+ }
1243
+
1244
+ /**
1245
+ * 解析群邀请信息 暂不支持
1246
+ * @param rawPayload
1247
+ */
1248
+ override async roomInvitationRawPayloadParser (rawPayload: any): Promise<PUPPET.payloads.RoomInvitation> {
1249
+ return PUPPET.throwUnsupportedError(rawPayload)
1250
+ }
1251
+
1252
+ /**
1253
+ * 好友申请信息解析
1254
+ * @param rawPayload
1255
+ */
1256
+ override async friendshipRawPayloadParser (rawPayload: PUPPET.payloads.Friendship): Promise<PUPPET.payloads.Friendship> {
1257
+ return rawPayload
1258
+ }
1259
+
1260
+ /**
1261
+ * 获取好友申请信息
1262
+ * @param id
1263
+ */
1264
+ override async friendshipRawPayload (id: string): Promise<PUPPET.payloads.Friendship> {
1265
+ const ret = await this._cacheMgr!.getFriendshipRawPayload(id)
1266
+
1267
+ if (!ret) {
1268
+ throw new Error(`Can not find friendship for id: ${id}`)
1269
+ }
1270
+
1271
+ return ret
1272
+ }
1273
+
1274
+ /****************************************************************************
1275
+ * moment section
1276
+ ***************************************************************************/
1277
+ /**
1278
+ * 发布朋友圈
1279
+ * @param payload
1280
+ */
1281
+ override async postPublish (payload: PUPPET.payloads.Post): Promise<void | string> {
1282
+ log.verbose(PRE, 'postPublish(%s)', payload)
1283
+ if (!PUPPET.payloads.isPostClient(payload)) {
1284
+ throw new Error('can only publish client post now')
1285
+ }
1286
+ const momentInfo:any = {
1287
+ content: '',
1288
+ mentionIdList: [],
1289
+ visibledList: [],
1290
+ imageUrls: [],
1291
+ videoUrl: '',
1292
+ urlLink: null,
1293
+ channel: null,
1294
+ miniInfo: null,
1295
+ location: null,
1296
+ rootId: '',
1297
+ parentId: '',
1298
+ }
1299
+ for (const item of payload.sayableList) {
1300
+ switch (item.type) {
1301
+ case PUPPET.types.Sayable.Text:
1302
+ momentInfo.content = `${momentInfo.content ? momentInfo.content + '\n' : ''}${item.payload.text}`
1303
+ momentInfo.mentionIdList = item.payload.mentions
1304
+ break
1305
+ case PUPPET.types.Sayable.Attachment: {
1306
+ const fileBox = item.payload.filebox as FileBoxInterface
1307
+ if (typeof item.payload.filebox !== 'string' && (fileBox as FileBoxInterface).type === FileBoxType.Url) {
1308
+ const fileType = fileBox.mediaType && fileBox.mediaType !== 'application/octet-stream' ? fileBox.mediaType : path.extname(fileBox.name)
1309
+ // @ts-ignore
1310
+ const fileUrl = fileBox.remoteUrl || ''
1311
+ if (fileBox.mediaType.startsWith('image/')) {
1312
+ momentInfo.imageUrls.push(fileUrl)
1313
+ } else if (fileType.includes('video/mp4') || fileType.includes('.mp4')) {
1314
+ momentInfo.videoUrl = fileUrl
1315
+ }
1316
+ }
1317
+ break
1318
+ }
1319
+ case PUPPET.types.Sayable.Url: {
1320
+ momentInfo.urlLink = item.payload
1321
+ break
1322
+ }
1323
+ case PUPPET.types.Sayable.Channel: {
1324
+ momentInfo.channel = item.payload
1325
+ break
1326
+ }
1327
+ case PUPPET.types.Sayable.MiniProgram: {
1328
+ momentInfo.miniInfo = item.payload
1329
+ break
1330
+ }
1331
+ default:
1332
+ throw new Error(`postPublish unsupported type ${item.type}`)
1333
+ }
1334
+ }
1335
+ if (payload.rootId) momentInfo.rootId = payload.rootId
1336
+ if (payload.parentId) momentInfo.parentId = payload.parentId
1337
+ if (payload.location) momentInfo.location = payload.location
1338
+
1339
+ momentInfo.visibleList = payload.visibleList
1340
+
1341
+ const res = await this._client?.sendSnsMoment(this._self?.wxid || '', momentInfo)
1342
+
1343
+ return res
1344
+ }
1345
+
1346
+ override async postUnpublish (id: string): Promise<void> {
1347
+ log.verbose(PRE, 'postUnpublish(%s)', id)
1348
+
1349
+ await this._client?.unSendMoment(id)
1350
+ }
1351
+
1352
+ override async postRawPayload (id: string): Promise<PUPPET.payloads.Post | string> {
1353
+ log.verbose(PRE, 'postRawPayload(%s)', id)
1354
+ return id
1355
+ }
1356
+
1357
+ override async postPayloadSayable (postId: string, sayableId: string): Promise<PUPPET.payloads.Sayable> {
1358
+ log.verbose(PRE, 'postPayloadSayable(%s, %s)', postId, sayableId)
1359
+ return postId as any
1360
+ }
1361
+
1362
+ override async postRawPayloadParser (payload: PUPPET.payloads.Post): Promise<PUPPET.payloads.Post> {
1363
+ // log.silly('PuppetService', 'postRawPayloadParser({id:%s})', payload.id)
1364
+ // passthrough
1365
+ return payload
1366
+ }
1367
+
1368
+ override async tap (postId: string, type: PUPPET.types.Tap, tap = true): Promise<boolean | void> {
1369
+ log.verbose(PRE, 'tap(%s, %s, %s)', postId, type, tap)
1370
+
1371
+ const res = await this._client?.sendMomentLike(postId, type)
1372
+
1373
+ return !!res
1374
+ }
1375
+
1376
+ /****************************************************************************
1377
+ * extra methods section
1378
+ ***************************************************************************/
1379
+
1380
+ async syncRoom () {
1381
+ if (this.state.active() !== true) {
1382
+ throw new Error('Can not sync contact before login')
1383
+ }
1384
+ await this._client?.getGroupList(1, 100)
1385
+ }
1386
+ /****************************************************************************
1387
+ * private section
1388
+ ***************************************************************************/
1389
+
1390
+ // 获取群成员列表
1391
+ private async _getRoomMemberList (roomId: string, force?: boolean): Promise<RoomMemberMap> {
1392
+ // FIX: https://github.com/wechaty/puppet-padlocal/issues/115
1393
+ if (!this._cacheMgr) {
1394
+ return {}
1395
+ }
1396
+
1397
+ let ret = await this._cacheMgr!.getRoomMember(roomId)
1398
+
1399
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
1400
+ if (!ret || (ret && !Object.keys(ret).length) || force) {
1401
+ const resMembers = await this._client?.getGroupMembers(roomId) || []
1402
+
1403
+ const roomMemberMap: RoomMemberMap = {}
1404
+ for (const roomMember of resMembers) {
1405
+ const hasContact = await this._cacheMgr!.hasContact(roomMember.wxid)
1406
+ let MemberInfo: ContactPayload
1407
+ // save chat room member as contact, to forbid massive this._client.api.getContact(id) requests while room.ready()
1408
+ if (!hasContact) {
1409
+ MemberInfo = chatRoomMemberToContact(roomMember)
1410
+ await this._cacheMgr!.setContact(MemberInfo.wxid, MemberInfo)
1411
+ roomMemberMap[roomMember.wxid] = MemberInfo
1412
+ } else {
1413
+ MemberInfo = await this._cacheMgr!.getContact(roomMember.wxid) as ContactPayload
1414
+ roomMemberMap[roomMember.wxid] = MemberInfo
1415
+ }
1416
+ }
1417
+ ret = roomMemberMap
1418
+ await this._updateRoomMember(roomId, roomMemberMap)
1419
+ }
1420
+
1421
+ return ret
1422
+ }
1423
+
1424
+ // 更新联系人缓存
1425
+ private async _updateContactCache (contact: ContactPayload): Promise<void> {
1426
+ if (!contact.wxid) {
1427
+ log.warn(PRE, `wxid is required for contact: ${JSON.stringify(contact)}`)
1428
+ return
1429
+ }
1430
+
1431
+ if (isRoomId(contact.wxid)) {
1432
+ const oldRoomPayload = await this._cacheMgr!.getRoom(contact.wxid)
1433
+
1434
+ const roomId = contact.wxid
1435
+ let finalPayload: ContactPayload = contact
1436
+ if (oldRoomPayload) {
1437
+ finalPayload = Object.assign(oldRoomPayload, contact)
1438
+ }
1439
+ await this._cacheMgr!.setRoom(roomId, finalPayload)
1440
+
1441
+ if (!finalPayload.chatroommemberList || finalPayload.chatroommemberList.length === 0) {
1442
+ const map = await this._getRoomMemberList(roomId)
1443
+ await this._cacheMgr!.setRoom(roomId, Object.assign(finalPayload, { chatroommemberList: Object.keys(map) }))
1444
+ }
1445
+
1446
+ } else {
1447
+ const oldContactPaylod = await this._cacheMgr!.getContact(contact.wxid)
1448
+ if (oldContactPaylod) {
1449
+ await this._cacheMgr!.setContact(contact.wxid, Object.assign(oldContactPaylod, contact))
1450
+ } else {
1451
+ await this._cacheMgr!.setContact(contact.wxid, contact)
1452
+ }
1453
+ }
1454
+ }
1455
+
1456
+ // 更新群成员
1457
+ private async _updateRoomMember (roomId: string, roomMemberMap?: RoomMemberMap) {
1458
+ if (roomMemberMap) {
1459
+ await this._cacheMgr!.setRoomMember(roomId, roomMemberMap)
1460
+ } else {
1461
+ await this._cacheMgr!.deleteRoomMember(roomId)
1462
+ }
1463
+ }
1464
+
1465
+ /** this.emit('login', { contactId: userId })
1466
+ * 更新群成员信息
1467
+ * @param roomId
1468
+ */
1469
+ public async _updateRoom (roomId:string) {
1470
+ if (!roomId) {
1471
+ log.warn(PRE, 'roomid is required for updateRoom')
1472
+ return
1473
+ }
1474
+ let contact: ContactPayload | undefined = await this._client?.searchContact([ roomId ]) as ContactPayload | undefined
1475
+
1476
+ if (contact) {
1477
+ const memeberMap: RoomMemberMap = await this._getRoomMemberList(contact.wxid, true)
1478
+ const chatroommemberList = Object.values(memeberMap).map(item => item.wxid)
1479
+ contact = { ...contact, memberNum: Object.keys(memeberMap).length, chatroommemberList }
1480
+ await this._onPushContact(contact)
1481
+ }
1482
+ }
1483
+
1484
+ // 添加好友信息到缓存
1485
+ private async _onPushContact (contact: ContactPayload): Promise<void> {
1486
+ log.silly(PRE, `on push contact: ${JSON.stringify(contact)}`)
1487
+
1488
+ await this._updateContactCache(contact)
1489
+
1490
+ if (contact.wxid) {
1491
+ const aliasToSet = await this._cacheMgr!.getContactStrangerAlias(contact.wxid)
1492
+ if (aliasToSet) {
1493
+ await this.contactAlias(contact.wxid, aliasToSet)
1494
+ await this._cacheMgr!.deleteContactStrangerAlias(contact.wxid)
1495
+ }
1496
+ }
1497
+ }
1498
+
1499
+ private async _onPushMessage (message: MessagePayload): Promise<void> {
1500
+ const messageId = message.id
1501
+ log.silly(PRE, `on push original message: ${JSON.stringify(message)}`)
1502
+ if (await this._cacheMgr!.hasMessage(messageId)) {
1503
+ return
1504
+ }
1505
+ await this._cacheMgr!.setMessage(message.id, message)
1506
+ const event = await parseEvent(this, message)
1507
+ log.info('event-------', JSON.stringify(event))
1508
+ switch (event.type) {
1509
+ case EventType.Message:
1510
+ this.emit('message', {
1511
+ messageId,
1512
+ })
1513
+ break
1514
+ case EventType.Friendship: {
1515
+ if (event.payload.type !== PUPPET.types.Friendship.Confirm && event.payload.type !== PUPPET.types.Friendship.Verify) {
1516
+ await this._cacheMgr?.setFriendshipRawPayload(event.payload.contactId, event.payload)
1517
+
1518
+ setTimeout(() => {
1519
+ this.emit('friendship', {
1520
+ friendshipId: event.payload.contactId,
1521
+ type: event.payload.type,
1522
+ })
1523
+ }, 1000)
1524
+
1525
+ } else {
1526
+ this.emit('message', {
1527
+ messageId,
1528
+ })
1529
+ }
1530
+ break
1531
+ }
1532
+ case EventType.RoomInvite: {
1533
+ const roomInvite: PUPPET.payloads.RoomInvitation = event.payload
1534
+ await this._cacheMgr!.setRoomInvitation(messageId, roomInvite)
1535
+
1536
+ this.emit('room-invite', {
1537
+ roomInvitationId: messageId,
1538
+ })
1539
+ break
1540
+ }
1541
+ case EventType.RoomJoin: {
1542
+ // const roomJoin: PUPPET.payloads.EventRoomJoin = event.payload
1543
+ // await this._updateRoomMember(roomJoin.roomId)
1544
+ // this.emit('room-join', roomJoin)
1545
+ break
1546
+ }
1547
+ case EventType.RoomLeave: {
1548
+ // const roomLeave: PUPPET.payloads.EventRoomLeave = event.payload
1549
+ // this.emit('room-leave', roomLeave)
1550
+
1551
+ // await this._updateRoomMember(roomLeave.roomId)
1552
+ break
1553
+ }
1554
+ case EventType.RoomTopic: {
1555
+ // const roomTopic: PUPPET.payloads.EventRoomTopic = event.payload
1556
+ // this.emit('room-topic', roomTopic)
1557
+ break
1558
+ }
1559
+ }
1560
+ }
1561
+
1562
+ // 刷新用户信息
1563
+ private async _refreshContact (wxid: string, isFriend?: number): Promise<ContactPayload | undefined> {
1564
+ const contact = await this._client?.searchContactDetail([ wxid ]) as ContactPayload | undefined
1565
+ // may return contact with empty payload, empty username, nickname, etc.
1566
+ if (contact && !contact.wxid) {
1567
+ contact.wxid = wxid
1568
+ await this._updateContactCache({ ...contact, isFriend })
1569
+ return contact
1570
+ } else if (contact) {
1571
+ return contact
1572
+ }
1573
+ return undefined
1574
+ }
1575
+
1576
+ // 开始监听心跳
1577
+ private async _startPuppetHeart (firstTime: boolean = true) {
1578
+ if (firstTime && this._heartBeatTimer) {
1579
+ return
1580
+ }
1581
+ this.emit('heartbeat', { data: 'heartbeat@matrix' })
1582
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
1583
+ this._heartBeatTimer = setTimeout(async (): Promise<void> => {
1584
+ await this._startPuppetHeart(false)
1585
+ return undefined
1586
+ }, 15 * 1000) // 15s
1587
+ }
1588
+
1589
+ // 停止监听心跳
1590
+ private _stopPuppetHeart () {
1591
+ if (!this._heartBeatTimer) {
1592
+ return
1593
+ }
1594
+
1595
+ clearTimeout(this._heartBeatTimer)
1596
+ this._heartBeatTimer = undefined
1597
+ }
1598
+
1599
+ /**
1600
+ * 解析图片
1601
+ * @param messageId
1602
+ * @param messagePayload
1603
+ * @private
1604
+ */
1605
+ private async _getMessageImageFileBox (messageId: string, messagePayload: MessagePayload):Promise<FileBoxInterface> {
1606
+ const imageInfo: ImageMessagePayload = await parseImageMessagePayload(messagePayload)
1607
+ if (!imageInfo.file_id) {
1608
+ log.error(`Can not get file for message: ${messageId}`)
1609
+ }
1610
+ const url = await this._client?.downloadImage(imageInfo)
1611
+ return FileBox.fromUrl(url)
1612
+ }
1613
+
1614
+ private async _getMessageAudioFileBox (messageId: string, messagePayload: MessagePayload):Promise<FileBoxInterface> {
1615
+ const audioInfo: AudioMessagePayload = await parseAudioMessagePayload(messagePayload)
1616
+ if (!audioInfo.length) {
1617
+ log.error(`Can not get file for message: ${messageId}`)
1618
+ }
1619
+ const url = await this._client?.downloadAudio(messageId, audioInfo)
1620
+ return FileBox.fromUrl(url)
1621
+ }
1622
+
1623
+ private async _getMessageVideoFileBox (messageId: string, messagePayload: MessagePayload):Promise<FileBoxInterface> {
1624
+ const videoInfo: VideoMessagePayload = await parseVideoMessagePayload(messagePayload)
1625
+ if (!videoInfo.file_id) {
1626
+ log.error(`Can not get file for message: ${messageId}`)
1627
+ }
1628
+ const url = await this._client?.downloadVideo(videoInfo)
1629
+ log.info(PRE, '_getMessageVideoFileBox(): %s', url)
1630
+ return FileBox.fromUrl(url)
1631
+ }
1632
+
1633
+ /**
1634
+ * 解析文件
1635
+ * @param messageId
1636
+ * @param messagePayload
1637
+ * @private
1638
+ */
1639
+ private async _getMessageFileFileBox (messageId: string, messagePayload: MessagePayload):Promise<FileBoxInterface> {
1640
+ const appPayload: AppMessagePayload = await parseAppmsgMessagePayload(messagePayload.msg)
1641
+ if (!appPayload.appattach?.attachid) {
1642
+ log.error(`Can not get file for message: ${messageId}`)
1643
+ }
1644
+ const appattach = appPayload.appattach
1645
+ const params = {
1646
+ aes_key: appattach?.aeskey || '',
1647
+ file_id: appattach?.attachid || '',
1648
+ file_name: appPayload.title,
1649
+ file_size: appattach?.totallen || '',
1650
+ }
1651
+
1652
+ const url = await this._client?.downloadFile(params)
1653
+
1654
+ if (!url) {
1655
+ log.error(`Can not get file url: ${JSON.stringify(params)}`)
1656
+ }
1657
+
1658
+ return FileBox.fromUrl(url, { name: appPayload.title })
1659
+ }
1660
+
1661
+ // @ts-ignore
1662
+ public async syncContact () {
1663
+ if (!this.currentUserId) {
1664
+ throw new Error('Can not sync contact before login')
1665
+ }
1666
+ log.info('syncContact')
1667
+ await this._client!.syncContact()
1668
+ }
1669
+
1670
+ }
1671
+
1672
+ export { PuppetMatrix, VERSION }
1673
+
1674
+ export default PuppetMatrix