wechaty-puppet-matrix 0.0.54 → 0.0.56

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.
Files changed (76) hide show
  1. package/dist/cjs/src/engine-schema.d.ts +1 -0
  2. package/dist/cjs/src/engine-schema.d.ts.map +1 -1
  3. package/dist/cjs/src/matrix/messages/message-contact-card.d.ts +16 -0
  4. package/dist/cjs/src/matrix/messages/message-contact-card.d.ts.map +1 -0
  5. package/dist/cjs/src/matrix/messages/message-contact-card.js +25 -0
  6. package/dist/cjs/src/matrix/messages/message-emotion.d.ts +7 -0
  7. package/dist/cjs/src/matrix/messages/message-emotion.d.ts.map +1 -1
  8. package/dist/cjs/src/matrix/messages/message-emotion.js +20 -0
  9. package/dist/cjs/src/matrix/messages/message-image.d.ts +8 -0
  10. package/dist/cjs/src/matrix/messages/message-image.d.ts.map +1 -1
  11. package/dist/cjs/src/matrix/messages/message-image.js +38 -2
  12. package/dist/cjs/src/matrix/messages/message-image.spec.d.ts +3 -0
  13. package/dist/cjs/src/matrix/messages/message-image.spec.d.ts.map +1 -0
  14. package/dist/cjs/src/matrix/messages/message-image.spec.js +66 -0
  15. package/dist/cjs/src/matrix/messages/message-video.d.ts +6 -0
  16. package/dist/cjs/src/matrix/messages/message-video.d.ts.map +1 -1
  17. package/dist/cjs/src/matrix/messages/message-video.js +20 -0
  18. package/dist/cjs/src/matrix/messages/message-video.spec.d.ts +3 -0
  19. package/dist/cjs/src/matrix/messages/message-video.spec.d.ts.map +1 -0
  20. package/dist/cjs/src/matrix/messages/message-video.spec.js +27 -0
  21. package/dist/cjs/src/matrix/service/request.d.ts +21 -0
  22. package/dist/cjs/src/matrix/service/request.d.ts.map +1 -1
  23. package/dist/cjs/src/matrix/service/request.js +194 -32
  24. package/dist/cjs/src/matrix/utils/file-type.d.ts +4 -0
  25. package/dist/cjs/src/matrix/utils/file-type.d.ts.map +1 -0
  26. package/dist/cjs/src/matrix/utils/file-type.js +55 -0
  27. package/dist/cjs/src/matrix/utils/file-type.spec.d.ts +3 -0
  28. package/dist/cjs/src/matrix/utils/file-type.spec.d.ts.map +1 -0
  29. package/dist/cjs/src/matrix/utils/file-type.spec.js +25 -0
  30. package/dist/cjs/src/puppet-matrix.d.ts +3 -0
  31. package/dist/cjs/src/puppet-matrix.d.ts.map +1 -1
  32. package/dist/cjs/src/puppet-matrix.js +121 -33
  33. package/dist/esm/src/engine-schema.d.ts +1 -0
  34. package/dist/esm/src/engine-schema.d.ts.map +1 -1
  35. package/dist/esm/src/matrix/messages/message-contact-card.d.ts +16 -0
  36. package/dist/esm/src/matrix/messages/message-contact-card.d.ts.map +1 -0
  37. package/dist/esm/src/matrix/messages/message-contact-card.js +22 -0
  38. package/dist/esm/src/matrix/messages/message-emotion.d.ts +7 -0
  39. package/dist/esm/src/matrix/messages/message-emotion.d.ts.map +1 -1
  40. package/dist/esm/src/matrix/messages/message-emotion.js +19 -0
  41. package/dist/esm/src/matrix/messages/message-image.d.ts +8 -0
  42. package/dist/esm/src/matrix/messages/message-image.d.ts.map +1 -1
  43. package/dist/esm/src/matrix/messages/message-image.js +37 -2
  44. package/dist/esm/src/matrix/messages/message-image.spec.d.ts +3 -0
  45. package/dist/esm/src/matrix/messages/message-image.spec.d.ts.map +1 -0
  46. package/dist/esm/src/matrix/messages/message-image.spec.js +64 -0
  47. package/dist/esm/src/matrix/messages/message-video.d.ts +6 -0
  48. package/dist/esm/src/matrix/messages/message-video.d.ts.map +1 -1
  49. package/dist/esm/src/matrix/messages/message-video.js +19 -0
  50. package/dist/esm/src/matrix/messages/message-video.spec.d.ts +3 -0
  51. package/dist/esm/src/matrix/messages/message-video.spec.d.ts.map +1 -0
  52. package/dist/esm/src/matrix/messages/message-video.spec.js +25 -0
  53. package/dist/esm/src/matrix/service/request.d.ts +21 -0
  54. package/dist/esm/src/matrix/service/request.d.ts.map +1 -1
  55. package/dist/esm/src/matrix/service/request.js +193 -32
  56. package/dist/esm/src/matrix/utils/file-type.d.ts +4 -0
  57. package/dist/esm/src/matrix/utils/file-type.d.ts.map +1 -0
  58. package/dist/esm/src/matrix/utils/file-type.js +47 -0
  59. package/dist/esm/src/matrix/utils/file-type.spec.d.ts +3 -0
  60. package/dist/esm/src/matrix/utils/file-type.spec.d.ts.map +1 -0
  61. package/dist/esm/src/matrix/utils/file-type.spec.js +23 -0
  62. package/dist/esm/src/puppet-matrix.d.ts +3 -0
  63. package/dist/esm/src/puppet-matrix.d.ts.map +1 -1
  64. package/dist/esm/src/puppet-matrix.js +121 -33
  65. package/package.json +1 -1
  66. package/src/engine-schema.ts +1 -0
  67. package/src/matrix/messages/message-contact-card.ts +48 -0
  68. package/src/matrix/messages/message-emotion.ts +36 -0
  69. package/src/matrix/messages/message-image.spec.ts +107 -0
  70. package/src/matrix/messages/message-image.ts +63 -2
  71. package/src/matrix/messages/message-video.spec.ts +37 -0
  72. package/src/matrix/messages/message-video.ts +28 -1
  73. package/src/matrix/service/request.ts +274 -32
  74. package/src/matrix/utils/file-type.spec.ts +29 -0
  75. package/src/matrix/utils/file-type.ts +59 -0
  76. package/src/puppet-matrix.ts +150 -35
