wxRobot.py 32.6 KB
Newer Older
1 2 3 4 5 6 7
# -*- coding: utf-8 -*-
"""
Created on Thu Feb 24 16:19:48 2022

@author: ljc545w
"""

L
ljc545w 已提交
8
# Before use,execute `CWeChatRobot.exe /regserver` in cmd by admin user
L
ljc545w 已提交
9
import os
L
ljc545w 已提交
10
import ctypes
L
ljc545w 已提交
11
import time
12
import json
L
ljc545w 已提交
13
import ctypes.wintypes
L
ljc545w 已提交
14
import socketserver
15
import threading
L
ljc545w 已提交
16 17
# need `pip install comtypes`
import comtypes.client
L
ljc545w 已提交
18 19
from comtypes.client import GetEvents
from comtypes.client import PumpEvents
L
ljc545w 已提交
20

L
ljc545w 已提交
21 22

class _WeChatRobotClient:
L
ljc545w 已提交
23
    _instance = None
L
ljc545w 已提交
24

L
ljc545w 已提交
25
    @classmethod
L
ljc545w 已提交
26
    def instance(cls) -> '_WeChatRobotClient':
L
ljc545w 已提交
27 28 29
        if not cls._instance:
            cls._instance = cls()
        return cls._instance
L
ljc545w 已提交
30

L
ljc545w 已提交
31 32 33
    def __init__(self):
        self.robot = comtypes.client.CreateObject("WeChatRobot.CWeChatRobot")
        self.event = comtypes.client.CreateObject("WeChatRobot.RobotEvent")
L
ljc545w 已提交
34 35
        self.com_pid = self.robot.CStopRobotService(0)

L
ljc545w 已提交
36 37 38 39 40
    @classmethod
    def __del__(cls):
        import psutil
        if cls._instance is not None:
            try:
L
ljc545w 已提交
41 42
                com_process = psutil.Process(cls._instance.com_pid)
                com_process.kill()
L
ljc545w 已提交
43 44 45
            except psutil.NoSuchProcess:
                pass
        cls._instance = None
L
ljc545w 已提交
46

L
ljc545w 已提交
47 48

class WeChatEventSink:
L
ljc545w 已提交
49 50 51 52
    """
    接收消息的默认回调,可以自定义,并将实例化对象作为StartReceiveMsgByEvent参数
    自定义的类需要包含以下所有成员
    """
L
ljc545w 已提交
53 54

    def OnGetMessageEvent(self, msg):
L
ljc545w 已提交
55
        msg = json.loads(msg[0])
L
ljc545w 已提交
56
        print(msg)
L
ljc545w 已提交
57

L
ljc545w 已提交
58 59 60 61 62

