├── .gitignore ├── README.md ├── example ├── christmas_hat_robot │ ├── christmas_hat_robot.md │ ├── christmas_hat_robot.py │ ├── hat.py │ └── static │ │ ├── hat.png │ │ └── shape_predictor_5_face_landmarks.dat ├── emoji_robot │ ├── emoji_robot.md │ └── emoji_robot.py └── simple │ ├── mybot_advance.py │ └── mybot_simple.py ├── img ├── christmas_hat_1.jpg ├── christmas_hat_2.jpg ├── christmas_hat_all_test.jpg ├── demo.png ├── group_chat_demo.png ├── onechat_demo.png └── self_demo.jpg └── src └── chatbot.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | tmp 3 | *.pyc 4 | *.pkl 5 | example/img 6 | example/chatbot.pkl 7 | example/chat_robot.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chatbot 2 | ![](https://img.shields.io/badge/chatbot-1.2-green.svg) 3 | ![](https://img.shields.io/badge/python-2.7.x|3.x-brightgreen.svg) 4 | 基于关键词触发的微信机器人框架, 支持Python2.7.x和python3(测试环境python2.7.15和python3.7.1) 5 | 6 | ## 功能 7 | - 识别关键词触发处理函数 8 | - 类falsk的操作,使用装饰器设置关键词 9 | - 聊天信息设置为上下文数据,直接在处理函数中访问 10 | 11 | ## 版本更新 12 | ### 2019-04-12 V1.0 13 | - 关键词触发函数 14 | - 装饰器设置监听 15 | - 聊天信息上下文 16 | ### 2019-04-12 V1.1 17 | - 登录方式配置化 18 | - 日志配置化 19 | - 关键词支持正则 20 | ### 2019-04-15 V1.2 21 | - 支持Python3版本,兼容Python2.7.x 22 | 23 | ## 使用方法(若有变动会随版本更新) 24 | **简单开始** 25 | ```python 26 | import chatbot 27 | 28 | botman = chatbot.Chatbot() 29 | 30 | @botman.listen('你好') 31 | def hello(): 32 | return '你好' 33 | 34 | if __name__ == "__main__": 35 | botman.run() 36 | ``` 37 | 登录方式与itchat登录方式一致,扫描二维码即可。 38 | ![](./img/demo.png) 39 | 40 | **说明** 41 | 使用import引入chatbot,实例化chatbot对象变量botman。 42 | 43 | listen方法设置关键词监听,原型如下: 44 | ```python 45 | listen(self, key_word, isOne=True, isSelf=False, isGroup=False, isAt=False, nickName=None) 46 | ``` 47 | **key_word**为关键词,匹配某条信息的全部内容,不匹配字串 48 | **isOne**设置私聊模式,默认为True,只监听私聊信息中别人的聊天内容 49 | **isSelf**设置本人模式,默认False,监听私聊和群聊中自己的聊天内容 50 | **isGroup**设置群聊模式,默认False,监听群聊中别人的聊天内容 51 | **isAt**设置@属性,默认False,在群聊模式中是否要求@触发 52 | **nickName**私聊模式下,指定nickName的用户触发,输入对方的微信昵称,群聊模式下指定nickName的群聊触发 53 | 54 | 处理函数直接返回字符串即可,chatbot将内容发送回当前上下文的聊天窗口;也支持返回元组以支持更多格式: 55 | ```python 56 | @botman.listen('文本') 57 | def text(): 58 | return 'text', 'Hello world!' 59 | 60 | @botman.listen('图片') 61 | return 'image', './hello.jpg' 62 | ``` 63 | image中返回值元组第二个元素为图片本地路径 64 | 65 | **处理函数中获取聊天内容** 66 | ```python 67 | @botman.listen('回复') 68 | def back(): 69 | msg = chatbot.context.msg 70 | return msg.Text.encode('utf-8') 71 | ``` 72 | context是当前聊天内容上下问,其中msg为本次信息对象,使用线程局部状态thread.local保证多次请求之间上下文隔离。 73 | 74 | **配置** 75 | ```python 76 | from chatbot import Chatbot 77 | conf = { 78 | "login_conf": { 79 | "hotReload": True, 80 | "statusStorageDir": 'chatbot.pkl', 81 | "enableCmdQR": False, 82 | "picDir": None, 83 | "qrCallback": None, 84 | "loginCallback": None, 85 | "exitCallback": None 86 | }, 87 | "logger_conf": { 88 | "path": "./default.log", 89 | "name": "chatbot", 90 | "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", 91 | "level": "DEBUG" 92 | } 93 | } 94 | botman = Chatbot(conf=conf) 95 | ``` 96 | 97 | ## 使用chatbot开发的服务 98 | - [表情包机器人](example/emoji_robot/emoji_robot.md) 99 | - [自动戴圣诞帽服务](example/christmas_hat_robot/christmas_hat_robot.md) 100 | -------------------------------------------------------------------------------- /example/christmas_hat_robot/christmas_hat_robot.md: -------------------------------------------------------------------------------- 1 | # 自动戴圣诞帽机器人 2 | 使用chatbot开发,触发关键词后,获取触发对象的头像,使用opencv库检测人脸模型,并将圣诞帽合成到头像中,成功后返回合成的头像图片。 3 | 4 | ## 功能 5 | 支持个人、私聊、群聊@触发,触发关键词`我要圣诞帽` 6 | 7 | ## 演示 8 | **私聊** 9 | ![](../../img/christmas_hat_2.jpg) 10 | 11 | **个人测试** 12 | ![](../../img/christmas_hat_all_test.jpg) 13 | 14 | ## 代码 15 | [代码](./christmas_hat_robot.py) -------------------------------------------------------------------------------- /example/christmas_hat_robot/christmas_hat_robot.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import sys 4 | import itchat 5 | import time 6 | 7 | import hat 8 | sys.path.append('../../src') 9 | 10 | from chatbot import Chatbot, context 11 | 12 | config = { 13 | "login_conf": { 14 | "hotReload": True, 15 | "statusStorageDir": 'chatbot.pkl', 16 | "enableCmdQR": False, 17 | "picDir": None, 18 | "qrCallback": None, 19 | "loginCallback": None, 20 | "exitCallback": None 21 | }, 22 | "logger_conf": { 23 | "path": "./christmas_hat_robot.log", 24 | "name": "christmas_hat_robot", 25 | "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", 26 | "level": "INFO" 27 | } 28 | } 29 | robot = Chatbot(conf=config) 30 | 31 | @robot.listen('我要圣诞帽', isOne=True, isGroup=True, isSelf=True, isAt=True) 32 | def christmas_hat(): 33 | hat_path = 'static/hat.png' 34 | # 获取用户头像 35 | head_img_content = itchat.get_head_img(context.msg['FromUserName']) 36 | head_img_path = 'img/headImg' + str(int(time.time())) + '.jpg' 37 | with open(head_img_path, 'wb') as f: 38 | f.write(head_img_content) 39 | # 合成图像 40 | combine_img_path = hat.add_hat(head_img_path, hat_path) 41 | if combine_img_path is None: 42 | return "失败了,可能是头像还不够清晰,检测不到人脸:(" 43 | return "image", combine_img_path 44 | 45 | if __name__ == "__main__": 46 | robot.run() -------------------------------------------------------------------------------- /example/christmas_hat_robot/hat.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf8 -*- 2 | 3 | #import hat 4 | import numpy 5 | import json 6 | import dlib 7 | import cv2 8 | import math 9 | 10 | predictor_path = "static/shape_predictor_5_face_landmarks.dat" 11 | 12 | #将图片沿着图片中点逆时针旋转 13 | def rotate(image, angle, center=None, scale=1.0): #1 14 | (h, w) = image.shape[:2] #2 15 | if center is None: #3 16 | center = (w // 2, h // 2) #4 17 | 18 | M = cv2.getRotationMatrix2D(center, angle, scale) #5 19 | 20 | rotated = cv2.warpAffine(image, M, (w, h)) #6 21 | return rotated #7 22 | 23 | # 求p2 -> p1与y正半轴的夹角弧度制 24 | def offset_angle(point1, point2): 25 | len = math.sqrt((point1[0] - point2[0]) * (point1[0] - point2[0]) + (point1[1] - point2[1]) * (point1[1] - point2[1])) 26 | theta = math.acos((point1[1] - point2[1]) / len) 27 | return theta 28 | 29 | def add_hat(head_img_path, hat_img_path): 30 | head_img = cv2.imread(head_img_path) 31 | hat_img = cv2.imread(hat_img_path, -1) 32 | 33 | #加载人脸识别训练模型,检测图片中的人脸图像 34 | predictor = dlib.shape_predictor(predictor_path) 35 | detector = dlib.get_frontal_face_detector() 36 | dets = detector(head_img, 1) 37 | 38 | if len(dets) > 0: 39 | head_img = add_hat_to_face(head_img, hat_img, predictor, dets) 40 | head_with_hat_img_path = head_img_path.replace('.jpg', '') + '_with_hat.jpg' 41 | cv2.imwrite(head_with_hat_img_path, head_img) 42 | return head_with_hat_img_path 43 | else: 44 | return None 45 | 46 | def add_hat_to_face(head_img, hat_img, predictor, dets): 47 | """return the path of head with hat 48 | """ 49 | for d in dets: 50 | # 关键点检测,5个关键 51 | shape = predictor(head_img, d) 52 | 53 | # 计算帽子随人脸的偏转角度 54 | # 右眼的中点 55 | mid1 = ((shape.part(0).x + shape.part(1).x) / 2, (shape.part(0).y + shape.part(1).y) / 2) 56 | # 左眼的中点 57 | mid2 = ((shape.part(2).x + shape.part(3).x) / 2, (shape.part(2).y + shape.part(3).y) / 2) 58 | theta = offset_angle(mid2, mid1) 59 | real_angle = int((math.pi * 2.5 - theta) * 180 / math.pi) 60 | ex_hat = rotate(hat_img, real_angle) 61 | 62 | # 根据人脸大小调整帽子大小, 人脸大小由双眼眼角距离决定 63 | leye = shape.part(0) 64 | reye = shape.part(2) 65 | eye_distance = math.sqrt((leye.x - reye.x) * (leye.x - reye.x) + (leye.y - reye.y) * (leye.y - reye.y)) 66 | # k1参数表示帽子图片相对人脸的大小 67 | k1 = 2.3 * 3 68 | face_width = eye_distance 69 | resized_hat_w = int(face_width * k1) 70 | resized_hat_h = int(hat_img.shape[0] * resized_hat_w / hat_img.shape[1]) 71 | resized_hat = cv2.resize(ex_hat,(resized_hat_w,resized_hat_h)) 72 | 73 | # 求解帽子相对人脸的垂直偏移 74 | # 系数k2表示帽子到双眼所在水平线的距离为双眼所在水平线到门中的距离的k倍 75 | k2 = 1.3 76 | 77 | #门中到双眼线的距离, 这里简化为求门中到双眼中点的距离 78 | mid_eye = ((mid1[0] + mid2[0]) / 2, (mid1[1] + mid2[1]) / 2) 79 | point_nose = shape.part(4) 80 | distance = math.sqrt((mid_eye[0] - point_nose.x) * (mid_eye[0] - point_nose.x) + (mid_eye[1] - point_nose.y) * (mid_eye[1] - point_nose.y)) 81 | 82 | # 求帽子在头像图片中的位置 83 | angle_offset = math.pi - theta 84 | img_bottom = int((d.top() + (resized_hat.shape[0] / 2) - (k2 * distance * math.sin(angle_offset) - (mid_eye[1] - d.top())))) 85 | img_top = int(img_bottom - resized_hat.shape[0]) 86 | img_left = int(k2 * distance * math.cos(angle_offset) + mid_eye[0] - 0.5 * resized_hat.shape[1]) 87 | img_right = int(img_left + resized_hat.shape[1]) 88 | #裁剪图片 89 | if img_left < 0: 90 | delta_left = 0 - img_left 91 | img_left = 0 92 | else: 93 | delta_left = 0 94 | if img_right > head_img.shape[1]: 95 | delta_right = img_right - head_img.shape[1] 96 | img_right = head_img.shape[1] - 1 97 | else: 98 | delta_right = 0 99 | if img_top < 0: 100 | deleta_top = 0 - img_top 101 | img_top = 0 102 | else: 103 | deleta_top = 0 104 | #默认bottom不会超过头像图片 105 | bg_roi = head_img[img_top:img_bottom, img_left:img_right] 106 | hat_roi = resized_hat[deleta_top:resized_hat.shape[0], delta_left:resized_hat.shape[1] - delta_right] 107 | r, g, b, a = cv2.split(hat_roi) 108 | rgb_hat_roi = cv2.merge((r, g, b)) 109 | 110 | mask_inv = cv2.bitwise_not(a) 111 | 112 | ## 原图ROI中提取放帽子的区域 113 | bg_roi = bg_roi.astype(float) 114 | mask_inv = cv2.merge((mask_inv,mask_inv,mask_inv)) 115 | alpha = mask_inv.astype(float)/255 116 | 117 | ## 相乘之前保证两者大小一致(可能会由于四舍五入原因不一致) 118 | alpha = cv2.resize(alpha, (bg_roi.shape[1],bg_roi.shape[0])) 119 | bg = cv2.multiply(alpha, bg_roi) 120 | bg = bg.astype('uint8') 121 | ## cv2.imwrite("bg.jpg",bg) 122 | 123 | ## 提取帽子区域 124 | hat = cv2.bitwise_and(rgb_hat_roi, rgb_hat_roi, mask = a) 125 | ## cv2.imwrite("hat.jpg",hat) 126 | 127 | ## 相加之前保证两者大小一致(可能会由于四舍五入原因不一致) 128 | hat = cv2.resize(hat,(bg_roi.shape[1],bg_roi.shape[0])) 129 | ## 两个ROI区域相加 130 | add_hat = cv2.add(bg, hat) 131 | 132 | ## 把添加好帽子的区域放回原图 133 | head_img[img_top:img_bottom, img_left:img_right] = add_hat 134 | return head_img 135 | -------------------------------------------------------------------------------- /example/christmas_hat_robot/static/hat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexueyuan/chatbot/bbe279d65f3217a05ae0f451b83dc297c3037a71/example/christmas_hat_robot/static/hat.png -------------------------------------------------------------------------------- /example/christmas_hat_robot/static/shape_predictor_5_face_landmarks.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexueyuan/chatbot/bbe279d65f3217a05ae0f451b83dc297c3037a71/example/christmas_hat_robot/static/shape_predictor_5_face_landmarks.dat -------------------------------------------------------------------------------- /example/emoji_robot/emoji_robot.md: -------------------------------------------------------------------------------- 1 | # 表情包机器人 2 | 使用chatbot进行开发,从[bee-ji.com](www.bee-ji.com)随机获取推荐表情包图片(仅供娱乐)返回 3 | 4 | ## 功能 5 | 支持个人、私聊、群聊@触发,触发关键词`我要表情包` 6 | 7 | ## 演示 8 | **个人消息触发** 9 | ![个人消息触发](../../img/self_demo.jpg) 10 | 11 | **私聊消息触发** 12 | ![私聊消息触发](../../img/onechat_demo.png) 13 | 14 | **群聊@消息触发** 15 | ![群聊@消息触发](../../img/group_chat_demo.png) 16 | 17 | ## 代码 18 | [代码](./emoji_robot.py) 19 | -------------------------------------------------------------------------------- /example/emoji_robot/emoji_robot.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | # python2 3 | 4 | import sys 5 | import urllib 6 | import json 7 | import random 8 | import time 9 | import requests 10 | 11 | try: 12 | import urllib2 as requestlib 13 | except ImportError: 14 | import urllib.request as requestlib 15 | 16 | try: 17 | import cookielib as cookielib 18 | except ImportError: 19 | import http.cookiejar as cookielib 20 | 21 | <<<<<<< HEAD:example/emoji_robot/emoji_robot.py 22 | sys.path.append('../../src') 23 | ======= 24 | sys.path.append('../src') 25 | >>>>>>> 56d84344ab1359d059bccefd5417b1c5647b2b30:example/emoji_robot.py 26 | 27 | from chatbot import Chatbot, context 28 | 29 | config = { 30 | "login_conf": { 31 | "hotReload": True, 32 | "statusStorageDir": 'chatbot.pkl', 33 | "enableCmdQR": False, 34 | "picDir": None, 35 | "qrCallback": None, 36 | "loginCallback": None, 37 | "exitCallback": None 38 | }, 39 | "logger_conf": { 40 | "path": "./chat_robot.log", 41 | "name": "chat_robot", 42 | "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", 43 | "level": "DEBUG" 44 | } 45 | } 46 | 47 | robot = Chatbot(conf=config) 48 | 49 | def emoji_image(): 50 | cj = cookielib.LWPCookieJar() 51 | cookie_support = requestlib.HTTPCookieProcessor(cj) 52 | opener = requestlib.build_opener(cookie_support, requestlib.HTTPHandler) 53 | requestlib.install_opener(opener) 54 | 55 | user_agent = 'Mozilla/5.0 (iPad; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.34 (KHTML, like Gecko) Version/11.0 Mobile/15A5341f Safari/604.1' 56 | cookie = "Hm_lvt_65e796f34b9ee7170192209a91520a9a=1555081237; Hm_lpvt_65e796f34b9ee7170192209a91520a9a=1555081315" 57 | url = 'http://www.bee-ji.com/data/search/json' 58 | image_base_url = "http://image.bee-ji.com/" 59 | image_path = './img/' 60 | 61 | req = requestlib.Request(url) 62 | req.add_header('User-Agent', user_agent) 63 | req.add_header('Cookie', cookie) 64 | data = opener.open(req).read() 65 | items = json.loads(data) 66 | if len(items) == 0: 67 | robot.logger.warning("未抓取到图片") 68 | return "哦豁,没了" 69 | 70 | image_item = random.choice(items) 71 | image_url = image_base_url + str(image_item['id']) 72 | robot.logger.info("获取图片url: {}".format(image_url)) 73 | 74 | path = image_path + str(int(time.time())) 75 | rsp = requests.get(image_url) 76 | with open(path, "wb") as f: 77 | f.write(rsp.content) 78 | robot.logger.info("图片保存路径: {}".format(path)) 79 | return 'image', path 80 | 81 | if __name__ == "__main__": 82 | robot.add_listen_rule('我要表情包', emoji_image, isOne=True, isGroup=True, isSelf=True, isAt=True) 83 | robot.run() 84 | -------------------------------------------------------------------------------- /example/simple/mybot_advance.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import sys 4 | 5 | sys.path.append('../src') 6 | 7 | from chatbot import Chatbot, context 8 | 9 | #配置 10 | conf = { 11 | "login_conf": { 12 | "hotReload": True 13 | } 14 | } 15 | botman = Chatbot(conf=conf) 16 | 17 | # 返回对方的用户名 18 | @botman.listen('你好') 19 | def hello(): 20 | fromUserName = botman.get_from_username(context.msg) 21 | return "你好,{}".format(fromUserName) 22 | 23 | # 匹配正则 24 | @botman.listen('大写:[a-zA-Z]*$') 25 | def upword(): 26 | text = context.msg.Text.encode('utf-8') 27 | text.replace('大写:', '') 28 | return text.upper() 29 | 30 | if __name__ == "__main__": 31 | botman.run() -------------------------------------------------------------------------------- /example/simple/mybot_simple.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | # python2 3 | 4 | import sys 5 | 6 | sys.path.append('../src') 7 | 8 | import chatbot 9 | 10 | botman = chatbot.Chatbot() 11 | 12 | @botman.listen('你好') 13 | def hello(): 14 | return '你好' 15 | 16 | if __name__ == "__main__": 17 | botman.run() -------------------------------------------------------------------------------- /img/christmas_hat_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexueyuan/chatbot/bbe279d65f3217a05ae0f451b83dc297c3037a71/img/christmas_hat_1.jpg -------------------------------------------------------------------------------- /img/christmas_hat_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexueyuan/chatbot/bbe279d65f3217a05ae0f451b83dc297c3037a71/img/christmas_hat_2.jpg -------------------------------------------------------------------------------- /img/christmas_hat_all_test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexueyuan/chatbot/bbe279d65f3217a05ae0f451b83dc297c3037a71/img/christmas_hat_all_test.jpg -------------------------------------------------------------------------------- /img/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexueyuan/chatbot/bbe279d65f3217a05ae0f451b83dc297c3037a71/img/demo.png -------------------------------------------------------------------------------- /img/group_chat_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexueyuan/chatbot/bbe279d65f3217a05ae0f451b83dc297c3037a71/img/group_chat_demo.png -------------------------------------------------------------------------------- /img/onechat_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexueyuan/chatbot/bbe279d65f3217a05ae0f451b83dc297c3037a71/img/onechat_demo.png -------------------------------------------------------------------------------- /img/self_demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexueyuan/chatbot/bbe279d65f3217a05ae0f451b83dc297c3037a71/img/self_demo.jpg -------------------------------------------------------------------------------- /src/chatbot.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import sys 4 | import itchat 5 | import logging 6 | import threading 7 | import collections 8 | import re 9 | 10 | if sys.version_info.major < 3: 11 | reload(sys) 12 | sys.setdefaultencoding("utf-8") 13 | 14 | context = threading.local() 15 | context.msg = None 16 | 17 | msg = context.msg 18 | 19 | class Chatbot(): 20 | nickName = "chatbot" 21 | userName = "" 22 | def __init__(self, conf=None): 23 | """ 24 | init methods. 25 | initialize listen rule, there are three element in it, `onechat`, `groupchat` and 26 | `mechat`, onechat means private chat, groupchat means a chatroom, mechat means self 27 | word content.All the rules defined will store in this dict, and in order to reduce 28 | code logic to set these three value as defaultdict. 29 | 30 | login wechat client.it set hotReload as True, so you can login without scan QR image 31 | agin and agin. 32 | 33 | get your information such as nickName and userName, nick name is different from username 34 | refer from itchat document and itchat support using username to search user information. 35 | 36 | initialize logger module.chatbot use python `logging` module to note the important data. 37 | 38 | initialize chat context.Chat context store the message object and it's relative independence 39 | in different threading. 40 | """ 41 | # listen_rule 42 | # store your listen rules 43 | # you can add new rule by using `listen` methods or `add_listen_rule` method 44 | self.listen_rule = { 45 | "onechat": collections.defaultdict(list), 46 | "groupchat": collections.defaultdict(list), 47 | "mechat": collections.defaultdict(list) 48 | } 49 | 50 | # login to wechat client 51 | if conf is not None: 52 | login_conf = conf.get('login_conf', {}) 53 | else: 54 | login_conf = {} 55 | hotReload = login_conf.get('hotReload', False) 56 | statusStorageDir = login_conf.get('statusStorageDir', 'chatbot.pkl') 57 | enableCmdQR = login_conf.get('enableCmdQR', False) 58 | picDir = login_conf.get('picDir', None) 59 | qrCallback = login_conf.get('qrCallback', None) 60 | loginCallback = login_conf.get('loginCallback', None) 61 | exitCallback = login_conf.get('exitCallback', None) 62 | itchat.auto_login( 63 | hotReload = hotReload, 64 | statusStorageDir= statusStorageDir, 65 | enableCmdQR = enableCmdQR, 66 | picDir = picDir, 67 | qrCallback = qrCallback, 68 | loginCallback = loginCallback, 69 | exitCallback = exitCallback) 70 | 71 | # initialize self information 72 | # itchat provide `search_friends` methods to search user information by user name 73 | # if no user name support it return your own infomation, it is useful so save it. 74 | me = itchat.search_friends() 75 | self.nickName = me['NickName'] 76 | self.userName = me['UserName'] 77 | 78 | # initialize logger module 79 | # it's important to log while the program is running, chatbot use logging module to 80 | # log the important data, and it send to stout device 81 | # TODO: log configurable 82 | if conf is not None: 83 | logger_conf = conf.get('logger_conf', {}) 84 | else: 85 | logger_conf = {} 86 | level = logger_conf.get('level', 'DEBUG') 87 | format = logger_conf.get('format', '%(asctime)s - %(name)s - %(levelname)s - %(message)s') 88 | name = logger_conf.get('name', __name__) 89 | path = logger_conf.get('path', None) 90 | 91 | if level.upper() == "INFO": 92 | level = logging.INFO 93 | elif level.upper() == "WARNING": 94 | level = logging.WARNING 95 | elif level.upper() == "ERROR": 96 | level = logging.ERROR 97 | elif level.upper() == "FATAL": 98 | level = logging.FATAL 99 | else: 100 | level = logging.DEBUG 101 | 102 | logging.basicConfig(level=level, format=format, filename=path) 103 | self.logger = logging.getLogger(name) 104 | 105 | def add_listen_rule(self, key_word, handler, isOne=True, isSelf=False, isGroup=False, isAt=False, nickName=None): 106 | """ 107 | add_listen_rule 108 | add a listen rule to chatbot. 109 | """ 110 | listen_rule = self.listen_rule 111 | 112 | rules_box = [] 113 | if isSelf: 114 | rules_box.append(listen_rule["mechat"]) 115 | if isGroup: 116 | rules_box.append(listen_rule["groupchat"]) 117 | if isOne: 118 | rules_box.append(listen_rule["onechat"]) 119 | 120 | for rules in rules_box: 121 | rule = { 122 | "handler": handler, 123 | "handlerName": handler.__name__, 124 | "isAt": isAt 125 | } 126 | if nickName is not None: 127 | rule['nickName'] = nickName 128 | rules[key_word].append(rule) 129 | 130 | def listen(self, key_word, isOne=False, isSelf=False, isGroup=False, isAt=False, nickName=None): 131 | """ 132 | add listen rule by decorator 133 | """ 134 | if not isOne and not isSelf and not isGroup: 135 | isOne = True 136 | def decorator(f): 137 | self.add_listen_rule(key_word, f, isOne, isSelf, isGroup, isAt, nickName) 138 | return f 139 | return decorator 140 | 141 | def get_from_username(self, msg, isGroupChat=False): 142 | """ 143 | get msg sender nickname 144 | """ 145 | if isGroupChat: 146 | return msg['ActualNickName'].encode() 147 | 148 | friend = itchat.search_friends(userName=msg["FromUserName"]) 149 | if friend is None: 150 | return "未知" 151 | else: 152 | return friend['NickName'] 153 | 154 | def get_group_selfname(self, msg): 155 | """ 156 | get your nickname in a centain group 157 | """ 158 | if msg.get('User').has_key('Self') and msg['User']['Self']['DisplayName'].encode() != '': 159 | return msg['User']['Self']['DisplayName'].encode() 160 | else: 161 | return self.nickName 162 | 163 | def _get_rules(self): 164 | """ 165 | get the rules base on context. 166 | """ 167 | global context 168 | msg = context.msg 169 | 170 | text = msg["Text"].encode() 171 | if context.isAt: 172 | prefix = '@' + self.get_group_selfname(msg) + ' ' 173 | text = text.replace(prefix, '') 174 | self.logger.debug('关键词: ({})'.format(text)) 175 | 176 | rules = [] 177 | aim_rules = None 178 | if context.fromUserNickName == self.nickName: 179 | self.logger.debug('检索个人规则词表') 180 | aim_rules = self.listen_rule['mechat'] 181 | elif context.isGroupChat: 182 | self.logger.debug('检索群聊规则词表') 183 | aim_rules = self.listen_rule["groupchat"] 184 | else: 185 | self.logger.debug('检索私聊规则词表') 186 | aim_rules = self.listen_rule["onechat"] 187 | 188 | for key, value in aim_rules.items(): 189 | key_com = re.compile(key) 190 | if sys.version_info.major < 3 and key_com.match(text): 191 | rules.extend(value) 192 | elif sys.version_info.major == 3 and key_com.match(text.decode()): 193 | rules.extend(value) 194 | return rules 195 | 196 | def _handler_one_rule(self, rule): 197 | """ 198 | running a handler rule 199 | """ 200 | self.logger.info("触发处理函数: {}".format(rule['handlerName'])) 201 | global context 202 | msg = context.msg 203 | 204 | if not context.isGroupChat: 205 | rule['isAt'] = False 206 | 207 | if rule['isAt'] == context.isAt and rule.get('nickName', context.fromUserNickName) == context.fromUserNickName: 208 | handler = rule['handler'] 209 | content = handler() 210 | 211 | if type(content) == type(str()): 212 | self.logger.debug("返回信息: {}".format(content)) 213 | msg.User.send(content) 214 | elif type(content) == type(tuple()): 215 | t, arg = content 216 | if t == "text": 217 | self.logger.debug("返回信息: {}".format(arg)) 218 | msg.User.send(arg) 219 | elif t == "image": 220 | self.logger.debug("返回图片: {}".format(arg)) 221 | msg.User.send_image(arg) 222 | else: 223 | self.logger.debug("未支持返回类型: {}".format(t)) 224 | else: 225 | self.logger.warning("处理函数返回格式错误,错误类型: {}".format(str(type(content)))) 226 | else: 227 | self.logger.info("处理函数配置项匹配失败") 228 | if rule['isAt'] != context.isAt: 229 | self.logger.debug("群聊@属性不匹配") 230 | self.logger.debug("{} != {}".format(str(rule['isAt']), str(context.isAt))) 231 | if rule.get('nickName', context.fromUserNickName) != context.fromUserNickName: 232 | self.logger.debug("对象昵称不匹配") 233 | self.logger.debug("{} != {}".format(rule.get('nickName', context.fromUserNickName), context.fromUserNickName)) 234 | 235 | def _handler_diliver(self, msg, isGroupChat): 236 | """ 237 | while msg is comming, check it and return 238 | """ 239 | global context 240 | context.msg = msg 241 | context.isGroupChat = isGroupChat 242 | context.isAt = msg.get('isAt', False) 243 | context.fromUserNickName = self.get_from_username(msg) 244 | 245 | rules = self._get_rules() 246 | 247 | self.logger.info("触发规则: {} 条".format(len(rules))) 248 | 249 | for rule in rules: 250 | self._handler_one_rule(rule) 251 | 252 | def run(self): 253 | """ 254 | run chatbot 255 | """ 256 | @itchat.msg_register(itchat.content.TEXT) 257 | def trigger_chatone(msg): 258 | fromUserName = self.get_from_username(msg) 259 | text = msg['Text'].encode() 260 | self.logger.info('(普通消息){}: {}'.format(fromUserName, text)) 261 | 262 | t = threading.Thread(target=self._handler_diliver, args=(msg, False)) 263 | t.setDaemon(True) 264 | t.start() 265 | 266 | @itchat.msg_register(itchat.content.TEXT, isGroupChat=True) 267 | def trigger_chatgroup(msg): 268 | fromUserName = self.get_from_username(msg, isGroupChat=True) 269 | text = msg['Text'].encode() 270 | self.logger.info('(群消息){}: {}'.format(fromUserName, text)) 271 | 272 | t = threading.Thread(target=self._handler_diliver, args=(msg, True)) 273 | t.setDaemon(True) 274 | t.start() 275 | 276 | itchat.run() --------------------------------------------------------------------------------