@@ -12,6 +12,7 @@ import Client from './matrix/service/request.js'
12
12
  import { getUnixTime } from 'date-fns'
13
13
 
14
14
  import { mergeWithFallback } from './matrix/utils/index.js'
15
+ import { detectImageExt, fetchRemoteHead, replaceExt } from './matrix/utils/file-type.js'
15
16
  import path from 'path'
16
17
  // 参考
17
18
  import { CacheManager, RoomMemberMap } from './matrix/cache-manager.js'
@@ -19,11 +20,12 @@ import { isIMContactId, isRoomId } from './matrix/utils/is-type.js'
19
20
  import { AppMessagePayload, AppMessageType, parseAppmsgMessagePayload } from './matrix/messages/message-appmsg.js'
20
21
  import { parseRecordItem, RecordItem, RecordItemType } from './matrix/messages/message-chat-history.js'
21
22
  import { parseMiniProgramMessagePayload } from './matrix/messages/message-miniprogram.js'
22
- import { parseEmotionMessagePayload } from './matrix/messages/message-emotion.js'
23
+ import { EmojiMessagePayload, parseEmotionMessagePayload } from './matrix/messages/message-emotion.js'
23
24
  import { ImageMessagePayload, parseImageMessagePayload } from './matrix/messages/message-image.js'
24
25
  import { parseAudioMessagePayload, AudioMessagePayload } from './matrix/messages/message-audio.js'
25
26
  import { parseVideoMessagePayload, VideoMessagePayload } from './matrix/messages/message-video.js'
26
27
  import { parseLocationMessagePayload } from './matrix/messages/message-location.js'
28
+ import { ContactCardPayload, parseContactCardMessagePayload } from './matrix/messages/message-contact-card.js'
27
29
  import { CachedPromiseFunc } from './matrix/utils/cached-promise.js'
28
30
  import { engineMessageToWechaty } from './matrix/schema-mapper/message.js'
29
31
  import { engineContactToWechaty } from './matrix/schema-mapper/contact.js'
