0%

微信自动回复

微信机器人三个主要功能:自动回复,撤回消息查看,图灵机器人。
代码:https://github.com/maplesugarr/wxbot-use-itchat

库选择

可以自己写一个,也是比较简单的,但是浪费时间,尤其是改bug会消耗很多精力,github存在的意义就在于此了。使用别人的库,可以学别人的思路,实现各种功能可以参考的代码也比较多。
python版itchat库:https://github.com/littlecodersh/ItChat
java版itchat4j库:https://github.com/yaphone/itchat4j
这里选择python版的itchat库,它的使用说明:https://itchat.readthedocs.io/zh/latest/。

itchat的原理

使用itchat,会在相应文件夹下面输出一张图片qr.png,扫描登陆就行了。其实登陆的是网页版微信。也就是使用itchat,就不能用网页微信了。

使用wireshark或者直接浏览器控制台,就可以得到微信的网页端各种api,包装一下就是itchat了。
如果滥用,会被微信封掉网页端的。我把程序放在了服务器上,ip是还是国外的,已经用了几个月了,也没有什么异常,只是在开始提醒我是不是我自己登陆的。

itchat基本使用

基本的使用查看itchat作者写的文档就行了。

对itchat可能有问题的地方

a)为什么自己从手机发出的消息能够被itchat相关的函数接受?
因为itchat登陆的是网页端,自己从手机发出的消息,微信服务器会自动同步到网页端。
b)为什么要把控制微信自动回复机器人的命令发到文件助手上,而不能发给自己?
从微信自动回复机器人,也就是微信网页端发出的命令,会自动同步到手机上。
从手机上发给文件助手的命令,微信网页端也进行同步,微信自动回复机器人也能接收到。
但是自己发给自己,比如从微信网页端发消息给自己,微信不会同步到手机上,它认为你自己发给自己,你自己是知道的,就不会进行同步。同理从手机发给自己,也不会进行同步,微信网页端也收不到消息。
c)把登陆二维码发到手机上,通过微信扫二维码功能,选择相册中浏览本地图片可以登陆吗?
不可以,显然微信做了限制,只能扫其他设备的二维码才能登陆。

在自动回复过程中,主人在手机上回复给了好友,也就相当于主人知道了好友消息,正在手动回复,这时需要关闭自动回复功能。需要一个自动回复的会话机制。

在接受好友消息的函数中,创建一个会话,会话id(session_id)是msg[‘FromUserName’] + msg[‘ToUserName’],好友的名称+主人的名称。如果在自动回复好友过程中,也就是有一个好友的名称+主人的名称会话,主人再去回复好友,这时拿到的会话是一个,再去处理函数中other_to_host判断如果主人回复了,就把会话中的相关值更改SessionList[session_id][‘HostIn’] = True,不再启用自动回复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
''' 消息接收,isGroupChat = True代表接收群消息,如果写成isMpChat = True就代表接收公众号消息 '''
@itchat.msg_register([TEXT, PICTURE, FRIENDS, CARD, MAP, SHARING, RECORDING, ATTACHMENT, VIDEO, VOICE],isFriendChat=True, isGroupChat=False, isMpChat=False )
def handle_receive_friend_msg(msg):
''' 判断会话是否已经存在 '''
if (msg['FromUserName'] + msg['ToUserName']) in SessionList:
session_id = msg['FromUserName'] + msg['ToUserName']
elif (msg['ToUserName'] + msg['FromUserName']) in SessionList:
session_id = msg['ToUserName'] + msg['FromUserName']
else:
''' 会话不存在,创建会话 。会话对相关信息进行初始化,然后在一个线程中,持续计时 '''
session_id = create_session(msg)

# 创建线程处理会话
new_sess = threading.Thread(target=operate_session, args=(session_id, msg, 'Text'))
new_sess.start()
# 别人发给自己消息处理
def other_to_host(session_id, msg, msg_type):
# 判断主人是否在会话中
if msg['FromUserName'] == Host['UserName']:
SessionList[session_id]['HostIn'] = True

撤回消息查看的逻辑

好友发给你的消息,一定到了你的手机端和网页端,好友撤回消息后,微信会把你手机端和网页端对应的消息撤回。如果好友每发一个消息,就保存下来,撤回后(itchat中撤回消息装饰器参数是itchat.content.NOTE),就把对应的消息发到文件助手上通知,就做到了撤回消息的查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
''' 消息接收,isGroupChat = True代表接收群消息,如果写成isMpChat = True就代表接收公众号消息 '''
@itchat.msg_register([TEXT, PICTURE, FRIENDS, CARD, MAP, SHARING, RECORDING, ATTACHMENT, VIDEO, VOICE],isFriendChat=True, isGroupChat=False, isMpChat=False )
def handle_receive_friend_msg(msg):
''' 缓存消息,用于撤回 '''
msg_time_rec = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) #接受消息的时间
msg_from = FriendsInfoList[msg['FromUserName']]
msg_time = msg['CreateTime'] #信息发送的时间
msg_id = msg['MsgId'] #每条信息的id
msg_content = '' #储存信息的内容
msg_share_url = '' #储存分享的链接,比如分享的文章和音乐
if msg['Type'] == 'Text' or msg['Type'] == 'Friends': #如果发送的消息是文本或者好友推荐
msg_content = msg['Text']

