python-wechaty icon indicating copy to clipboard operation
python-wechaty copied to clipboard

有关padlocal协议能实现的和不能实现的实践总结【抛砖引玉】

Open bigbrother666sh opened this issue 2 years ago • 13 comments

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())

最后再次欢迎大家跟帖完善总结,这将给所有人带来便利!

bigbrother666sh avatar Apr 10 '22 05:04 bigbrother666sh

以上测试日期:2022-4-10

bigbrother666sh avatar Apr 10 '22 05:04 bigbrother666sh

Great shares! This would be great to be published as a blog post for the community!

huan avatar Apr 13 '22 01:04 huan

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.

wj-Mcat avatar Apr 14 '22 02:04 wj-Mcat

另外补充一个新发现的,关于 mention_list和msg.mention_text, 对于 @所有人 这样的操作,mention_list 是能够正确识别的,但是mention_text 就无法正常的 把 @所有人 去掉

bigbrother666sh avatar Apr 14 '22 03:04 bigbrother666sh

  • [ ] 7、非微信原生emoj不会被正确识别(被识别为?,表情包被识别为xml),另外emoj的消息类型不是img,这是两个type

这一点我再多说明下吧,如果对方是用手机客户端,应该没问题,因为emoj就一页,都是默认的,除非他发自定义表情包,那个就是img格式的消息了。 但如果对方使用的是Windows或者mac微信客户端就不好说了,点表情图标后出来的emoj只有第一页的可以被正确识别,从第二页开始都不会被正确识别

bigbrother666sh avatar Apr 14 '22 16:04 bigbrother666sh

  • [ ] 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拿不到,会报错…… 实在搞不懂了,正则就是时间黑洞啊,等其他兄弟补充完善吧

bigbrother666sh avatar Apr 14 '22 16:04 bigbrother666sh

  • [ ] 7、非微信原生emoj不会被正确识别(被识别为?,表情包被识别为xml),另外emoj的消息类型不是img,这是两个type

这一点我再多说明下吧,如果对方是用手机客户端,应该没问题,因为emoj就一页,都是默认的,除非他发自定义表情包,那个就是img格式的消息了。 但如果对方使用的是Windows或者mac微信客户端就不好说了,点表情图标后出来的emoj只有第一页的可以被正确识别,从第二页开始都不会被正确识别

如果不能被正确识别,那会被识别成什么? 可以提供一下原发送emoji 和接受后的错误emoji 吗?

wj-Mcat avatar Apr 14 '22 23:04 wj-Mcat

从这个头像开始😃【仔细看了下,手机客户端没有这些,电脑上才有】

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

bigbrother666sh avatar Apr 15 '22 03:04 bigbrother666sh

再补充个经验(也许回头可以整理个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可以识别但无法解析内容(红包的金额不会写在消息中,是打开才知道的)

bigbrother666sh avatar Apr 15 '22 07:04 bigbrother666sh

欢迎大家补充正则搭配用发,可以极大拓展python-wechaty的功能,但需要注意,微信里面有些符号不一定是“看上去”那样的,比如金额中的小数点用\d无法匹配,还有引用消息的换行用\n也无法匹配……都是坑啊……

bigbrother666sh avatar Apr 15 '22 07:04 bigbrother666sh

你的这个issue和关于转账的脚本很有实践价值,我建议你可以将其添加到python-wechaty 文档.开箱即用当中,这样就可以长期保存以及被协同优化。

wj-Mcat avatar Apr 17 '22 01:04 wj-Mcat

你的这个issue和关于转账的脚本很有实践价值,我建议你可以将其添加到python-wechaty 文档.开箱即用当中,这样就可以长期保存以及被协同优化。

也是通过jelly 项目的PR?

bigbrother666sh avatar Apr 17 '22 03:04 bigbrother666sh

请问self.Friendship.search( phone='183888888888' ),用mobile查询微信联系人信息的功能还能用吗?我走的padlocal

duanghuang avatar Sep 02 '22 06:09 duanghuang

mark,这太多坑了啊

Pengchengistaken avatar Sep 29 '22 13:09 Pengchengistaken

mark,这太多坑了啊

更新最新版,很多问题是解决了的

bigbrother666sh avatar Oct 05 '22 13:10 bigbrother666sh

关于聊天记录的识别,现在修复了吗

lain-github avatar Dec 02 '22 06:12 lain-github

14、从缓存中提取之前的消息(nb)这个功能没法儿实现,暂不知道是padlocal不支持还是我代码实现不对。 补充:再次试过了,无论是按room_id 还是 contact_id,还是text 都无法找到,不报错,就是找不到,可能跟缓存机制有关 (find和findall都一样,测试代码如下)

补充两个神奇的问题:

  1. findfind_all方法是classmethod,过程中会调用cls._puppet,然而cls._puppet是空的,所以使用Message.find()会报错;
  2. 如果去掉@classmethod注释,使用实例调用,findfind_all最终会调用PuppetService.message_search(),然而这个方法是空的,仅仅是返回了一个[]而已。 image ┓( ´∀` )┏

HengeLiu avatar Dec 04 '23 04:12 HengeLiu