class ReceiveMsgBaseServer(socketserver.BaseRequestHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

L
ljc545w 已提交
63 64 65 66 67
    def handle(self):
        conn = self.request
        comtypes.CoInitialize()
        while True:
            try:
68 69
                ptr_data = b""
                while True:
L
ljc545w 已提交
70
                    data = conn.recv(1024)
L
ljc545w 已提交
71
                    ptr_data += data
72 73 74 75
                    if len(data) == 0 or data[-1] == 0xA:
                        break
                msg = json.loads(ptr_data.decode('utf-8'))
                ReceiveMsgBaseServer.msg_callback(msg)
L
ljc545w 已提交
76 77
            except OSError:
                break
78 79 80
            except json.JSONDecodeError:
                pass
            conn.sendall("200 OK".encode())
L
ljc545w 已提交
81 82
        conn.close()
        comtypes.CoUninitialize()
L
ljc545w 已提交
83 84

    @staticmethod
85
    def msg_callback(msg):
L
ljc545w 已提交
86
        # 主线程中已经注入,此处禁止调用StartService和StopService
L
ljc545w 已提交
87 88
        robot = comtypes.client.CreateObject("WeChatRobot.CWeChatRobot")
        event = comtypes.client.CreateObject("WeChatRobot.RobotEvent")
89 90
        wx = WeChatRobot(msg['pid'], robot, event)
        userinfo = wx.GetWxUserInfo(msg['wxid'])
L
ljc545w 已提交
91
        msg['alias'] = userinfo['wxNumber']
92 93 94
        if msg['isSendMsg'] == 0:
            if '@chatroom' in msg['sender']:
                chatroom_info = wx.GetWxUserInfo(msg['sender'])
L
ljc545w 已提交
95
                msg['chatroom_name'] = chatroom_info['wxNickName']
96
                msg['nickname'] = wx.GetChatRoomMemberNickname(msg['sender'], msg['wxid'])
L
ljc545w 已提交
97 98 99
            else:
                msg['nickname'] = userinfo['wxNickName']
        # TODO: 在这里写额外的消息处理逻辑
L
ljc545w 已提交
100

L
ljc545w 已提交
101
        print(msg)
L
ljc545w 已提交
102 103
        robot.Release()
        event.Release()
104

L
ljc545w 已提交
105 106 107

class ChatSession:
    def __init__(self, pid, robot, wxid):
L
ljc545w 已提交
108
        self.pid = pid
109
        self.robot = robot
L
ljc545w 已提交
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
        self.chat_with = wxid

    def SendText(self, msg):
        return self.robot.CSendText(self.pid, self.chat_with, msg)

    def SendImage(self, img_path):
        return self.robot.CSendImage(self.pid, self.chat_with, img_path)

    def SendFile(self, filepath):
        return self.robot.CSendFile(self.pid, self.chat_with, filepath)

    def SendMp4(self, mp4path):
        return self.robot.CSendImage(self.pid, self.chat_with, mp4path)

    def SendArticle(self, title, abstract, url, img_path=None):
        return self.robot.CSendArticle(self.pid, self.chat_with, title, abstract, url, img_path)

    def SendCard(self, shared_wxid, nickname):
        return self.robot.CSendCard(self.pid, self.chat_with, shared_wxid, nickname)

    def SendAtText(self, wxid: list or str or tuple, msg, auto_nickname=True):
        if '@chatroom' not in self.chat_with:
L
ljc545w 已提交
132
            return 1
L
ljc545w 已提交
133 134 135 136 137
        return self.robot.CSendAtText(self.pid, self.chat_with, wxid, msg, auto_nickname)

    def SendAppMsg(self, appid):
        return self.robot.CSendAppMsg(self.pid, self.chat_with, appid)

L
ljc545w 已提交
138

L
ljc545w 已提交
139
class WeChatRobot:
140

L
ljc545w 已提交
141
    def __init__(self, pid: int = 0, robot=None, event=None):
L
ljc545w 已提交
142 143 144
        self.pid = pid
        self.robot = robot or _WeChatRobotClient.instance().robot
        self.event = event or _WeChatRobotClient.instance().event
145
        self.AddressBook = []
L
ljc545w 已提交
146

L
ljc545w 已提交
147 148 149 150 151 152 153 154 155 156
    def StartService(self) -> int:
        """
        注入DLL到微信以启动服务

        Returns
        -------
        int
            0成功,非0失败.

        """
L
ljc545w 已提交
157
        status = self.robot.CStartRobotService(self.pid)
158
        return status
L
ljc545w 已提交
159

L
ljc545w 已提交
160 161 162 163 164 165 166 167 168 169
    def IsWxLogin(self) -> int:
        """
        获取微信登录状态

        Returns
        -------
        bool
            微信登录状态.

        """
L
ljc545w 已提交
170
        return self.robot.CIsWxLogin(self.pid)
171

L
ljc545w 已提交
172
    def SendText(self, receiver: str, msg: str) -> int:
L
ljc545w 已提交
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
        """
        发送文本消息

        Parameters
        ----------
        receiver : str
            消息接收者wxid.
        msg : str
            消息内容.

        Returns
        -------
        int
            0成功,非0失败.

        """
L
ljc545w 已提交
189 190 191
        return self.robot.CSendText(self.pid, receiver, msg)

    def SendImage(self, receiver: str, img_path: str) -> int:
L
ljc545w 已提交
192 193 194 195 196 197 198
        """
        发送图片消息

        Parameters
        ----------
        receiver : str
            消息接收者wxid.
L
ljc545w 已提交
199
        img_path : str
L
ljc545w 已提交
200 201 202 203 204 205 206 207
            图片绝对路径.

        Returns
        -------
        int
            0成功,非0失败.

        """
L
ljc545w 已提交
208 209 210
        return self.robot.CSendImage(self.pid, receiver, img_path)

    def SendFile(self, receiver: str, filepath: str) -> int:
L
ljc545w 已提交
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
        """
        发送文件

        Parameters
        ----------
        receiver : str
            消息接收者wxid.
        filepath : str
            文件绝对路径.

        Returns
        -------
        int
            0成功,非0失败.

        """
L
ljc545w 已提交
227 228 229
        return self.robot.CSendFile(self.pid, receiver, filepath)

    def SendArticle(self, receiver: str, title: str, abstract: str, url: str, img_path: str or None = None) -> int:
L
ljc545w 已提交
230 231 232 233 234 235 236 237 238 239 240 241 242
        """
        发送XML文章

        Parameters
        ----------
        receiver : str
            消息接收者wxid.
        title : str
            消息卡片标题.
        abstract : str
            消息卡片摘要.
        url : str
            文章链接.
L
ljc545w 已提交
243
        img_path : str or None, optional
L
ljc545w 已提交
244 245 246 247 248 249 250 251
            消息卡片显示的图片绝对路径,不需要可以不指定. The default is None.

        Returns
        -------
        int
            0成功,非0失败.

        """
L
ljc545w 已提交
252 253 254
        return self.robot.CSendArticle(self.pid, receiver, title, abstract, url, img_path)

    def SendCard(self, receiver: str, shared_wxid: str, nickname: str) -> int:
L
ljc545w 已提交
255 256 257 258 259 260 261
        """
        发送名片

        Parameters
        ----------
        receiver : str
            消息接收者wxid.
L
ljc545w 已提交
262
        shared_wxid : str
L
ljc545w 已提交
263 264 265 266 267 268 269 270 271 272
            被分享人wxid.
        nickname : str
            名片显示的昵称.

        Returns
        -------
        int
            0成功,非0失败.

        """
L
ljc545w 已提交
273 274 275
        return self.robot.CSendCard(self.pid, receiver, shared_wxid, nickname)

    def SendAtText(self, chatroom_id: str, at_users: list or str or tuple, msg: str, auto_nickname: bool = True) -> int:
L
ljc545w 已提交
276 277 278 279 280
        """
        发送群艾特消息,艾特所有人可以将AtUsers设置为`notify@all`
        无目标群管理权限请勿使用艾特所有人
        Parameters
        ----------
L
ljc545w 已提交
281
        chatroom_id : str
L
ljc545w 已提交
282
            群聊ID.
L
ljc545w 已提交
283
        at_users : list or str or tuple
L
ljc545w 已提交
284 285 286
            被艾特的人列表.
        msg : str
            消息内容.
L
ljc545w 已提交
287
        auto_nickname : bool, optional
L
ljc545w 已提交
288 289 290 291 292 293 294 295
            是否自动填充被艾特人昵称. 默认自动填充.

        Returns
        -------
        int
            0成功,非0失败.

        """
L
ljc545w 已提交
296
        if '@chatroom' not in chatroom_id:
L
ljc545w 已提交
297
            return 1
L
ljc545w 已提交
298
        return self.robot.CSendAtText(self.pid, chatroom_id, at_users, msg, auto_nickname)
L
ljc545w 已提交
299

L
ljc545w 已提交
300 301 302 303 304 305 306 307 308 309
    def GetSelfInfo(self) -> dict:
        """
        获取个人信息

        Returns
        -------
        dict
            调用成功返回个人信息,否则返回空字典.

        """
310 311
        self_info = self.robot.CGetSelfInfo(self.pid)
        return json.loads(self_info)
L
ljc545w 已提交
312

L
ljc545w 已提交
313 314
    def StopService(self) -> int:
        """
L
ljc545w 已提交
315
        停止服务,会将DLL从微信进程中卸载
L
ljc545w 已提交
316 317 318 319 320 321 322

        Returns
        -------
        int
            COM进程pid.

        """
L
ljc545w 已提交
323 324 325
        com_pid = self.robot.CStopRobotService(self.pid)
        return com_pid

L
ljc545w 已提交
326 327 328 329 330 331 332 333 334 335
    def GetAddressBook(self) -> list:
        """
        获取联系人列表

        Returns
        -------
        list
            调用成功返回通讯录列表,调用失败返回空列表.

        """
L
ljc545w 已提交
336
        try:
L
ljc545w 已提交
337 338
            friend_tuple = self.robot.CGetFriendList(self.pid)
            self.AddressBook = [dict(i) for i in list(friend_tuple)]
L
ljc545w 已提交
339 340
        except IndexError:
            self.AddressBook = []
341
        return self.AddressBook
L
ljc545w 已提交
342

L
ljc545w 已提交
343 344 345 346 347 348 349 350 351 352
    def GetFriendList(self) -> list:
        """
        从通讯录列表中筛选出好友列表

        Returns
        -------
        list
            好友列表.

        """
353 354
        if not self.AddressBook:
            self.GetAddressBook()
355 356
        friend_list = [item for item in self.AddressBook \
                       if (item['wxType'] == 3 and item['wxid'][0:3] != 'gh_')]
L
ljc545w 已提交
357 358
        return friend_list

L
ljc545w 已提交
359 360 361 362 363 364 365 366 367 368
    def GetChatRoomList(self) -> list:
        """
        从通讯录列表中筛选出群聊列表

        Returns
        -------
        list
            群聊列表.

        """
369 370
        if not self.AddressBook:
            self.GetAddressBook()
371 372
        chatroom_list = [item for item in self.AddressBook \
                         if item['wxType'] == 2]
L
ljc545w 已提交
373 374
        return chatroom_list

L
ljc545w 已提交
375 376 377 378 379 380 381 382 383 384
    def GetOfficialAccountList(self) -> list:
        """
        从通讯录列表中筛选出公众号列表

        Returns
        -------
        list
            公众号列表.

        """
385 386
        if not self.AddressBook:
            self.GetAddressBook()
387 388 389
        official_account_list = [item for item in self.AddressBook \
                                 if (item['wxType'] == 3 and \
                                     item['wxid'][0:3] == 'gh_')]
L
ljc545w 已提交
390 391 392
        return official_account_list

    def GetFriendByWxRemark(self, remark: str) -> dict or None:
L
ljc545w 已提交
393 394 395 396 397 398 399 400 401 402 403 404 405 406
        """
        通过备注搜索联系人

        Parameters
        ----------
        remark : str
            好友备注.

        Returns
        -------
        dict or None
            搜索到返回联系人信息,否则返回None.

        """
407 408 409 410 411 412
        if not self.AddressBook:
            self.GetAddressBook()
        for item in self.AddressBook:
            if item['wxRemark'] == remark:
                return item
        return None
L
ljc545w 已提交
413 414

    def GetFriendByWxNumber(self, wx_number: str) -> dict or None:
L
ljc545w 已提交
415 416 417 418 419
        """
        通过微信号搜索联系人

        Parameters
        ----------
L
ljc545w 已提交
420
        wx_number : str
L
ljc545w 已提交
421 422 423 424 425 426 427 428
            联系人微信号.

        Returns
        -------
        dict or None
            搜索到返回联系人信息,否则返回None.

        """
429 430 431
        if not self.AddressBook:
            self.GetAddressBook()
        for item in self.AddressBook:
L
ljc545w 已提交
432
            if item['wxNumber'] == wx_number:
433 434
                return item
        return None
L
ljc545w 已提交
435 436

    def GetFriendByWxNickName(self, nickname: str) -> dict or None:
L
ljc545w 已提交
437 438 439 440 441
        """
        通过昵称搜索联系人

        Parameters
        ----------
L
ljc545w 已提交
442
        nickname : str
L
ljc545w 已提交
443 444 445 446 447 448 449 450
            联系人昵称.

        Returns
        -------
        dict or None
            搜索到返回联系人信息,否则返回None.

        """
451 452 453
        if not self.AddressBook:
            self.GetAddressBook()
        for item in self.AddressBook:
L
ljc545w 已提交
454
            if item['wxNickName'] == nickname:
455 456
                return item
        return None
L
ljc545w 已提交
457 458

    def GetChatSession(self, wxid: str) -> 'ChatSession':
L
ljc545w 已提交
459 460 461 462 463 464 465 466 467 468 469 470 471 472
        """
        创建一个会话,没太大用处

        Parameters
        ----------
        wxid : str
            联系人wxid.

        Returns
        -------
        'ChatSession'
            返回ChatSession类.

        """
L
ljc545w 已提交
473 474 475
        return ChatSession(self.pid, self.robot, wxid)

    def GetWxUserInfo(self, wxid: str) -> dict:
L
ljc545w 已提交
476 477 478 479 480 481 482 483 484 485 486 487 488 489
        """
        通过wxid查询联系人信息

        Parameters
        ----------
        wxid : str
            联系人wxid.

        Returns
        -------
        dict
            联系人信息.

        """
490 491
        userinfo = self.robot.CGetWxUserInfo(self.pid, wxid)
        return json.loads(userinfo)
L
ljc545w 已提交
492 493

    def GetChatRoomMembers(self, chatroom_id: str) -> dict or None:
L
ljc545w 已提交
494 495 496 497 498
        """
        获取群成员信息

        Parameters
        ----------
L
ljc545w 已提交
499
        chatroom_id : str
L
ljc545w 已提交
500 501 502 503
            群聊id.

        Returns
        -------
L
ljc545w 已提交
504 505
        dict or None
            获取成功返回群成员信息,失败返回None.
L
ljc545w 已提交
506 507

        """
L
ljc545w 已提交
508
        info = dict(self.robot.CGetChatRoomMembers(self.pid, chatroom_id))
L
ljc545w 已提交
509 510 511
        if not info:
            return None
        members = info['members'].split('^G')
L
ljc545w 已提交
512
        data = self.GetWxUserInfo(chatroom_id)
L
ljc545w 已提交
513 514
        data['members'] = []
        for member in members:
L
ljc545w 已提交
515 516
            member_info = self.GetWxUserInfo(member)
            data['members'].append(member_info)
L
ljc545w 已提交
517
        return data
L
ljc545w 已提交
518 519

    def CheckFriendStatus(self, wxid: str) -> int:
L
ljc545w 已提交
520 521 522 523 524 525 526 527 528 529 530 531
        """
        获取好友状态码

        Parameters
        ----------
        wxid : str
            好友wxid.

        Returns
        -------
        int
            0x0: 'Unknown',
532 533
            0xB0:'被删除',
            0xB1:'是好友',
L
ljc545w 已提交
534
            0xB2:'已拉黑',
535
            0xB5:'被拉黑',
L
ljc545w 已提交
536 537

        """
L
ljc545w 已提交
538 539
        return self.robot.CCheckFriendStatus(self.pid, wxid)

L
ljc545w 已提交
540
    # 接收消息的函数
L
ljc545w 已提交
541
    def StartReceiveMessage(self, port: int = 10808) -> int:
L
ljc545w 已提交
542
        """
L
ljc545w 已提交
543
        启动接收消息Hook
L
ljc545w 已提交
544 545 546

        Parameters
        ----------
L
ljc545w 已提交
547
        port : int
L
ljc545w 已提交
548
            socket的监听端口号.如果要使用连接点回调,则将端口号设置为0.
L
ljc545w 已提交
549 550 551 552 553 554 555

        Returns
        -------
        int
            启动成功返回0,失败返回非0值.

        """
L
ljc545w 已提交
556
        status = self.robot.CStartReceiveMessage(self.pid, port)
557
        return status
L
ljc545w 已提交
558

L
ljc545w 已提交
559
    def StopReceiveMessage(self) -> int:
L
ljc545w 已提交
560
        """
L
ljc545w 已提交
561
        停止接收消息Hook
L
ljc545w 已提交
562 563 564

        Returns
        -------
L
ljc545w 已提交
565 566
        int
            成功返回0,失败返回非0值.
L
ljc545w 已提交
567 568

        """
L
ljc545w 已提交
569 570
        status = self.robot.CStopReceiveMessage(self.pid)
        return status
L
ljc545w 已提交
571

L
ljc545w 已提交
572 573 574 575 576 577 578 579 580 581
    def GetDbHandles(self) -> dict:
        """
        获取数据库句柄和表信息

        Returns
        -------
        dict
            数据库句柄和表信息.

        """
L
ljc545w 已提交
582 583
        tables_tuple = self.robot.CGetDbHandles(self.pid)
        tables = [dict(i) for i in tables_tuple]
L
ljc545w 已提交
584 585 586 587
        dbs = {}
        for table in tables:
            dbname = table['dbname']
            if dbname not in dbs.keys():
L
ljc545w 已提交
588
                dbs[dbname] = {'Handle': table['Handle'], 'tables': []}
L
ljc545w 已提交
589
            dbs[dbname]['tables'].append(
L
ljc545w 已提交
590
                {'name': table['name'], 'tbl_name': table['tbl_name'],
L
ljc545w 已提交
591
                 'root_page': table['rootpage'], 'sql': table['sql']}
L
ljc545w 已提交
592
            )
L
ljc545w 已提交
593
        return dbs
L
ljc545w 已提交
594 595

    def ExecuteSQL(self, handle: int, sql: str) -> list:
L
ljc545w 已提交
596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611
        """
        执行SQL

        Parameters
        ----------
        handle : int
            数据库句柄.
        sql : str
            SQL.

        Returns
        -------
        list
            查询结果.

        """
L
ljc545w 已提交
612
        result = self.robot.CExecuteSQL(self.pid, handle, sql)
L
ljc545w 已提交
613 614
        if len(result) == 0:
            return []
L
ljc545w 已提交
615 616 617 618
        query_list = []
        keys = list(result[0])
        for item in result[1:]:
            query_dict = {}
L
ljc545w 已提交
619
            for key, value in zip(keys, item):
L
ljc545w 已提交
620 621 622
                query_dict[key] = value if not isinstance(value, tuple) else bytes(value)
            query_list.append(query_dict)
        return query_list
L
ljc545w 已提交
623 624

    def BackupSQLiteDB(self, handle: int, filepath: str) -> int:
L
ljc545w 已提交
625 626 627 628 629 630 631
        """
        备份数据库

        Parameters
        ----------
        handle : int
            数据库句柄.
L
ljc545w 已提交
632
        filepath : int
L
ljc545w 已提交
633 634 635 636 637 638 639 640
            备份文件保存位置.

        Returns
        -------
        int
            成功返回0,失败返回非0值.

        """
L
ljc545w 已提交
641 642 643 644 645 646 647
        filepath = filepath.replace('/', '\\')
        save_path = filepath.replace(filepath.split('\\')[-1], '')
        if not os.path.exists(save_path):
            os.makedirs(save_path)
        return self.robot.CBackupSQLiteDB(self.pid, handle, filepath)

    def VerifyFriendApply(self, v3: str, v4: str) -> int:
L
ljc545w 已提交
648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663
        """
        通过好友请求

        Parameters
        ----------
        v3 : str
            v3数据(encryptUserName).
        v4 : str
            v4数据(ticket).

        Returns
        -------
        int
            成功返回0,失败返回非0值..

        """
L
ljc545w 已提交
664 665 666
        return self.robot.CVerifyFriendApply(self.pid, v3, v4)

    def AddFriendByWxid(self, wxid: str, message: str or None) -> int:
L
ljc545w 已提交
667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682
        """
        wxid加好友

        Parameters
        ----------
        wxid : str
            要添加的wxid.
        message : str or None
            验证信息.

        Returns
        -------
        int
            请求发送成功返回0,失败返回非0值.

        """
L
ljc545w 已提交
683 684 685
        return self.robot.CAddFriendByWxid(self.pid, wxid, message)

    def AddFriendByV3(self, v3: str, message: str or None, add_type: int = 0x6) -> int:
L
ljc545w 已提交
686
        """
L
ljc545w 已提交
687 688 689 690
        v3数据加好友

        Parameters
        ----------
L
ljc545w 已提交
691
        v3 : str
L
ljc545w 已提交
692 693 694
            v3数据(encryptUserName).
        message : str or None
            验证信息.
L
ljc545w 已提交
695
        add_type : int
L
ljc545w 已提交
696 697 698 699 700 701 702
            添加方式(来源).手机号: 0xF;微信号: 0x3;QQ号: 0x1;朋友验证消息: 0x6.

        Returns
        -------
        int
            请求发送成功返回0,失败返回非0值.

L
ljc545w 已提交
703
        """
L
ljc545w 已提交
704 705
        return self.robot.CAddFriendByV3(self.pid, v3, message, add_type)

L
ljc545w 已提交
706 707 708 709 710 711 712 713 714 715
    def GetWeChatVer(self) -> str:
        """
        获取微信版本号

        Returns
        -------
        str
            微信版本号.

        """
L
ljc545w 已提交
716
        return self.robot.CGetWeChatVer()
L
ljc545w 已提交
717 718

    def GetUserInfoByNet(self, keyword: str) -> dict or None:
L
ljc545w 已提交
719 720 721 722 723 724 725 726 727 728 729 730 731 732
        """
        网络查询用户信息

        Parameters
        ----------
        keyword : str
            查询关键字,可以是微信号、手机号、QQ号.

        Returns
        -------
        dict or None
            查询成功返回用户信息,查询失败返回None.

        """
L
ljc545w 已提交
733
        userinfo = self.robot.CSearchContactByNet(self.pid, keyword)
L
ljc545w 已提交
734 735
        if userinfo:
            return dict(userinfo)
L
ljc545w 已提交
736
        return None
L
ljc545w 已提交
737 738

    def AddBrandContact(self, public_id: str) -> int:
L
ljc545w 已提交
739 740 741 742 743
        """
        关注公众号

        Parameters
        ----------
L
ljc545w 已提交
744
        public_id : str
L
ljc545w 已提交
745 746 747 748 749 750 751 752
            公众号id.

        Returns
        -------
        int
            请求成功返回0,失败返回非0值.

        """
L
ljc545w 已提交
753 754 755
        return self.robot.CAddBrandContact(self.pid, public_id)

    def ChangeWeChatVer(self, version: str) -> int:
L
ljc545w 已提交
756
        """
L
ljc545w 已提交
757 758 759 760 761 762 763 764 765 766 767 768
        自定义微信版本号,一定程度上防止自动更新

        Parameters
        ----------
        version : str
            版本号,类似`3.7.0.26`

        Returns
        -------
        int
            成功返回0,失败返回非0值.

L
ljc545w 已提交
769
        """
L
ljc545w 已提交
770 771 772
        return self.robot.CChangeWeChatVer(self.pid, version)

    def HookImageMsg(self, save_path: str) -> int:
L
ljc545w 已提交
773 774 775 776 777
        """
        开始Hook未加密图片

        Parameters
        ----------
L
ljc545w 已提交
778
        save_path : str
L
ljc545w 已提交
779 780 781 782 783 784 785 786
            图片保存路径(绝对路径).

        Returns
        -------
        int
            成功返回0,失败返回非0值.

        """
L
ljc545w 已提交
787 788
        return self.robot.CHookImageMsg(self.pid, save_path)

L
ljc545w 已提交
789 790 791 792 793 794 795 796 797 798
    def UnHookImageMsg(self) -> int:
        """
        取消Hook未加密图片

        Returns
        -------
        int
            成功返回0,失败返回非0值.

        """
L
ljc545w 已提交
799
        return self.robot.CUnHookImageMsg(self.pid)
L
ljc545w 已提交
800 801

    def HookVoiceMsg(self, save_path: str) -> int:
L
ljc545w 已提交
802 803 804 805 806
        """
        开始Hook语音消息

        Parameters
        ----------
L
ljc545w 已提交
807
        save_path : str
L
ljc545w 已提交
808 809 810 811 812 813 814 815
            语音保存路径(绝对路径).

        Returns
        -------
        int
            成功返回0,失败返回非0值.

        """
L
ljc545w 已提交
816 817
        return self.robot.CHookVoiceMsg(self.pid, save_path)

L
ljc545w 已提交
818 819 820 821 822 823 824 825 826 827
    def UnHookVoiceMsg(self) -> int:
        """
        取消Hook语音消息

        Returns
        -------
        int
            成功返回0,失败返回非0值.

        """
L
ljc545w 已提交
828
        return self.robot.CUnHookVoiceMsg(self.pid)
L
ljc545w 已提交
829

L
ljc545w 已提交
830
    def DeleteUser(self, wxid: str) -> int:
L
ljc545w 已提交
831 832 833 834 835 836 837 838 839 840 841 842 843 844
        """
        删除好友

        Parameters
        ----------
        wxid : str
            被删除好友wxid.

        Returns
        -------
        int
            成功返回0,失败返回非0值.

        """
L
ljc545w 已提交
845
        return self.robot.CDeleteUser(self.pid, wxid)
L
ljc545w 已提交
846

L
ljc545w 已提交
847
    def SendAppMsg(self, wxid: str, appid: str) -> int:
L
ljc545w 已提交
848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863
        """
        发送小程序

        Parameters
        ----------
        wxid : str
            消息接收者wxid.
        appid : str
            小程序id (在xml中是username,不是appid).

        Returns
        -------
        int
            成功返回0,失败返回非0值.

        """
L
ljc545w 已提交
864 865 866
        return self.robot.CSendAppMsg(self.pid, wxid, appid)

    def EditRemark(self, wxid: str, remark: str or None) -> int:
L
ljc545w 已提交
867 868 869 870 871 872
        """
        修改好友或群聊备注

        Parameters
        ----------
        wxid : str
L
ljc545w 已提交
873
            wxid或chatroom_id.
L
ljc545w 已提交
874 875 876 877 878 879 880 881 882
        remark : str or None
            要修改的备注.

        Returns
        -------
        int
            成功返回0,失败返回非0值.

        """
L
ljc545w 已提交
883 884 885
        return self.robot.CEditRemark(self.pid, wxid, remark)

    def SetChatRoomName(self, chatroom_id: str, name: str) -> int:
L
ljc545w 已提交
886 887 888 889 890
        """
        修改群名称.请确认具有相关权限再调用。

        Parameters
        ----------
L
ljc545w 已提交
891
        chatroom_id : str
L
ljc545w 已提交
892 893 894 895 896 897 898 899 900 901
            群聊id.
        name : str
            要修改为的群名称.

        Returns
        -------
        int
            成功返回0,失败返回非0值.

        """
L
ljc545w 已提交
902 903 904
        return self.robot.CSetChatRoomName(self.pid, chatroom_id, name)

    def SetChatRoomAnnouncement(self, chatroom_id: str, announcement: str or None) -> int:
L
ljc545w 已提交
905 906 907 908 909
        """
        设置群公告.请确认具有相关权限再调用。

        Parameters
        ----------
L
ljc545w 已提交
910
        chatroom_id : str
L
ljc545w 已提交
911 912 913 914 915 916 917 918 919 920
            群聊id.
        announcement : str or None
            公告内容.

        Returns
        -------
        int
            成功返回0,失败返回非0值.

        """
L
ljc545w 已提交
921 922 923
        return self.robot.CSetChatRoomAnnouncement(self.pid, chatroom_id, announcement)

    def SetChatRoomSelfNickname(self, chatroom_id: str, nickname: str) -> int:
L
ljc545w 已提交
924 925 926 927 928
        """
        设置群内个人昵称

        Parameters
        ----------
L
ljc545w 已提交
929
        chatroom_id : str
L
ljc545w 已提交
930 931 932 933 934 935 936 937 938 939
            群聊id.
        nickname : str
            要修改为的昵称.

        Returns
        -------
        int
            成功返回0,失败返回非0值.

        """
L
ljc545w 已提交
940 941 942
        return self.robot.CSetChatRoomSelfNickname(self.pid, chatroom_id, nickname)

    def GetChatRoomMemberNickname(self, chatroom_id: str, wxid: str) -> str:
L
ljc545w 已提交
943 944 945 946 947
        """
        获取群成员昵称

        Parameters
        ----------
L
ljc545w 已提交
948
        chatroom_id : str
L
ljc545w 已提交
949 950 951 952 953 954 955 956 957 958
            群聊id.
        wxid : str
            群成员wxid.

        Returns
        -------
        str
            成功返回群成员昵称,失败返回空字符串.

        """
L
ljc545w 已提交
959 960
        return self.robot.CGetChatRoomMemberNickname(self.pid, chatroom_id, wxid)

L
ljc545w 已提交
961
    def DelChatRoomMember(self, chatroom_id: str, wxid_list: str or list or tuple) -> int:
L
ljc545w 已提交
962 963 964 965 966
        """
        删除群成员.请确认具有相关权限再调用。

        Parameters
        ----------
L
ljc545w 已提交
967
        chatroom_id : str
L
ljc545w 已提交
968
            群聊id.
L
ljc545w 已提交
969
        wxid_list : str or list or tuple
L
ljc545w 已提交
970 971 972 973 974 975 976 977
            要删除的成员wxid或wxid列表.

        Returns
        -------
        int
            成功返回0,失败返回非0值.

        """
L
ljc545w 已提交
978 979
        return self.robot.CDelChatRoomMember(self.pid, chatroom_id, wxid_list)

L
ljc545w 已提交
980
    def AddChatRoomMember(self, chatroom_id: str, wxid_list: str or list or tuple) -> int:
L
ljc545w 已提交
981 982 983 984 985
        """
        添加群成员.请确认具有相关权限再调用。

        Parameters
        ----------
L
ljc545w 已提交
986
        chatroom_id : str
L
ljc545w 已提交
987
            群聊id.
L
ljc545w 已提交
988
        wxid_list : str or list or tuple
L
ljc545w 已提交
989 990 991 992 993 994 995 996
            要添加的成员wxid或wxid列表.

        Returns
        -------
        int
            成功返回0,失败返回非0值.

        """
L
ljc545w 已提交
997 998
        return self.robot.CAddChatRoomMember(self.pid, chatroom_id, wxid_list)

L
ljc545w 已提交
999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039
    def OpenBrowser(self,url: str) -> int:
        """
        打开微信内置浏览器

        Parameters
        ----------
        url : str
            目标网页url.

        Returns
        -------
        int
            成功返回0,失败返回非0值.

        """
        return self.robot.COpenBrowser(self.pid,url)

    def GetHistoryPublicMsg(self,public_id:str,offset:str = "") -> str:
        """
        获取公众号历史消息,一次获取十条推送记录

        Parameters
        ----------
        public_id : str
            公众号id.
        offset : str, optional
            起始偏移,为空的话则从新到久获取十条,该值可从返回数据中取得. The default is "".

        Returns
        -------
        str
            成功返回json数据,失败返回错误信息或空字符串.

        """
        ret = self.robot.CGetHistoryPublicMsg(self.pid,public_id,offset)[0]
        try:
            ret = json.loads(ret)
        except json.JSONDecodeError:
            pass
        return ret

1040
    def ForwardMessage(self,wxid:str,msgid:int) -> int:
1041 1042 1043 1044 1045 1046 1047
        """
        转发消息,只支持单条转发

        Parameters
        ----------
        wxid : str
            消息接收人wxid.
1048 1049
        msgid : int
            消息id,可以在实时消息接口中获取.
1050 1051 1052 1053 1054 1055 1056

        Returns
        -------
        int
            成功返回0,失败返回非0值.

        """
1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076
        return self.robot.CForwardMessage(self.pid,wxid,msgid)

    def GetQrcodeImage(self) -> bytes:
        """
        获取二维码,同时切换到扫码登录

        Returns
        -------
        bytes
            二维码bytes数据.
        You can convert it to image object,like this:
        >>> from io import BytesIO
        >>> from PIL import Image
        >>> buf = wx.GetQrcodeImage()
        >>> image = Image.open(BytesIO(buf)).convert("L")
        >>> image.save('./qrcode.png')

        """
        data = self.robot.CGetQrcodeImage(self.pid)
        return bytes(data)
1077

L
ljc545w 已提交
1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099
    def GetA8Key(self,url:str) -> dict or str:
        """
        获取A8Key

        Parameters
        ----------
        url : str
            公众号文章链接.

        Returns
        -------
        dict
            成功返回A8Key信息,失败返回空字符串.

        """
        ret = self.robot.CGetA8Key(self.pid,url)
        try:
            ret = json.loads(ret)
        except json.JSONDecodeError:
            pass
        return ret

L
ljc545w 已提交
1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132
    def SendXmlMsg(self,wxid:str,xml:str,img_path:str="") -> int:
        """
        发送原始xml消息

        Parameters
        ----------
        wxid : str
            消息接收人.
        xml : str
            xml内容.
        img_path : str, optional
            图片路径. 默认为空.

        Returns
        -------
        int
            发送成功返回0,发送失败返回非0值.

        """
        return self.robot.CSendXmlMsg(self.pid,wxid,xml,img_path)

    def Logout(self) -> int:
        """
        退出登录

        Returns
        -------
        int
            成功返回0,失败返回非0值.

        """
        return self.robot.CLogout(self.pid)

L
ljc545w 已提交
1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153
    def GetTransfer(self,wxid:str,transcationid:str,transferid:str) -> int:
        """
        收款

        Parameters
        ----------
        wxid : str
            转账人wxid.
        transcationid : str
            从转账消息xml中获取.
        transferid : str
            从转账消息xml中获取.

        Returns
        -------
        int
            成功返回0,失败返回非0值.

        """
        return self.robot.CGetTransfer(self.pid,wxid,transcationid,transferid)

L
ljc545w 已提交
1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193
    def SendEmotion(self, wxid: str, img_path: str) -> int:
        """
        发送图片消息

        Parameters
        ----------
        wxid : str
            消息接收者wxid.
        img_path : str
            图片绝对路径.

        Returns
        -------
        int
            0成功,非0失败.

        """
        return self.robot.CSendEmotion(self.pid, wxid, img_path)

    def GetMsgCDN(self,msgid: int) -> str:
        """
        下载图片、视频、文件

        Parameters
        ----------
        msgid : int
            msgid.

        Returns
        -------
        str
            成功返回文件路径,失败返回空字符串.

        """
        path = self.robot.CGetMsgCDN(self.pid,msgid)
        if path != "":
            while not os.path.exists(path):
                time.sleep(0.5)
        return path

L
ljc545w 已提交
1194 1195

def get_wechat_pid_list() -> list:
L
ljc545w 已提交
1196 1197 1198 1199 1200 1201 1202 1203 1204 1205
    """
    获取所有微信pid

    Returns
    -------
    list
        微信pid列表.

    """
    import psutil
L
ljc545w 已提交
1206
    pid_list = []
L
ljc545w 已提交
1207 1208
    process_list = psutil.pids()
    for pid in process_list:
L
ljc545w 已提交
1209 1210 1211 1212
        try:
            if psutil.Process(pid).name() == 'WeChat.exe':
                pid_list.append(pid)
        except psutil.NoSuchProcess:
L
ljc545w 已提交
1213
            pass
L
ljc545w 已提交
1214 1215 1216 1217
    return pid_list


def start_wechat() -> 'WeChatRobot' or None:
L
ljc545w 已提交
1218 1219 1220 1221 1222
    """
    启动微信

    Returns
    -------
L
ljc545w 已提交
1223 1224
    WeChatRobot or None
        成功返回WeChatRobot对象,失败返回None.
L
ljc545w 已提交
1225 1226 1227 1228 1229

    """
    pid = _WeChatRobotClient.instance().robot.CStartWeChat()
    if pid != 0:
        return WeChatRobot(pid)
L
ljc545w 已提交
1230 1231
    return None

L
ljc545w 已提交
1232

1233
def register_msg_event(wx_pid: int, event_sink: 'WeChatEventSink' or None = None) -> None:
L
ljc545w 已提交
1234 1235
    """
    通过COM组件连接点接收消息,真正的回调
1236
    只会收到wx_pid对应的微信消息
L
ljc545w 已提交
1237 1238 1239

    Parameters
    ----------
1240
    wx_pid: 微信PID
L
ljc545w 已提交
1241
    event_sink : object, optional
L
ljc545w 已提交
1242 1243 1244 1245 1246 1247 1248 1249 1250 1251
        回调的实现类,该类要继承`WeChatEventSink`类或实现其中的方法.

    Returns
    -------
    None
        .

    """
    event = _WeChatRobotClient.instance().event
    if event is not None:
L
ljc545w 已提交
1252 1253 1254
        sink = event_sink or WeChatEventSink()
        connection_point = GetEvents(event, sink)
        assert connection_point is not None
1255
        event.CRegisterWxPidWithCookie(wx_pid, connection_point.cookie)
L
ljc545w 已提交
1256 1257 1258
        while True:
            try:
                PumpEvents(2)
L
ljc545w 已提交
1259
            except KeyboardInterrupt:
L
ljc545w 已提交
1260
                break
L
ljc545w 已提交
1261 1262 1263 1264 1265 1266
        del connection_point


def start_socket_server(port: int = 10808,
                        request_handler: 'ReceiveMsgBaseServer' = ReceiveMsgBaseServer,
                        main_thread=True) -> int or None:
L
ljc545w 已提交
1267 1268 1269 1270 1271 1272 1273
    """
    创建消息监听线程

    Parameters
    ----------
    port : int
        socket的监听端口号.
L
ljc545w 已提交
1274

L
ljc545w 已提交
1275 1276
    request_handler : ReceiveMsgBaseServer
        用于处理消息的类,需要继承自socketserver.BaseRequestHandler或ReceiveMsgBaseServer
L
ljc545w 已提交
1277

L
ljc545w 已提交
1278
    main_thread : bool
L
ljc545w 已提交
1279 1280 1281 1282 1283
        是否在主线程中启动server

    Returns
    -------
    int or None
L
ljc545w 已提交
1284
        main_thread为False时返回线程id,否则返回None.
L
ljc545w 已提交
1285 1286

    """
L
ljc545w 已提交
1287
    ip_port = ("127.0.0.1", port)
L
ljc545w 已提交
1288
    try:
L
ljc545w 已提交
1289 1290
        s = socketserver.ThreadingTCPServer(ip_port, request_handler)
        if main_thread:
L
ljc545w 已提交
1291 1292
            s.serve_forever()
        else:
L
ljc545w 已提交
1293
            socket_server = threading.Thread(target=s.serve_forever)
L
ljc545w 已提交
1294 1295 1296 1297 1298 1299 1300 1301
            socket_server.setDaemon(True)
            socket_server.start()
            return socket_server.ident
    except KeyboardInterrupt:
        pass
    except Exception as e:
        print(e)
    return None
L
ljc545w 已提交
1302 1303 1304


def stop_socket_server(thread_id: int) -> None:
L
ljc545w 已提交
1305 1306 1307 1308 1309
    """
    强制结束消息监听线程

    Parameters
    ----------
L
ljc545w 已提交
1310
    thread_id : int
L
ljc545w 已提交
1311 1312 1313 1314 1315 1316 1317 1318
        消息监听线程ID.

    Returns
    -------
    None
        .

    """
L
ljc545w 已提交
1319
    if not thread_id:
L
ljc545w 已提交
1320 1321 1322
        return
    import inspect
    try:
L
ljc545w 已提交
1323 1324
        tid = comtypes.c_long(thread_id)
        res = 0
L
ljc545w 已提交
1325
        if not inspect.isclass(SystemExit):
L
ljc545w 已提交
1326 1327
            exec_type = type(SystemExit)
            res = comtypes.pythonapi.PyThreadState_SetAsyncExc(tid, comtypes.py_object(exec_type))
L
ljc545w 已提交
1328 1329 1330
        if res == 0:
            raise ValueError("invalid thread id")
        elif res != 1:
L
ljc545w 已提交
1331
            ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
L
ljc545w 已提交
1332
            raise SystemError("PyThreadState_SetAsyncExc failed")
L
ljc545w 已提交
1333 1334
    except (ValueError, SystemError):
        pass