#如果发送的消息是附件、视屏、图片、语音
elif msg['Type'] == "Attachment" or msg['Type'] == "Video" \
or msg['Type'] == 'Picture' \
or msg['Type'] == 'Recording' \
or msg['Type'] == 'Voice':
msg_content = r"" + msg['FileName']
'''
itchat 的附件下载方法存储在 msg 的 Text 键中.
发送的文件名(图片给出的默认文件名), 都存储在 msg 的 FileName 键中.
下载方法, 接受一个可用的位置参数(包括文件名), 并将文件响应的存储.
注意:下载的文件存储在指定的文件中,直接将路径与FileName连接即可,如msg["Text"]('/tmp/weichat'+msg['FileName'])
'''
if not os.path.exists(rec_tmp_dir_friends + time.strftime("%Y%m%d", time.localtime()) + os.sep):
os.makedirs(rec_tmp_dir_friends + time.strftime("%Y%m%d", time.localtime()) + os.sep)
msg['Text'](rec_tmp_dir_friends + time.strftime("%Y%m%d", time.localtime()) + os.sep + msg['FileName'])
#print(msg['Text']+str(msg_content))
elif msg['Type'] == 'Card': #如果消息是推荐的名片
msg_content = msg['RecommendInfo']['NickName'] + '的名片' #内容就是推荐人的昵称和性别
if msg['RecommendInfo']['Sex'] == 1:
msg_content += '性别为男'
else:
msg_content += '性别为女'

elif msg['Type'] == 'Map': #如果消息为分享的位置信息
x, y, location = re.search(
"<location x=\"(.*?)\" y=\"(.*?)\".*label=\"(.*?)\".*", msg['OriContent']).group(1, 2, 3)
if location is None:
msg_content = r"纬度->" + x.__str__() + " 经度->" + y.__str__() #内容为详细的地址
else:
msg_content = r"" + location
elif msg['Type'] == 'Sharing': #如果消息为分享的音乐或者文章,详细的内容为文章的标题或者是分享的名字
msg_content = msg['Text']
msg_share_url = msg['Url'] #记录分享的url
##将信息存储在字典中,每一个msg_id对应一条信息
msg_cache.update(
{
msg_id: {
"msg_from": msg_from, "msg_time": msg_time, "msg_time_rec": msg_time_rec,
"msg_type": msg["Type"],
"msg_content": msg_content, "msg_share_url": msg_share_url
}
}
)

##这个是用于监听是否有friend消息撤回
@itchat.msg_register(itchat.content.NOTE, isFriendChat=True, isGroupChat=False, isMpChat=False)
def note_friends_withdraw_msg(msg):
withdraw_msg(msg, True)

#向文件助手发送撤回消息
#第二个参数,是好友,还是群聊发的
def withdraw_msg(msg,isFriend=True):

#这里如果这里的msg['Content']中包含消息撤回和id,就执行下面的语句
if '撤回了一条消息' in msg['Content']:
old_msg_id = re.search("\<msgid\>(.*?)\<\/msgid\>", msg['Content']).group(1) #在返回的content查找撤回的消息的id
old_msg = msg_cache.get(old_msg_id, None) #得到消息
#防止拿不到的时候程序出错
if old_msg is None:
return;

#发送撤回的提示给文件助手
if isFriend:
msg_body = "【"\
+ old_msg.get('msg_from') + " 撤回了 】\n"\
+ old_msg.get("msg_type") + " 消息:" + "\n" \
+ old_msg.get('msg_time_rec') + "\n" \
+ r"" + old_msg.get('msg_content')
else:
msg_body = "【群聊中:"\
+ old_msg.get('msg_from') + " 撤回了 】\n"\
+ old_msg.get("msg_type") + " 消息:" + "\n" \
+ old_msg.get('msg_time_rec') + "\n" \
+ r"" + old_msg.get('msg_content')
#如果是分享的文件被撤回了,那么就将分享的url加在msg_body中发送给文件助手
if old_msg['msg_type'] == "Sharing":
msg_body += "\n就是这个链接➣ " + old_msg.get('msg_share_url')

# 将撤回消息发送到文件助手
itchat.send_msg(msg_body, toUserName='filehelper')
# 有文件的话也要将文件发送回去
if isFriend:
tmp_dir = rec_tmp_dir_friends
else:
tmp_dir = rec_tmp_dir_group
tmp_dir = tmp_dir + time.strftime("%Y%m%d", time.localtime()) + os.sep
if os.path.exists(os.path.join(tmp_dir, old_msg.get('msg_content'))):
if old_msg["msg_type"] == "Picture" \
or old_msg["msg_type"] == "Recording" \
or old_msg["msg_type"] == "Video" \
or old_msg["msg_type"] == "Attachment" \
or old_msg['msg_type'] == 'Voice':
itchat.send_file(os.path.join(tmp_dir, old_msg.get('msg_content')), toUserName="filehelper")

调用次数限制

为了防止别人频繁的和助手对话,网页端微信可能被封。限制助手在一天内回复特定人的次数。
每次回复都把一个dict类型的(保存着用户名-调用助手的次数)中对应的次数加一。
到特定的次数就把他放到不回复的名单中,明天把这个名单和dict类型(保存着用户名-调用助手的次数)都清空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def set_replytimes(msg):
global FriendsUseTimesList
global NeverListToday
''' 每次调用,先查看是否是另一天了 '''
if Today != str(datetime.date.today()):
for key in FriendsUseTimesList:
FriendsUseTimesList[key] = 0;
NeverListToday.clear()
''' 每次自动回复,对应的用户的使用次数就加1 '''
FriendsUseTimesList[FriendsInfoList[msg['FromUserName']]]+=1
''' 如果大于指定自动回复次数,就拉入黑名单,是内存中的黑名单 '''
if FriendsUseTimesList[FriendsInfoList[msg['FromUserName']]] >= UseTimesLimit:
NeverListToday.append(FriendsInfoList[msg['FromUserName']])
itchat.send_msg('[自动回复]你调戏的次数太多了,已经被加入黑名单,明天恢复。', msg['FromUserName'])

网页控制