wechaty-puppet-matrix 0.0.55 → 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 (34) 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/service/request.d.ts +12 -0
  10. package/dist/cjs/src/matrix/service/request.d.ts.map +1 -1
  11. package/dist/cjs/src/matrix/service/request.js +138 -0
  12. package/dist/cjs/src/puppet-matrix.d.ts +2 -0
  13. package/dist/cjs/src/puppet-matrix.d.ts.map +1 -1
  14. package/dist/cjs/src/puppet-matrix.js +105 -32
  15. package/dist/esm/src/engine-schema.d.ts +1 -0
  16. package/dist/esm/src/engine-schema.d.ts.map +1 -1
  17. package/dist/esm/src/matrix/messages/message-contact-card.d.ts +16 -0
  18. package/dist/esm/src/matrix/messages/message-contact-card.d.ts.map +1 -0
  19. package/dist/esm/src/matrix/messages/message-contact-card.js +22 -0
  20. package/dist/esm/src/matrix/messages/message-emotion.d.ts +7 -0
  21. package/dist/esm/src/matrix/messages/message-emotion.d.ts.map +1 -1
  22. package/dist/esm/src/matrix/messages/message-emotion.js +19 -0
  23. package/dist/esm/src/matrix/service/request.d.ts +12 -0
  24. package/dist/esm/src/matrix/service/request.d.ts.map +1 -1
  25. package/dist/esm/src/matrix/service/request.js +138 -0
  26. package/dist/esm/src/puppet-matrix.d.ts +2 -0
  27. package/dist/esm/src/puppet-matrix.d.ts.map +1 -1
  28. package/dist/esm/src/puppet-matrix.js +105 -32
  29. package/package.json +1 -1
  30. package/src/engine-schema.ts +1 -0
  31. package/src/matrix/messages/message-contact-card.ts +48 -0
  32. package/src/matrix/messages/message-emotion.ts +36 -0
  33. package/src/matrix/service/request.ts +189 -0
  34. package/src/puppet-matrix.ts +130 -34
@@ -20,11 +20,12 @@ import { isIMContactId, isRoomId } from './matrix/utils/is-type.js'
20
20
  import { AppMessagePayload, AppMessageType, parseAppmsgMessagePayload } from './matrix/messages/message-appmsg.js'
21
21
  import { parseRecordItem, RecordItem, RecordItemType } from './matrix/messages/message-chat-history.js'
22
22
  import { parseMiniProgramMessagePayload } from './matrix/messages/message-miniprogram.js'
23
- import { parseEmotionMessagePayload } from './matrix/messages/message-emotion.js'
23
+ import { EmojiMessagePayload, parseEmotionMessagePayload } from './matrix/messages/message-emotion.js'
24
24
  import { ImageMessagePayload, parseImageMessagePayload } from './matrix/messages/message-image.js'
25
25
  import { parseAudioMessagePayload, AudioMessagePayload } from './matrix/messages/message-audio.js'
26
26
  import { parseVideoMessagePayload, VideoMessagePayload } from './matrix/messages/message-video.js'
27
27
  import { parseLocationMessagePayload } from './matrix/messages/message-location.js'
28
+ import { ContactCardPayload, parseContactCardMessagePayload } from './matrix/messages/message-contact-card.js'
28
29
  import { CachedPromiseFunc } from './matrix/utils/cached-promise.js'
29
30
  import { engineMessageToWechaty } from './matrix/schema-mapper/message.js'
30
31
  import { engineContactToWechaty } from './matrix/schema-mapper/contact.js'
@@ -36,7 +37,6 @@ import {
36
37
  import { parseEvent, EventType } from './matrix/events/mod.js'
37
38
  import { addRunningPuppet, removeRunningPuppet } from './cleanup.js'
38
39
  import { packageJson } from './package-json.js'
39
- import { JsonToXml } from './matrix/utils/xml-to-json.js'
40
40
 
41
41
  const VERSION = packageJson.version || '0.0.0'
42
42
  const PRE = '[PuppetMatrix]'
@@ -656,7 +656,8 @@ class PuppetMatrix extends PUPPET.Puppet {
656
656
  hello = (option as any).hello
657
657
  }
658
658
  }