@@ -35,7 +37,6 @@ import {
35
37
  import { parseEvent, EventType } from './matrix/events/mod.js'
36
38
  import { addRunningPuppet, removeRunningPuppet } from './cleanup.js'
37
39
  import { packageJson } from './package-json.js'
38
- import { JsonToXml } from './matrix/utils/xml-to-json.js'
39
40
 
40
41
  const VERSION = packageJson.version || '0.0.0'
41
42
  const PRE = '[PuppetMatrix]'
@@ -655,7 +656,8 @@ class PuppetMatrix extends PUPPET.Puppet {
655
656
  hello = (option as any).hello
656
657
  }
657
658
  }
658
- await this._client?.addFriendByWxid({ content: hello, ticket, scene: addContactScene, wxid: contactId })
659
+ // 搜索联系人的 id 带有 SEARCH_CONTACT_PREFIX, 真实用户名在缓存的 wxid 里
660
+ await this._client?.addFriendByWxid({ content: hello, ticket, scene: addContactScene, wxid: cachedContactSearch?.wxid || contactId })
659
661
  }
660
662
 
661
663
  /**
@@ -724,7 +726,70 @@ class PuppetMatrix extends PUPPET.Puppet {
724
726
  // 名片
725
727
  override async messageContact (messageId: string): Promise<string> {
726
728
  log.verbose('PuppetWeChat', 'messageContact(%s)', messageId)
727
- return PUPPET.throwUnsupportedError(messageId)
729
+ const rawPayload = await this.messageRawPayload(messageId)
730
+ if (rawPayload.msgType !== WechatMessageType.ShareCard) {
731
+ throw new Error(`message ${messageId} is not a contact card`)
732
+ }
733
+ const card = await parseContactCardMessagePayload(rawPayload.msg)
734
+ if (!card) {
735
+ throw new Error(`can not parse contact card from message ${messageId}`)
736
+ }
737
+
738
+ // 好友名片: username 就是 wxid
739
+ if (!card.username.includes(STRANGER_SUFFIX)) {
740
+ return card.username
741
+ }
742
+
743
+ // 微信发出的名片即便是好友也只带 v3 加密名, 尝试在本地联系人中匹配出 wxid
744
+ const friendId = await this._findFriendByCard(card)
745
+ if (friendId) {
746
+ log.verbose(PRE, 'messageContact(%s) matched friend %s', messageId, friendId)
747
+ return friendId
748
+ }
749
+
750
+ // 非好友名片: 缓存为搜索联系人, friendshipAdd 可用 scene 17 + v4 票据添加
751
+ const searchId = `${SEARCH_CONTACT_PREFIX}${card.username}`
752
+ await this._cacheMgr!.setContactSearch(searchId, {
753
+ alias: '',
754
+ avatar: card.avatar,
755
+ city: card.city,
756
+ country: '',
757
+ isFriend: 0,
758
+ name: card.nickname,
759
+ province: card.province,
760
+ scene: card.scene,
761
+ sex: card.sex,
762
+ sign: card.sign,
763
+ ticket: card.antispamticket,
764
+ wxNum: card.alias,
765
+ wxid: card.username,
766
+ })
767
+ return searchId
768
+ }
769
+
770
+ /**
771
+ * 名片(v3 加密名) -> 本地好友 wxid
772
+ * 先按头像路径匹配(忽略 /0 /132 尺寸后缀), 再按唯一昵称匹配
773
+ */
774
+ private async _findFriendByCard (card: ContactCardPayload): Promise<string | undefined> {
775
+ const avatarKey = (url?: string) => (url || '').replace(/^https?:\/\//, '').replace(/\/\d+$/, '')
776
+ const cardAvatar = avatarKey(card.avatar)
777
+ const contacts = (await this._cacheMgr!.getAllContacts())
778
+ .filter(c => c.wxid && !isRoomId(c.wxid) && !c.wxid.includes(STRANGER_SUFFIX) && c.isFriend !== 0)
779
+
780
+ if (cardAvatar) {
781
+ const byAvatar = contacts.find(c => avatarKey(c.avatar) === cardAvatar)
782
+ if (byAvatar) {
783
+ return byAvatar.wxid
784
+ }
785
+ }
786
+ if (card.nickname) {
787
+ const byName = contacts.filter(c => c.name === card.nickname)
788
+ if (byName.length === 1) {
789
+ return byName[0]!.wxid
790
+ }
791
+ }
792
+ return undefined
728
793
  }
729
794
 
730
795
  override async messageChannel (
@@ -774,12 +839,22 @@ class PuppetMatrix extends PUPPET.Puppet {
774
839
  return this._getMessageFileFileBox(messageId, messagePayload)
775
840
  case PUPPET.types.Message.Emoticon: {
776
841
  const emotionPayload = await parseEmotionMessagePayload(messagePayload)
777
- const emoticonBox = emotionPayload.is_ww ? await this._client?.downloadWWMedia({
778
- aes_key: emotionPayload.aeskey || '',
779
- auth_key: emotionPayload.authkey || '',
780
- file_id: emotionPayload.cdnurl || '',
781
- file_name: `message-${messageId}-emoticon.jpg`,
782
- }, 3) : FileBox.fromUrl(emotionPayload.cdnurl, { name: `message-${messageId}-emoticon.jpg` })
842
+ // downloadWWMedia 返回的是下载地址字符串
843
+ const url: string = emotionPayload.is_ww
844
+ ? (await this._client?.downloadWWMedia({
845
+ aes_key: emotionPayload.aeskey || '',
846
+ auth_key: emotionPayload.authkey || '',
847
+ file_id: emotionPayload.cdnurl || '',
848
+ file_name: `message-${messageId}-emoticon.gif`,
849
+ }, 3)) || ''
850
+ : emotionPayload.cdnurl
851
+ if (!url) {
852
+ throw new Error(`Can not get emoticon url for message: ${messageId}`)
853
+ }
854
+ // 按文件头确定扩展名(静态多为 png,动图为 gif),识别失败时按表情类型猜测
855
+ const head = await fetchRemoteHead(url)
856
+ const ext = (head && detectImageExt(head)) || (emotionPayload.type === 2 && !emotionPayload.game ? 'gif' : 'png')
857
+ const emoticonBox = FileBox.fromUrl(url, { name: `message-${messageId}-emoticon.${ext}` })
783
858
 
784
859
  emoticonBox.metadata = {
785
860
  payload: emotionPayload,
@@ -953,28 +1028,14 @@ class PuppetMatrix extends PUPPET.Puppet {
953
1028
  ***************************************************************************/
954
1029
  // 发送名片
955
1030
  override async messageSendContact (toUserId: string, contactId: string): Promise<void | string> {
956
- log.verbose('PuppetWeChat', 'messageSend("%s", %s)', toUserId, contactId)
1031
+ log.verbose('PuppetWeChat', 'messageSendContact("%s", %s)', toUserId, contactId)
957
1032
  const contactPayload = await this.contactRawPayload(contactId)
958
- const xmlObj = {
959
- msg: {
960
- antispamticket: '', // TODO 需要考虑怎么设置
961
- bigheadimgurl: contactPayload?.avatar,
962
- brandFlags: '0',
963
- certflag: '0',
964
- city: contactPayload?.city,
965
- fullpy: contactPayload?.name,
966
- imagestatus: '3',
967
- nickname: contactPayload?.name,
968
- province: contactPayload?.province,
969
- regionCode: 'CN_Shanghai',
970
- scene: '17',
971
- sex: contactPayload?.sex,
972
- smallheadimgurl: contactPayload?.avatar,
973
- username: contactPayload?.ticket,
974
- },
975
- }
976
- const xml = JsonToXml(xmlObj)
977
- const res = await this._client?.sendContactCard(toUserId, xml, this._self?.wxid)
1033
+ const res = await this._client?.sendContact({
1034
+ shareHeadImg: contactPayload?.avatar,
1035
+ shareId: contactPayload?.wxid || contactId,
1036
+ shareNickname: contactPayload?.name,
1037
+ toId: toUserId,
1038
+ }, this._self?.wxid)
978
1039
  if (res) {
979
1040
  await this._cacheMgr?.setMessage(res.id, res)
980
1041
  return res.id
@@ -993,7 +1054,17 @@ class PuppetMatrix extends PUPPET.Puppet {
993
1054
  // @ts-ignore
994
1055
  const fileUrl = fileBox.remoteUrl || ''
995
1056
  if (metadata.type === 'emoticon') {
996
- PUPPET.throwUnsupportedError(conversationId, fileBox)
1057
+ // 优先 md5 发送(收到的表情原样转发、骰子/猜拳均可),失败再按 url 发送
1058
+ const emoji = metadata.payload as EmojiMessagePayload | undefined
1059
+ const byMd5 = emoji?.md5 && emoji.len
1060
+ ? await this._client?.sendEmojiMd5(conversationId, emoji.md5, emoji.len, this._self?.wxid)
1061
+ : undefined
1062
+ const msgInfo = byMd5 || (fileUrl ? await this._client?.sendEmojiUrl(conversationId, fileUrl, this._self?.wxid) : undefined)
1063
+ if (!msgInfo) {
1064
+ throw new Error(`send emoticon failed: ${fileBox.name}`)
1065
+ }
1066
+ await this._cacheMgr?.setMessage(msgInfo.id, msgInfo)
1067
+ return msgInfo.id
997
1068
  } else if (fileBox.mediaType.startsWith('image/')) {
998
1069
  if (fileUrl) {
999
1070
  const msgInfo = await this._client?.sendUrlImg(conversationId, fileUrl, this._self?.wxid)
@@ -1114,11 +1185,37 @@ class PuppetMatrix extends PUPPET.Puppet {
1114
1185
  }
1115
1186
 
1116
1187
  /**
1117
- * 消息撤回 暂不支持
1188
+ * 拍一拍(Wechaty 无标准接口,通过 bot.puppet 调用)
1189
+ * @param conversationId 会话 id:私聊为好友 wxid,群聊为群 id
1190
+ * @param contactId 被拍的人 wxid,私聊时默认拍对方
1191
+ */
1192
+ public async sendPat (conversationId: string, contactId?: string): Promise<boolean> {
1193
+ log.verbose(PRE, 'sendPat(%s, %s)', conversationId, contactId)
1194
+ const patted = contactId || conversationId
1195
+ if (isRoomId(patted)) {
1196
+ throw new Error('sendPat: contactId is required in room')
1197
+ }
1198
+ return (await this._client?.sendPat(conversationId, patted)) || false
1199
+ }
1200
+
1201
+ /**
1202
+ * 消息撤回(只能撤回自己发送的消息)
1118
1203
  * @param messageId
1119
1204
  */
1120
1205
  override async messageRecall (messageId: string): Promise<boolean> {
1121
- return PUPPET.throwUnsupportedError(messageId)
1206
+ log.verbose(PRE, 'messageRecall(%s)', messageId)
1207
+ const payload = await this._cacheMgr?.getMessage(messageId)
1208
+ if (!payload) {
1209
+ log.warn(PRE, 'messageRecall(%s): message not found in cache', messageId)
1210
+ return false
1211
+ }
1212
+ // 自己发送的消息 listenerId 为会话 id(好友 wxid 或群 id)
1213
+ const toId = payload.listenerId || payload.fromWxid || ''
1214
+ if (!toId) {
1215
+ log.warn(PRE, 'messageRecall(%s): can not find conversation id', messageId)
1216
+ return false
1217
+ }
1218
+ return (await this._client?.revokeMsg(toId, messageId, payload.clientMsgId)) || false
1122
1219
  }
1123
1220
 
1124
1221
  /**
@@ -1894,7 +1991,25 @@ class PuppetMatrix extends PUPPET.Puppet {
1894
1991
  log.error(`Can not get file for message: ${messageId}`)
1895
1992
  }
1896
1993
  const url = imageInfo.is_ww ? await this._client?.downloadWWMedia(imageInfo, 1) : await this._client?.downloadImage(imageInfo)
1897
- return this._fileBoxFromUrl(url, messageId)
1994
+ return this._fileBoxFromUrl(url, messageId, url ? await this._imageNameByHead(url) : undefined)
1995
+ }
1996
+
1997
+ /**
1998
+ * 下载接口固定返回 .png 文件名,实际多为 JPEG。读取文件头修正扩展名,识别失败时保持原名
1999
+ */
2000
+ private async _imageNameByHead (url: string): Promise<string | undefined> {
2001
+ const head = await fetchRemoteHead(url)
2002
+ const ext = head && detectImageExt(head)
2003
+ if (!ext) {
2004
+ return undefined
2005
+ }
2006
+ let rawName = 'image'
2007
+ try {
2008
+ rawName = decodeURIComponent(new URL(url).pathname.split('/').pop() || rawName)
2009
+ } catch {
2010
+ // url 解析失败时用默认名
2011
+ }
2012
+ return replaceExt(rawName, ext)
1898
2013
  }
1899
2014
 
1900
2015
  private async _getMessageAudioFileBox (messageId: string, messagePayload: MessagePayload): Promise<FileBoxInterface> {