python-wechaty
python-wechaty copied to clipboard
有关padlocal协议能实现的和不能实现的实践总结【抛砖引玉】
padlocal协议应该算是社区目前功能比较全的puppet了,这两天因为策划新项目申请了7天试用账号,详细测试了puppet-padlocal与python-wecahty对接目前能够实现的功能和一些还不能实现的,总结下来,以便后面其他兄弟参考。 部分不能实现的,也许也是因为我代码写的不对,希望大家跟帖完善!
环境: python-wechaty: 0.8.35 padlocal- docker img: 0.65 (注意0.78和latest版本是会有问题的,详见 https://github.com/wechaty/wechaty/issues/2349 )
运行后gateway窗口显示:
- wechaty-puppet-padlocal version: ~~0.52.1~~ (现在是0.4.2了)
- padlocal-ts-client version: 0.4.1
测试代码主要参考 https://github.com/wechaty/python-wechaty/tree/master/docs/references (对于直接使用的部分简称“官方代码”)
正文: 1、所有消息类型都能识别了,包括红包转账(当然不能发,因为python-wechaty就不支持发红包。另外群内的转账transfer,是可以解析出信息的,具体方法见跟帖); 1.5 IMAGE、VIDEO、ATTACHMENT以及 AUDIO 四个类型的消息,配合filebox可以存贮在本地,对于前三类甚至可以直接用转存的filebox实现发送(但msg.forward()不行),不过对于Audio不行,需要重新按格式配置filebox才能发送。 以上四类消息保存在本地的示例代码如下:
image_file_box = await msg.to_file_box()
print(f'saving file<{image_file_box.name}>')
path = os.getcwd()+f'\material\{room.room_id}'
if not os.path.exists(path):
os.mkdir(path)
await image_file_box.to_file(path+f'\{image_file_box.name}')
2、可以正确识别自己发出的消息(包括多终端同步消息,也能识别出来是自己发送的了); 3、可以正确识别性别; 4、可以正确识别自己给用户设定的昵称,如果有昵称,在群里@的话就显示的是昵称(但是如果账号没有给用户设置昵称,用户自己在群聊里设置昵称的话,那么还是会@显示账号名); 5、可以识别聊天记录类型消息,但提取不到里面的内容。(对于聊天记录类型消息,目前几乎干不了什么,没法存储、转发。通过msg.text()解析因为内容量太大,不好弄) ~~(实际上除了媒体类型消息外,msg.text()都能提取,但除文本类型外,提取的都是经过转化的xml格式)~~; 6、接受语音、视频邀请程序不会崩溃,会把消息识别为unspecified,无法解析; 7、非微信原生emoj不会被正确识别(被识别为?,表情包被识别为~~xml~~ emojion类型,注意不是img,这是两个type); 8、客户端程序更换重启,不必重启gateway程序; 9、可以更改联系人昵称,但不能删除; 10、貌似puppet每做一个动作都会导致一条is_self的信息,但这个信息会被识别为unspecified,所以可以设置一个if语句,简单的return所有unspecified类型 (通过其他设备主动发送的消息,会同步给puppet,但这个会被识别为正常的type,所以判断is_self()也是有必要的) 11、可以识别撤回消息,但用msg.to_recalled()拿不到撤回消息的文本。可以侦测到这个消息后提取上一条消息部分代替【因为只要它发过,程序都会记录,是否保存要看业务代码怎么写】 (另外用户撤回消息,微信系统发的“xx撤回了一条消息”也会触发一个message事件,get recall message,但这个代码怎么识别貌似没给接口,on_message对应的是receive事件,不是get事件) 12、filebox可以正常发送了,msg.say()也不会报错了【puppet-xp 1.10.20版本是不行的】 13、可以识别引用消息,且能够正常提取text(他会把引用消息也当成text类型),~~但是无法提取引用内容,同时会触发一个对recall类型消息的识别~~ 不会触发错误信息,会把引用内容和回复内容按特定格式组成一个txt类消息,可以用正则方案提取,参考代码如:
import re
if re.match(r"^「.+」\s-+\s.+", text, re.S): #判断是否为引用消息
quote = re.search(r":.+」",text, re.S).group()[1:-1] #引用内容
reply = re.search(r"-\n.+", text, re.S).group()[2:] #回复内容
14、从缓存中提取之前的消息(nb)这个功能没法儿实现,暂不知道是padlocal不支持还是我代码实现不对。 补充:再次试过了,无论是按room_id 还是 contact_id,还是text 都无法找到,不报错,就是找不到,可能跟缓存机制有关 (find和findall都一样,测试代码如下)
msg_list = await msg.find_all(talker_id=talker.contact_id)
if msg_list:
for old_msg in msg_list:
print(old_msg.date(),':',old_msg.talker().name,"say”", old_msg.text(),"“to ",old_msg.to().name)
else:
print('nothing found')
old_msg = await msg.find(talker_id=talker.contact_id)
if old_msg:
print(old_msg.date(),':',old_msg.talker().name,"say”", old_msg.text(),"“to ",old_msg.to().name)
else:
print('nothing found')
15、貌似padlocal协议每次登录都会同步下历史消息,但这个消息会被认为是recall……导致很多不确定问题,虽然会报错,但不影响程序继续 16、【更新】公众号文章就是url类型消息,可以接收识别,~~但解析为xml~~ 配合msg.to_url_link()可以完美转存,转存对象可以完美发送,发送形态为卡片,代码参考如下:
if msg.type() == MessageType.MESSAGE_TYPE_URL:
urlfile = await msg.to_url_link()
await someone.say(urlfile)
17、【更新】可以识别小程序,并且按python-wechaty官方文档方法可以成功存储,但不支持发送(所以转发功能实现不了) 跟@wjcat确认过了,目前向room转发可以,向contact转发不行,是bug,他正在fix。 代码参考:
if msg.type() == MessageType.MESSAGE_TYPE_MINI_PROGRAM:
minipro = await msg.to_mini_program()
await room.say(minipro)
18、【更新】被拉群会自动加入,无需代码实现; 但对于人数过多的群,不会被直接拉进去,所以就还得通过roominvation.accept()实现,
19、可以实现文本消息转发(使用官方代码即可); 20、mention_self()正常了(终于不用正则了),也可以正确提取mention_text了 注: \u0x2005 为不可见字符, 提及(@)的消息的格式一般为 @Gary\u0x2005 (注意mention的消息其msg.text()是会把@昵称一起带着的,包括不可见字符。但mention_text会把这些都去掉,非常方便) 21、撤回消息,即recall 无法实现 (gateweay报错ERR PuppetServiceImpl grpcError() messageRecall() rejection: Cannot read property 'clientmsgid' of undefined) 22、msg.say()无法实现mention 23、所在群被群主解散时会先触发一个收到recall信息的事件,不知道为什么……然后room-leave事件会导致报错: 2022-04-10 12:40:48,973 - Wechaty - ERROR - internal error <WechatyPluginError('the plugin args of room-join is invalid, the source args:<([<wechaty.wechaty.ContactSelf object at 0x000001AFAA750E80>], <wechaty.wechaty.ContactSelf object at 0x000001AFAA750E80>, datetime.datetime(2022, 4, 10, 12, 40, 47))>, but expected args is room, invitees, inviter, date', None, None)> 24、不支持建群操作 (有关群的更多功能有待进一步测试) 25、【补充】对于CONTACT类型消息,目前msg.to_contact()可能存在bug,所以这一类消息,仅能侦测。如果要提取相关信息可以通过msg.text()解析相关字段完成。(https://github.com/wechaty/python-wechaty/issues/319)
测试程序代码(欢迎大家跟帖修正、补充)
import os,time
from wechaty import (
Contact,
Message,
Wechaty,
MessageType,
ScanStatus,
FileBox,
Room,
)
import asyncio
async def on_message(msg: Message):
"""
Message Handler for the Bot
"""
if msg.is_self():
print(msg.to().name)
return
talker=msg.talker()
text=msg.text()
print(text)
if msg.type() == MessageType.MESSAGE_TYPE_RECALLED:
recalled_message = await msg.to_recalled()
print(f"{recalled_message}被撤回")
return
elif msg.type() == MessageType.MESSAGE_TYPE_AUDIO:
audio_message = await msg.to_file_box()
await wukong.say(audio_message)
return
elif msg.type() == MessageType.MESSAGE_TYPE_MINI_PROGRAM:
minipro = await msg.to_mini_program()
await wukong.say(minipro)
return
elif msg.type() == MessageType.MESSAGE_TYPE_CONTACT:
receivecon = await msg.to_contact()
await wukong.say(receivecon)
return
elif msg.type() == MessageType.MESSAGE_TYPE_URL:
file_box1 = FileBox.from_url(await msg.to_url_link(), "forward-url.png")
await wukong.say(file_box1)
return
if msg.room():
contact_mention_list = await msg.mention_list()
print(contact_mention_list)
if await msg.mention_self():
await msg.say("[害羞]",[talker.contact_id])
print(await msg.mention_text())
time.sleep(1)
print(await msg.recall())
return
await msg.forward(wukong)
print(msg.date())
return
if text == 'ding':
await msg.say('dong')
file_box = FileBox.from_url(
'https://pics0.baidu.com/feed/4610b912c8fcc3ce0b95fdc36c79d582d53f20ab.jpeg?token=0534d821f67bcc69a5c3cacbd49ca036',
name='ding-dong.jpg'
)
await msg.say(file_box)
contact_list = [wukong, daxiongdi]
#print('机器人创建所用的联系人列表为: %s', contact_list.join(','))
room = await Room.create(contact_list, 'ding')
print('Bot createDingRoom() new ding room created: %s', room)
await room.topic('ding - created') # 设置群聊名称
await room.say('ding - 创建完成')
if text == 'bell':
msg_list = await msg.find_all([talker.contact_id])
for old_msg in msg_list:
print(old_msg.date(),':',old_msg.talker().name,"say”", old_msg.text(),"“to ",old_msg.to().name)
async def on_scan(
qrcode: str,
status: ScanStatus,
_data,
):
"""
Scan Handler for the Bot
"""
print('Status: ' + str(status))
print('View QR Code Online: https://wechaty.js.org/qrcode/' + quote(qrcode))
async def on_login(user: Contact):
"""
Login Handler for the Bot
"""
global wukong
wukong = await bot.Contact.find('无空')
global daxiongdi
daxiongdi = await bot.Contact.find('大兄弟666')
alias = await wukong.alias()
if alias is None or alias == "":
print('您还没有为联系人设置任何别名' + wukong.name)
await wukong.alias('老赵')
print(f"改变{wukong.name}的备注成功!")
else:
print('您已经为联系人设置了别名 ' + wukong.name + ':' + alias)
await wukong.alias(None)
print(f"成功删除{wukong.name}的备注!")
# TODO: To be written
async def main():
"""
Async Main Entry
"""
#
# Make sure we have set WECHATY_PUPPET_SERVICE_TOKEN in the environment variables.
# Learn more about services (and TOKEN) from https://wechaty.js.org/docs/puppet-services/
#
# It is highly recommanded to use token like [paimon] and [wxwork].
# Those types of puppet_service are supported natively.
# https://wechaty.js.org/docs/puppet-services/paimon
# https://wechaty.js.org/docs/puppet-services/wxwork
#
# Replace your token here and umcommt that line, you can just run this python file successfully!
#os.environ['WECHATY_PUPPET_SERVICE_TOKEN'] = 'puppet_paimon_ac1b15a-a4c9-41f6-ad8a-7ef07bdb2d79'
#os.environ['WECHATY_PUPPET'] = 'wechaty-puppet-service'
#
if 'WECHATY_PUPPET_SERVICE_TOKEN' not in os.environ:
print('''
Error: WECHATY_PUPPET_SERVICE_TOKEN is not found in the environment variables
You need a TOKEN to run the Python Wechaty. Please goto our README for details
https://github.com/wechaty/python-wechaty-getting-started/#wechaty_puppet_service_token
''')
global bot
bot = Wechaty()
bot.on('scan', on_scan)
bot.on('login', on_login)
bot.on('message', on_message)
await bot.start()
print('[Python Wechaty] Ding Dong Bot started.')
asyncio.run(main())
最后再次欢迎大家跟帖完善总结,这将给所有人带来便利!
以上测试日期:2022-4-10
Great shares! This would be great to be published as a blog post for the community!
Thanks for your so detailed testing under the given code. I think your great work will make python-wechaty more robust and less existing bugs. After review the list of testing, there are some possible bug, so I will relist it bellow to follow:
- [ ] 5、可以识别聊天记录类型消息,但提取不到里面的内容 (实际上除了媒体类型消息外,msg.text()都能提取,但除文本类型外,提取的都是经过转化的xml格式)
- [ ] 6、接受语音、视频邀请程序不会崩溃,会把消息识别为unspecified,无法解析;
- [ ] 7、非微信原生emoj不会被正确识别(被识别为?,表情包被识别为xml),另外emoj的消息类型不是img,这是两个type
- [ ] 13、可以识别引用消息,且能够正常提取text(他会把引用消息也当成text类型),但是无法提取引用内容,同时会触发一个对recall类型消息的识别(不知道什么逻辑,但不报错,无关大雅)
- [ ] 14、从缓存中提取之前的消息(nb)这个功能没法儿实现,暂不知道是padlocal不支持还是我代码实现不对。
- [ ] 16、公众号文章就是url类型消息,可以接收识别,但解析为xml (所有解析为xml的消息,虽然wechaty-python提供了转存方法,但目前均未成功实现)
- [ ] 17、可以识别小程序,并且按python-wechaty官方文档方法可以成功存储,但不支持发送(所以转发功能实现不了)
- [ ] 21、撤回消息,即recall 无法实现 (gateweay报错ERR PuppetServiceImpl grpcError() messageRecall() rejection: Cannot read property 'clientmsgid' of undefined)
- [ ] 22、msg.say()无法实现mention
- [ ] 23、所在群被群主解散时会先触发一个收到recall信息的事件,不知道为什么……然后room-leave事件会导致报错: 2022-04-10 12:40:48,973 - Wechaty - ERROR - internal error <WechatyPluginError('the plugin args of room-join is invalid, the source args:<([<wechaty.wechaty.ContactSelf object at 0x000001AFAA750E80>], <wechaty.wechaty.ContactSelf object at 0x000001AFAA750E80>, datetime.datetime(2022, 4, 10, 12, 40, 47))>, but expected args is room, invitees, inviter, date', None, None)>
The above are the possible bugs & needed to be affirmed, so @bigbrother666sh let's pay attentions in next few days to fix these. In all, thanks for your great work.
另外补充一个新发现的,关于 mention_list和msg.mention_text, 对于 @所有人 这样的操作,mention_list 是能够正确识别的,但是mention_text 就无法正常的 把 @所有人 去掉
- [ ] 7、非微信原生emoj不会被正确识别(被识别为?,表情包被识别为xml),另外emoj的消息类型不是img,这是两个type
这一点我再多说明下吧,如果对方是用手机客户端,应该没问题,因为emoj就一页,都是默认的,除非他发自定义表情包,那个就是img格式的消息了。 但如果对方使用的是Windows或者mac微信客户端就不好说了,点表情图标后出来的emoj只有第一页的可以被正确识别,从第二页开始都不会被正确识别
- [ ] 13、可以识别引用消息,且能够正常提取text(他会把引用消息也当成text类型),但是无法提取引用内容,同时会触发一个对recall类型消息的识别(不知道什么逻辑,但不报错,无关大雅)
经过测试,不会触发recall,引用消息连同回复消息会转为特定格式文本,可以通过正则方案提取,所以这一条可以暂忽略, 附引用消息正则提取代码参考:
import re
if re.match(r"^「.+」\s-+\s.+", text, re.S): #判断是否为引用消息
quote = re.search(r":.+」",text, re.S).group()[1:-1] #引用内容
reply = re.search(r"-\n.+", text, re.S).group()[2:] #回复内容
判断规则更新了下,更严格一些,提高鲁棒性, 现在哪怕是用户信息也凑巧是以「 开头的也不会被判断为引用消息。 不过目前这个判断也还是有缺陷,假如用户就是要跟你对着干,故意输入
「.*」
- - - - - - - - - - - - - - - -
这样的信息,则按上面的会被判断为引用消息,但reply拿不到,会报错…… 实在搞不懂了,正则就是时间黑洞啊,等其他兄弟补充完善吧
- [ ] 7、非微信原生emoj不会被正确识别(被识别为?,表情包被识别为xml),另外emoj的消息类型不是img,这是两个type
这一点我再多说明下吧,如果对方是用手机客户端,应该没问题,因为emoj就一页,都是默认的,除非他发自定义表情包,那个就是img格式的消息了。 但如果对方使用的是Windows或者mac微信客户端就不好说了,点表情图标后出来的emoj只有第一页的可以被正确识别,从第二页开始都不会被正确识别
如果不能被正确识别,那会被识别成什么? 可以提供一下原发送emoji 和接受后的错误emoji 吗?
从这个头像开始😃【仔细看了下,手机客户端没有这些,电脑上才有】
2022-04-15 11:45:09,209 - Wechaty - INFO - receive message <Message#message_type_text[🗣 Contact <wx id_tnv0hd5hj3rs11> <无空> [转圈]>
[转圈]
2022-04-15 11:45:15,160 - Wechaty - INFO - receive message <Message#message_type_text[🗣 Contact <wx id_tnv0hd5hj3rs11> <无空> 😃>
😃
我感觉是编码还是被识别了,上面gateway信息我是从cli窗口(一部Windows电脑)拷贝的,但是在cli窗口里面,图标是一个◇里面有个❓ 的样子…… 而上买呢[转圈],是支持的微信默认的emoj
再补充个经验(也许回头可以整理个wiki?) 对于群转账(注意不是群红包),padlocal会传给Python-wechaty一个MessageType.MESSAGE_TYPE_TRANSFER类型的消息,然后我们可以获取到 msg.text(),依靠正则可以完美解析出发款人id、收款人id、金额、转账留言以及转账消息类型(发出、接收、拒绝),代码如下:
text = msg.text()
if msg.type() == MessageType.MESSAGE_TYPE_TRANSFER: #先判断消息类型
from_id = re.search(r"<payer_username><!\[CDATA\[.+?\]", text).group()[25:-1] #取发款人ID
receive_id = re.search(r"<receiver_username><!\[CDATA\[.+?\]", text).group()[28:-1] #收款人 ID
amount = re.search(r"<feedesc><!\[CDATA\[.+?\]", text).group()[18:-1] #金额(从feedesc字段取,包含货币符号,不能计算,仅文本)
pay_memo = re.search(r"<pay_memo><!\[CDATA\[.+?\]", text).group()[19:-1] #转账留言
direction = re.search(r"<paysubtype>\d", text).group()[-1] #转账方向,从paysubtype字段获取,1为发出,2为接收,4为拒绝,不知道为何没有3,这可能是个藏bug的地方
if direction == "1":
print(from_id+" send"+amount+" to "+receive_id+"and say:"+pay_memo)
elif direction == "3":
print(receive_id+" have received"+amount+" from "+from_id+" .transaction finished.")
elif direction == "4":
print(receive_id+" have rejected"+amount+" from "+from_id+" .transaction abort.")
再次提醒,这个是对于转账类型的,不是红包,对于红包消息类型,padlocal+python-wechaty可以识别但无法解析内容(红包的金额不会写在消息中,是打开才知道的)
欢迎大家补充正则搭配用发,可以极大拓展python-wechaty的功能,但需要注意,微信里面有些符号不一定是“看上去”那样的,比如金额中的小数点用\d无法匹配,还有引用消息的换行用\n也无法匹配……都是坑啊……
你的这个issue和关于转账的脚本很有实践价值,我建议你可以将其添加到python-wechaty 文档.开箱即用当中,这样就可以长期保存以及被协同优化。
请问self.Friendship.search( phone='183888888888' )
,用mobile查询微信联系人信息的功能还能用吗?我走的padlocal
mark,这太多坑了啊
mark,这太多坑了啊
更新最新版,很多问题是解决了的
关于聊天记录的识别,现在修复了吗
14、从缓存中提取之前的消息(nb)这个功能没法儿实现,暂不知道是padlocal不支持还是我代码实现不对。 补充:再次试过了,无论是按room_id 还是 contact_id,还是text 都无法找到,不报错,就是找不到,可能跟缓存机制有关 (find和findall都一样,测试代码如下)
补充两个神奇的问题:
-
find
与find_all
方法是classmethod
,过程中会调用cls._puppet
,然而cls._puppet
是空的,所以使用Message.find()
会报错; - 如果去掉
@classmethod
注释,使用实例调用,find
与find_all
最终会调用PuppetService.message_search()
,然而这个方法是空的,仅仅是返回了一个[]
而已。┓( ´∀` )┏