659
- 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 })
660
661
  }
661
662
 
662
663
  /**
@@ -725,7 +726,70 @@ class PuppetMatrix extends PUPPET.Puppet {
725
726
  // 名片
726
727
  override async messageContact (messageId: string): Promise<string> {
727
728
  log.verbose('PuppetWeChat', 'messageContact(%s)', messageId)
728
- 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
729
793
  }
730
794
 
731
795
  override async messageChannel (
@@ -775,12 +839,22 @@ class PuppetMatrix extends PUPPET.Puppet {
775
839
  return this._getMessageFileFileBox(messageId, messagePayload)
776
840
  case PUPPET.types.Message.Emoticon: {
777
841
  const emotionPayload = await parseEmotionMessagePayload(messagePayload)
778
- const emoticonBox = emotionPayload.is_ww ? await this._client?.downloadWWMedia({
779
- aes_key: emotionPayload.aeskey || '',
780
- auth_key: emotionPayload.authkey || '',
781
- file_id: emotionPayload.cdnurl || '',
782
- file_name: `message-${messageId}-emoticon.jpg`,
783
- }, 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}` })
784
858
 
785
859
  emoticonBox.metadata = {
786
860
  payload: emotionPayload,
@@ -954,28 +1028,14 @@ class PuppetMatrix extends PUPPET.Puppet {
954
1028
  ***************************************************************************/
955
1029
  // 发送名片
956
1030
  override async messageSendContact (toUserId: string, contactId: string): Promise<void | string> {
957
- log.verbose('PuppetWeChat', 'messageSend("%s", %s)', toUserId, contactId)
1031
+ log.verbose('PuppetWeChat', 'messageSendContact("%s", %s)', toUserId, contactId)
958
1032
  const contactPayload = await this.contactRawPayload(contactId)
959
- const xmlObj = {
960
- msg: {
961
- antispamticket: '', // TODO 需要考虑怎么设置
962
- bigheadimgurl: contactPayload?.avatar,
963
- brandFlags: '0',
964
- certflag: '0',
965
- city: contactPayload?.city,
966
- fullpy: contactPayload?.name,
967
- imagestatus: '3',
968
- nickname: contactPayload?.name,
969
- province: contactPayload?.province,
970
- regionCode: 'CN_Shanghai',
971
- scene: '17',
972
- sex: contactPayload?.sex,
973
- smallheadimgurl: contactPayload?.avatar,
974
- username: contactPayload?.ticket,
975
- },
976
- }
977
- const xml = JsonToXml(xmlObj)
978
- 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)
979
1039
  if (res) {
980
1040
  await this._cacheMgr?.setMessage(res.id, res)
981
1041
  return res.id
@@ -994,7 +1054,17 @@ class PuppetMatrix extends PUPPET.Puppet {
994
1054
  // @ts-ignore
995
1055
  const fileUrl = fileBox.remoteUrl || ''
996
1056
  if (metadata.type === 'emoticon') {
997
- 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
998
1068
  } else if (fileBox.mediaType.startsWith('image/')) {
999
1069
  if (fileUrl) {
1000
1070
  const msgInfo = await this._client?.sendUrlImg(conversationId, fileUrl, this._self?.wxid)
@@ -1115,11 +1185,37 @@ class PuppetMatrix extends PUPPET.Puppet {
1115
1185
  }
1116
1186
 
1117
1187
  /**
1118
- * 消息撤回 暂不支持
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
+ * 消息撤回(只能撤回自己发送的消息)
1119
1203
  * @param messageId
1120
1204
  */
1121
1205
  override async messageRecall (messageId: string): Promise<boolean> {
1122
- 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
1123
1219
  }
1124
1220
 
1125
1221
  /**