def __init__(self, qid, pwd): self.qid = qid # QQ 号 self.__pwd = pwd # QQ密码 self.nickname = None # 初始化QQ昵称 self.http_stream = HTTPStream.instance() # HTTP 流 self.msg_dispatch = MessageDispatch(self) self.aid = 1003903 # aid 固定 self.clientid = random.randrange(11111111, 99999999) # 客户端id 随机固定 self.msg_id = random.randrange(1111111, 99999999) # 消息id, 随机初始化 self.require_check = False # 是否需要验证码 self.poll_and_heart = False # 开始拉取消息和心跳 # 初始化WebQQ登录期间需要保存的数据 self.check_code = None self.skey = None self.ptwebqq = None self.check_data = None # 初始化检查时返回的数据 self.blogin_data = None # 初始化登录前返回的数据 self.friend_info = {} # 初始化好友列表 self.group_info = {} # 初始化组列表 self.group_members_info = {} # 初始化组成员列表 self.hb_time = int(time.time() * 1000) self.login_time = None # 登录的时间 self.last_group_msg_time = time.time() self.last_msg_content = None self.last_msg_numbers = 0 # 剩余位发送的消息数量
def __init__(self): my_jid = JID(USER + '/Bot') self.my_jid = my_jid settings = XMPPSettings({ "software_name": "qxbot", "software_version": __version__, "software_os": "Linux", "tls_verify_peer": False, "starttls": True, "ipv6": False, "poll_interval": 10, }) settings["password"] = PASSWORD version_provider = VersionProvider(settings) event_queue = settings["event_queue"] self.webqq = WebQQ(QQ, event_queue) self.connected = False #self.mainloop = TornadoMainLoop(settings) self.mainloop = EpollMainLoop(settings) self.client = Client(my_jid, [self, version_provider], settings, self.mainloop) self.logger = get_logger() self.msg_dispatch = MessageDispatch(self, self.webqq, BRIDGES) self.xmpp_msg_queue = Queue.Queue()
def __init__(self, qid, pwd, handler = None): self.qid = qid # QQ 号 self.__pwd = pwd # QQ密码 self.nickname = None # 初始化QQ昵称 self.http = TornadoHTTPClient() self.http.set_user_agent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/28.0.1500.71 Chrome/28.0.1500.71 Safari/537.36") self.http.debug = TRACE self.http.validate_cert = False self.http.set_global_headers({"Accept-Charset": "UTF-8,*;q=0.5"}) self.msg_dispatch = MessageDispatch(self) self.rc = random.randrange(0, 100) self.aid = 1003903 # aid 固定 self.clientid = random.randrange(11111111, 99999999) # 客户端id 随机固定 self.msg_id = random.randrange(11111111, 99999999) # 消息id, 随机初始化 self.require_check = False # 是否需要验证码 self.poll_and_heart = False # 开始拉取消息和心跳 # 初始化WebQQ登录期间需要保存的数据 self.check_code = None self.ptwebqq = None self.require_check_time = None # 需要验证码的时间 self.checkimg_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "check.jpg") self.friend_info = {} # 初始化好友列表 self.group_info = {} # 初始化组列表 self.group_sig = {} # 组签名映射, 用作发送临时消息(sess_message) self.group_members_info = {} # 初始化组成员列表 self.mark_to_uin = {} # 备注名->uin的映射 self.daid = 164 self.login_sig = None self.login_time = None # 登录的时间 self.last_msg_time = time.time() self.last_msg_content = None self.last_msg_numbers = 0 # 剩余位发送的消息数量 self.status_callback = None self.poll_stoped = True # 获取消息是否停止 self.hThread = None # 心跳线程 #self.base_header = {"Referer":"https://d.web2.qq.com/cfproxy.html?v=20110331002&callback=1"} self.base_header = {"Referer":"http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=3"}
def __init__(self): my_jid = JID(USER+'/Bot') self.my_jid = my_jid settings = XMPPSettings({ "software_name": "qxbot", "software_version": __version__, "software_os": "Linux", "tls_verify_peer": False, "starttls": True, "ipv6":False, "poll_interval": 10, }) settings["password"] = PASSWORD version_provider = VersionProvider(settings) event_queue = settings["event_queue"] self.webqq = WebQQ(QQ, event_queue) self.connected = False #self.mainloop = TornadoMainLoop(settings) self.mainloop = EpollMainLoop(settings) self.client = Client(my_jid, [self, version_provider], settings, self.mainloop) self.logger = get_logger() self.msg_dispatch = MessageDispatch(self, self.webqq, BRIDGES) self.xmpp_msg_queue = Queue.Queue()
def __init__(self, qid, pwd): self.qid = qid # QQ 号 self.__pwd = pwd # QQ密码 self.nickname = None # 初始化QQ昵称 self.http = TornadoHTTPClient() self.http.set_user_agent( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/28.0.1500.71 Chrome/28.0.1500.71 Safari/537.36" ) self.http.debug = DEBUG self.http.validate_cert = False self.http.set_global_headers({"Accept-Charset": "UTF-8,*;q=0.5"}) self.msg_dispatch = MessageDispatch(self) self.rc = random.randrange(0, 100) self.aid = 1003903 # aid 固定 self.clientid = random.randrange(11111111, 99999999) # 客户端id 随机固定 self.msg_id = random.randrange(1111111, 99999999) # 消息id, 随机初始化 self.require_check = False # 是否需要验证码 self.poll_and_heart = False # 开始拉取消息和心跳 # 初始化WebQQ登录期间需要保存的数据 self.check_code = None self.ptwebqq = None self.check_data = None # 初始化检查时返回的数据 self.blogin_data = None # 初始化登录前返回的数据 self.friend_info = {} # 初始化好友列表 self.group_info = {} # 初始化组列表 self.group_sig = {} # 组签名映射, 用作发送临时消息(sess_message) self.group_members_info = {} # 初始化组成员列表 self.hb_time = int(time.time() * 1000) self.daid = 164 self.login_sig = None self.login_time = None # 登录的时间 self.last_group_msg_time = time.time() self.last_msg_content = None self.last_msg_numbers = 0 # 剩余位发送的消息数量 self.base_header = { "Referer": "https://d.web2.qq.com/cfproxy.html?v=20110331002&callback=1" }
def __init__(self, qid, pwd, handler=None): self.qid = qid # QQ 号 self.__pwd = pwd # QQ密码 self.nickname = None # 初始化QQ昵称 self.http = TornadoHTTPClient() self.http.set_user_agent( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/28.0.1500.71 Chrome/28.0.1500.71 Safari/537.36" ) self.http.debug = TRACE self.http.validate_cert = False self.http.set_global_headers({"Accept-Charset": "UTF-8,*;q=0.5"}) self.msg_dispatch = MessageDispatch(self) self.rc = random.randrange(0, 100) self.aid = 1003903 # aid 固定 self.clientid = random.randrange(11111111, 99999999) # 客户端id 随机固定 self.msg_id = random.randrange(11111111, 99999999) # 消息id, 随机初始化 self.require_check = False # 是否需要验证码 self.poll_and_heart = False # 开始拉取消息和心跳 # 初始化WebQQ登录期间需要保存的数据 self.check_code = None self.ptwebqq = None self.require_check_time = None # 需要验证码的时间 self.checkimg_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "check.jpg") self.friend_info = {} # 初始化好友列表 self.group_info = {} # 初始化组列表 self.group_sig = {} # 组签名映射, 用作发送临时消息(sess_message) self.group_members_info = {} # 初始化组成员列表 self.mark_to_uin = {} # 备注名->uin的映射 self.daid = 164 self.login_sig = None self.login_time = None # 登录的时间 self.last_msg_time = time.time() self.last_msg_content = None self.last_msg_numbers = 0 # 剩余位发送的消息数量 self.status_callback = None self.poll_stoped = True # 获取消息是否停止 self.hThread = None # 心跳线程 # self.base_header = {"Referer":"https://d.web2.qq.com/cfproxy.html?v=20110331002&callback=1"} self.base_header = {"Referer": "http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=3"}
def __init__(self, qid, pwd): self.qid = qid # QQ 号 self.__pwd = pwd # QQ密码 self.nickname = None # 初始化QQ昵称 self.http = TornadoHTTPClient() self.http.set_user_agent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/28.0.1500.71 Chrome/28.0.1500.71 Safari/537.36") self.http.debug = TRACE self.http.validate_cert = False self.http.set_global_headers({"Accept-Charset": "UTF-8,*;q=0.5"}) self.msg_dispatch = MessageDispatch(self) self.rc = random.randrange(0, 100) self.aid = 1003903 # aid 固定 self.clientid = random.randrange(11111111, 99999999) # 客户端id 随机固定 self.msg_id = random.randrange(1111111, 99999999) # 消息id, 随机初始化 self.require_check = False # 是否需要验证码 self.poll_and_heart = False # 开始拉取消息和心跳 # 初始化WebQQ登录期间需要保存的数据 self.check_code = None self.ptwebqq = None self.check_data = None # 初始化检查时返回的数据 self.blogin_data = None # 初始化登录前返回的数据 self.friend_info = {} # 初始化好友列表 self.group_info = {} # 初始化组列表 self.group_sig = {} # 组签名映射, 用作发送临时消息(sess_message) self.group_members_info = {} # 初始化组成员列表 self.hb_time = int(time.time() * 1000) self.daid = 164 self.login_sig = None self.login_time = None # 登录的时间 self.last_msg_time = time.time() self.last_msg_content = None self.last_msg_numbers = 0 # 剩余位发送的消息数量 self.base_header = {"Referer":"https://d.web2.qq.com/cfproxy.html?v=20110331002&callback=1"} self.last_heartbeat = None
class QXBot(EventHandler, XMPPFeatureHandler): def __init__(self): my_jid = JID(USER+'/Bot') self.my_jid = my_jid settings = XMPPSettings({ "software_name": "qxbot", "software_version": __version__, "software_os": "Linux", "tls_verify_peer": False, "starttls": True, "ipv6":False, "poll_interval": 10, }) settings["password"] = PASSWORD version_provider = VersionProvider(settings) event_queue = settings["event_queue"] self.webqq = WebQQ(QQ, event_queue) self.connected = False #self.mainloop = TornadoMainLoop(settings) self.mainloop = EpollMainLoop(settings) self.client = Client(my_jid, [self, version_provider], settings, self.mainloop) self.logger = get_logger() self.msg_dispatch = MessageDispatch(self, self.webqq, BRIDGES) self.xmpp_msg_queue = Queue.Queue() def run(self, timeout = None): self.client.connect() self.client.run(timeout) def disconnect(self): self.client.disconnect() while True: try: self.run(2) except: pass else: break @presence_stanza_handler("subscribe") def handle_presence_subscribe(self, stanza): self.logger.info(u"{0} join us".format(stanza.from_jid)) return stanza.make_accept_response() @presence_stanza_handler("subscribed") def handle_presence_subscribed(self, stanza): self.logger.info(u"{0!r} accepted our subscription request" .format(stanza.from_jid)) return stanza.make_accept_response() @presence_stanza_handler("unsubscribe") def handle_presence_unsubscribe(self, stanza): self.logger.info(u"{0} canceled presence subscription" .format(stanza.from_jid)) return stanza.make_accept_response() @presence_stanza_handler("unsubscribed") def handle_presence_unsubscribed(self, stanza): self.logger.info(u"{0!r} acknowledged our subscrption cancelation" .format(stanza.from_jid)) @presence_stanza_handler(None) def handle_presence_available(self, stanza): self.logger.info(r"{0} has been online".format(stanza.from_jid)) @presence_stanza_handler("unavailable") def handle_presence_unavailable(self, stanza): self.logger.info(r"{0} has been offline".format(stanza.from_jid)) @message_stanza_handler() def handle_message(self, stanza): if self.webqq.connected: self.msg_dispatch.dispatch_xmpp(stanza) else: self.xmpp_msg_queue.put(stanza) @event_handler(DisconnectedEvent) def handle_disconnected(self, event): return QUIT @event_handler(ConnectedEvent) def handle_connected(self, event): pass @event_handler(RosterReceivedEvent) def handle_roster_received(self, event): """ 此处代表xmpp已经连接 开始连接QQ, 先将检查是否需要验证码的handler加入到mainloop """ checkhandler = CheckHandler(self.webqq) self.mainloop.add_handler(checkhandler) self.connected = True @event_handler(CheckedEvent) def handle_webqq_checked(self, event): """ 第一步已经完毕, 删除掉检查的handler, 将登录前handler加入mainloop""" bloginhandler = BeforeLoginHandler(self.webqq, password = QQ_PWD) self.mainloop.remove_handler(event.handler) self.mainloop.add_handler(bloginhandler) @event_handler(BeforeLoginEvent) def handle_webqq_blogin(self, event): """ 登录前完毕开始真正的登录 """ loginhandler = LoginHandler(self.webqq) self.mainloop.remove_handler(event.handler) self.mainloop.add_handler(loginhandler) @event_handler(WebQQLoginedEvent) def handle_webqq_logined(self, event): """ 登录后将获取群列表的handler放入mainloop """ self.mainloop.remove_handler(event.handler) self.mainloop.add_handler(GroupListHandler(self.webqq)) @event_handler(GroupListEvent) def handle_webqq_group_list(self, event): """ 获取群列表后""" self.mainloop.remove_handler(event.handler) data = event.data group_map = {} if data.get("retcode") == 0: group_list = data.get("result", {}).get("gnamelist", []) for group in group_list: gcode = group.get("code") group_map[gcode] = group self.webqq.group_map = group_map self.webqq.group_lst_updated = False # 开放添加GroupListHandler i = 1 for gcode in group_map: if i == len(group_map): self.mainloop.add_handler( GroupMembersHandler(self.webqq, gcode = gcode, done = True)) else: self.mainloop.add_handler( GroupMembersHandler(self.webqq, gcode = gcode, done = False)) i += 1 @event_handler(GroupMembersEvent) def handle_group_members(self, event): """ 获取所有群成员 """ self.mainloop.remove_handler(event.handler) members = event.data.get("result", {}).get("minfo", []) self.webqq.group_m_map[event.gcode] = {} for m in members: uin = m.get("uin") self.webqq.group_m_map[event.gcode][uin] = m cards = event.data.get("result", {}).get("cards", []) for card in cards: uin = card.get("muin") group_name = card.get("card") self.webqq.group_m_map[event.gcode][uin]["nick"] = group_name # 防止重复添加GroupListHandler if not self.webqq.group_lst_updated: self.webqq.group_lst_updated = True self.mainloop.add_handler(GroupListHandler(self.webqq, delay = 300)) @event_handler(WebQQRosterUpdatedEvent) def handle_webqq_roster(self, event): """ 群成员都获取完毕后开启,Poll获取消息和心跳 """ self.mainloop.remove_handler(event.handler) self.msg_dispatch.get_map() if not self.webqq.polled: self.webqq.polled = True self.mainloop.add_handler(PollHandler(self.webqq)) if not self.webqq.heartbeated: self.webqq.heartbeated = True hb = HeartbeatHandler(self.webqq) self.mainloop.add_handler(hb) while True: try: stanza = self.xmpp_msg_queue.get_nowait() self.msg_dispatch.dispatch_xmpp(stanza) except Queue.Empty: break self.webqq.connected = True @event_handler(WebQQHeartbeatEvent) def handle_webqq_hb(self, event): """ 心跳完毕后, 延迟60秒在此触发此事件 重复心跳 """ self.mainloop.remove_handler(event.handler) self.mainloop.add_handler(HeartbeatHandler(self.webqq, delay = 60)) @event_handler(WebQQPollEvent) def handle_webqq_poll(self, event): """ 延迟1秒重复触发此事件, 轮询获取消息 """ self.mainloop.remove_handler(event.handler) self.mainloop.add_handler(PollHandler(self.webqq)) @event_handler(WebQQMessageEvent) def handle_webqq_msg(self, event): """ 有消息到达, 处理消息 """ self.msg_dispatch.dispatch_qq(event.message) @event_handler(RetryEvent) def handle_retry(self, event): """ 有handler触发异常, 需重试 """ self.mainloop.remove_handler(event.handler) handler = event.cls(self.webqq, event.req, *event.args, **event.kwargs) self.mainloop.add_handler(handler) @event_handler(RemoveEvent) def handle_remove(self, event): """ 触发此事件, 移除handler """ self.mainloop.remove_handler(event.handler) def send_qq_group_msg(self, group_uin, content): """ 发送qq群消息 """ handler = GroupMsgHandler(self.webqq, group_uin = group_uin, content = content) self.mainloop.add_handler(handler) @property def roster(self): return self.client.roster @property def stream(self): return self.client.stream @event_handler() def handle_all(self, event): self.logger.info(u"-- {0}".format(event)) def make_message(self, to, typ, body): """ 构造消息 `to` - 接收人 JID `typ` - 消息类型 `body` - 消息主体 """ if typ not in ['normal', 'chat', 'groupchat', 'headline']: typ = 'chat' m = Message(from_jid = self.my_jid, to_jid = to, stanza_type = typ, body = body) return m def send_msg(self, to, body): if not isinstance(to, JID): to = JID(to) msg = self.make_message(to, 'chat', body) self.stream.send(msg)
class WebQQ(object): def __init__(self, qid, pwd): self.qid = qid # QQ 号 self.__pwd = pwd # QQ密码 self.nickname = None # 初始化QQ昵称 self.http = TornadoHTTPClient() self.http.set_user_agent( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/28.0.1500.71 Chrome/28.0.1500.71 Safari/537.36" ) self.http.debug = DEBUG self.http.validate_cert = False self.http.set_global_headers({"Accept-Charset": "UTF-8,*;q=0.5"}) self.msg_dispatch = MessageDispatch(self) self.rc = random.randrange(0, 100) self.aid = 1003903 # aid 固定 self.clientid = random.randrange(11111111, 99999999) # 客户端id 随机固定 self.msg_id = random.randrange(1111111, 99999999) # 消息id, 随机初始化 self.require_check = False # 是否需要验证码 self.poll_and_heart = False # 开始拉取消息和心跳 # 初始化WebQQ登录期间需要保存的数据 self.check_code = None self.ptwebqq = None self.check_data = None # 初始化检查时返回的数据 self.blogin_data = None # 初始化登录前返回的数据 self.friend_info = {} # 初始化好友列表 self.group_info = {} # 初始化组列表 self.group_sig = {} # 组签名映射, 用作发送临时消息(sess_message) self.group_members_info = {} # 初始化组成员列表 self.hb_time = int(time.time() * 1000) self.daid = 164 self.login_sig = None self.login_time = None # 登录的时间 self.last_group_msg_time = time.time() self.last_msg_content = None self.last_msg_numbers = 0 # 剩余位发送的消息数量 self.base_header = { "Referer": "https://d.web2.qq.com/cfproxy.html?v=20110331002&callback=1" } def ptuiCB(self, scode, r, url, status, msg, nickname=None): """ 模拟JS登录之前的回调, 保存昵称 """ if int(scode) == 0: logging.info("从Cookie中获取ptwebqq的值") self.ptwebqq = self.http.cookie['.qq.com']['/']['ptwebqq'].value self.logined = True elif int(scode) == 4: logging.error(msg) self.check() else: logging.error(u"server response: {0}".format(msg.decode('utf-8'))) exit(2) if nickname: self.nickname = nickname def get_group_member_nick(self, gcode, uin): return self.group_members_info.get(gcode, {}).get(uin, {}).get("nick") def get_uptime(self): MIN = 60 HOUR = 60 * MIN DAY = 24 * HOUR up_time = datetime.fromtimestamp(self.login_time).strftime("%H:%M:%S") now = time.time() sub = now - self.login_time days = int(sub / DAY) hours = int(sub / HOUR) mins = int(sub / MIN) if mins: num = mins unit = "min" if hours: num = hours unit = "hours" if hours > 1 else "hour" if days: num = days unit = "days" if days > 1 else "day" if not days and not mins and not hours: num = int(sub) unit = "sec" return "{0} up {1} {2}".format(up_time, num, unit) def get_login_sig(self): logging.info("获取 login_sig...") url = "https://ui.ptlogin2.qq.com/cgi-bin/login" params = [("daid", self.daid), ("target", "self"), ("style", 5), ("mibao_css", "m_webqq"), ("appid", self.aid), ("enable_qlogin", 0), ("no_verifyimg", 1), ("s_url", "http://web2.qq.com/loginproxy.html"), ("f_url", "loginerroralert"), ("strong_login", 1), ("login_state", 10), ("t", "20130723001")] self.http.get(url, params, callback=self._get_login_sig) self.http.get("http://web2.qq.com") def _get_login_sig(self, resp): sigs = SIG_RE.findall(resp.body) if len(sigs) == 1: self.login_sig = sigs[0] logging.info(u"获取Login Sig: {0}".format(self.login_sig)) else: logging.warn(u"没有获取到 Login Sig, 后续操作可能失败") self.login_sig = "" self.check() def check(self): """ 检查是否需要验证码 url : https://ssl.ptlogin2.qq.com/check 方法: GET 参数: { uin // qq号 appid // 程序id 固定为1003903 r // 随机数 u1 // http://web2.qq.com/loginproxy.html js_ver // 10040 js_type // 0 } 返回: ptui_checkVC('0','!PTH','\x00\x00\x00\x00\x64\x74\x8b\x05'); 第一个参数表示状态码, 0 不需要验证, 第二个为验证码, 第三个为uin """ logging.info(u"检查是否需要验证码...") #url = "https://ssl.ptlogin2.qq.com/check" url = "http://check.ptlogin2.qq.com/check" params = { "uin": self.qid, "appid": self.aid, "u1": "http://web2.qq.com/loginproxy.html", "login_sig": self.login_sig, "js_ver": 10040, "js_type": 0, "r": random.random() } headers = { "Referer": "https://ui.ptlogin2.qq.com/cgi-bin/login?daid=" "164&target=self&style=5&mibao_css=m_webqq&appid=1003903&" "enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fweb2.q" "q.com%2Floginproxy.html&f_url=loginerroralert&strong_log" "in=1&login_state=10&t=20130723001" } self.http.get(url, params, headers=headers, callback=self.handle_verify) cookie_url = "http://www.simsimi.com/talk.htm?lc=ch" cookie_params = (("lc", "ch"), ) headers = {"Referer": "http://www.simsimi.com/talk.htm"} self.http.get(cookie_url, cookie_params, headers=headers) headers = {"Referer": "http://www.simsimi.com/talk.htm?lc=ch"} self.http.get("http://www.simsimi.com/func/langInfo", cookie_params, headers=headers) def handle_pwd(self, r, vcode, huin): """ 根据检查返回结果,调用回调生成密码和保存验证码 """ pwd = md5(md5(self.__pwd).digest() + huin).hexdigest().upper() pwd = md5(pwd + vcode).hexdigest().upper() return pwd def get_check_img(self, r, vcode, uin): """ 获取验证图片 """ def callback(resp): path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "check.jpg") fp = open(path, 'wb') fp.write(resp.body) fp.close() if UPLOAD_CHECKIMG: res = upload_file("check.jpg", path) path = res.url print u"验证图片: {0}".format(path) check_code = "" while not check_code: check_code = raw_input("输入验证图片上的验证码: ") ccode = check_code.strip().lower() self.check_code = ccode pwd = self.handle_pwd(r, ccode.upper(), uin) self.before_login(pwd) url = "https://ssl.captcha.qq.com/getimage" params = [("aid", self.aid), ("r", random.random()), ("uin", self.qid)] self.http.get(url, params, callback=callback) def handle_verify(self, resp): ptui_checkVC = lambda r, v, u: (r, v, u) r, vcode, uin = eval(resp.body.strip().rstrip(";")) if int(r) == 0: logging.info("验证码检查完毕, 不需要验证码") password = self.handle_pwd(r, vcode, uin) self.before_login(password) self.check_code = vcode else: logging.warn("验证码检查完毕, 需要验证码") self.get_check_img(r, vcode, uin) self.require_check = True def before_login(self, password): """ 登录之前的操作 url: https://ssl.ptlogin2.qq.com/login 方法: GET 参数: { u // qq号码 p // 经过处理的密码 verifycode // 验证码 webqq_type // 固定为10 remember_uin // 是否记住qq号, 传1 即可 login2qq // 登录qq, 传1 aid // appid 固定为 1003903 u1 // 固定为 http://www.qq.com h // 固定为1 ptrediect // 固定为0 ptlang // 固定为2052 from_ui // 固定为 1 pttype // 固定为1 dumy // 固定为空 fp // 固定为loginerroralert ( 重要) mibao_css // 固定为 m_webqq t // 固定为1 g // 固定为 js_type // 固定为0 js_ver // 固定为10021 其他: 如果check步骤验证了需要验证码, 需加上 Referer头 值为: https://ui.ptlogin2.qq.com/cgi-bin/login?target=self&style=5&mibao_css=m_webqq&appid=1003903&enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fweb.qq.com%2Floginproxy.html&f_url=loginerroralert&strong_login=1&login_state=10&t=20130221001 接口返回: ptuiCB('0','0','http://www.qq.com','0','登录成功!', 'nickname'); 先检查是否需要验证码,不需要验证码则首先执行一次登录 然后获取Cookie里的ptwebqq保存在实例里,供后面的接口调用 """ url = "https://ssl.ptlogin2.qq.com/login" params = [("u", self.qid), ("p", password), ("verifycode", self.check_code), ("webqq_type", 10), ("remember_uin", 1), ("login2qq", 1), ("aid", self.aid), ("u1", "http://www.qq.com/loginproxy.h" "tml?login2qq=1&webqq_type=10"), ("h", 1), ("action", 4 - 5 - 8246), ("ptredirect", 0), ("ptlang", 2052), ("from_ui", 1), ("daid", self.daid), ("pttype", 1), ("dumy", ""), ("fp", "loginerroralert"), ("mibao_css", "m_webqq"), ("t", 1), ("g", 1), ("js_type", 0), ("js_ver", 10040), ("login_sig", self.login_sig)] headers = {} if self.require_check: headers.update(Referer="https://ui.ptlogin2.qq.com/cgi-" "bin/login?target=self&style=5&mibao_css=m_" "webqq&appid=1003903&enable_qlogin=0&no_ver" "ifyimg=1&s_url=http%3A%2F%2Fweb.qq.com%2Fl" "oginproxy.html&f_url=loginerroralert&stron" "g_login=1&login_state=10&t=20130221001") logging.info("检查完毕, 开始登录前准备") self.http.get(url, params, headers=headers, callback=self.login0) def login0(self, resp): logging.info("开始登录前准备...") blogin_data = resp.body.decode("utf-8").strip().rstrip(";") eval("self." + blogin_data) location1 = re.findall(r'ptuiCB\(\'0\'\,\'0\'\,\'(.*)\'\,\'0\'\,', blogin_data)[0] params = [] header = { "Referer": "https://ui.ptlogin2.qq.com/cgi-bin/login?d" "aid=164&target=self&style=5&mibao_css=m_webqq&appid=1" "003903&enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2" "F%2Fweb2.qq.com%2Floginproxy.html&f_url=loginerrorale" "rt&strong_login=1&login_state=10&t=20130723001" } self.http.get(location1, params, headers=header, callback=self.get_location1) def get_location1(self, resp): if resp.code == 302: location2 = resp.headers.get("Location") params = [] header = {} self.http.get(location2, params, headers=header, callback=self.get_location1) else: logging.info("准备完毕, 开始登录") self.login() def login(self): """ 获取登录前的数据, 并进行登录 url: http://d.web2.qq.com/channel/login2 方法: POST 参数: { r : { status // 登录后的状态 ("online") ptwebqq // 上次请求返回的cookie passwd_sig // 固定为空 clientid // 随机的clientid psessionid // 传递 null } clientid // 客户端id psessionid // 传递null } 其他: 需加上 Referer和 Origin 头: "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3" "Origin": "http://d.web2.qq.com" 返回: {u'retcode': 0, u'result': { 'status': 'online', 'index': 1075, 'psessionid': '', u'user_state': 0, u'f': 0, u'uin': 1685359365, u'cip': 3673277226, u'vfwebqq': u'', u'port': 43332}} 保存result中的psessionid和vfwebqq供后面接口调用 """ time.sleep(4) url = "http://d.web2.qq.com/channel/login2" params = [("r", '{"status":"online","ptwebqq":"%s","passwd_sig":"",' '"clientid":"%d","psessionid":null}'\ % (self.ptwebqq, self.clientid)), ("clientid", self.clientid), ("psessionid", "null") ] headers = {"Origin": "http://d.web2.qq.com"} headers.update(self.base_header) logging.info("登录准备完毕, 开始登录") self.http.post(url, params, headers=headers, callback=self.update_friend) def _hash(self): a = str(self.qid) e = self.ptwebqq l = len(e) # 将qq号码转换成整形列表 b, k, d = 0, -1, 0 for d in a: d = int(d) b += d b %= l f = 0 if b + 4 > l: g = 4 + b - l for h in range(4): f |= h < g and (ord(e[b + h]) & 255) << (3 - h) * 8 or ( ord(e[h - g]) & 255) << (3 - h) * 8 else: for h in range(4): f |= (ord(e[b + h]) & 255) << (3 - h) * 8 k ^= f c = [k >> 24 & 255, k >> 16 & 255, k >> 8 & 255, k & 255] import string k = list(string.digits) + ['A', 'B', 'C', 'D', 'E', 'F'] d = [k[b >> 4 & 15] + k[b & 15] for b in c] return ''.join(d) def update_friend(self, resp, login=True): """ 更新好友列表 URL: http://s.web2.qq.com/api/get_user_friends2 METHOD: POST PARAMS: {r:{"h":"hello", "vfwebqq":""}} HEADER: Referer:http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=1 """ data = json.loads(resp.body) if login: if data.get("retcode") != 0: logging.error("登录失败") exit(2) self.vfwebqq = data.get("result", {}).get("vfwebqq") self.psessionid = data.get("result", {}).get("psessionid") url = "http://s.web2.qq.com/api/get_user_friends2" params = [("r", json.dumps({ "h": "hello", "hash": self._hash(), "vfwebqq": self.vfwebqq }))] headers = { "Referer": "http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=1" } callback = self.update_friend if login: logging.info("登录成功") if not DEBUG: aw = "" while aw.lower() not in ["y", "yes", "n", "no"]: aw = raw_input("是否将程序至于后台[y] ") if not aw: aw = "y" if aw in ["y", "yes"]: run_daemon(self.http.post, args=(url, params), kwargs=dict(headers=headers, kwargs=dict(login=False), callback=callback)) return self.http.post(url, params, headers=headers, callback=callback, kwargs=dict(login=False)) else: logging.info("加载好友信息") lst = data.get("result", {}).get("info", []) for info in lst: uin = info.get("uin") self.friend_info[uin] = info self.update_group() self.http.post(url, params, headers=self.base_header, delay=300, callback=callback) def update_group(self, resp=None): """ 获取组列表, 并获取组成员 获取组列表: url: http://s.web2.qq.com/api/get_group_name_list_mask2 method: POST params: { r : { vfwebqq // 登录前返回的cookie值 } } """ logging.info("获取群列表") url = "http://s.web2.qq.com/api/get_group_name_list_mask2" params = [ ("r", '{"vfwebqq":"%s"}' % self.vfwebqq), ] headers = {"Origin": "http://s.web2.qq.com"} headers.update(self.base_header) self.http.post(url, params, headers=headers, callback=self.group_members) def group_members(self, resp): """ 获取群列表, 获取群列表中的成员 url: http://s.web2.qq.com/api/get_group_info_ext2 method: GET params: { gcode // 群代码 vfwebqq // 登录前的cookie值 t // int(time.time()) } headers: "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3" """ logging.info("加载组成员信息") data = json.loads(resp.body) group_list = data.get("result", {}).get("gnamelist", []) if not group_list: self.heartbeat(0) self.poll() for i, group in enumerate(group_list): gcode = group.get("code") url = "http://s.web2.qq.com/api/get_group_info_ext2" params = [("gcode", gcode), ("vfwebqq", self.vfwebqq), ("t", int(time.time()))] callback = self.do_group_members if i == len(group_list) - 1: kwargs = dict(gcode=gcode, last=True) else: kwargs = dict(gcode=gcode) self.http.get(url, params, headers=self.base_header, callback=callback, kwargs=kwargs) self.group_info[gcode] = group def do_group_members(self, resp, gcode, last=False): """ 获取群成员数据 """ data = json.loads(resp.body) members = data.get("result", {}).get("minfo", []) self.group_members_info[gcode] = {} for m in members: uin = m.get("uin") self.group_members_info[gcode][uin] = m cards = data.get("result", {}).get("cards", []) for card in cards: uin = card.get("muin") group_name = card.get("card") self.group_members_info[gcode][uin]["nick"] = group_name if last and not self.poll_and_heart: logging.info("万事具备,开始拉取信息和心跳") self.poll() self.heartbeat(0) def poll(self): """ 建立长连接获取消息 url:http://d.web2.qq.com/channel/poll2 方法: POST 参数: { r:{ clientid // 客户端id psessionid // session id key // 固定为0 ids // 固定为 [] } clientid psessionid } 头部: "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=2" """ if not self.poll_and_heart: self.poll_and_heart = True url = "http://d.web2.qq.com/channel/poll2" rdic = { "clientid": self.clientid, "psessionid": self.psessionid, "key": 0, "ids": [] } params = [("r", json.dumps(rdic)), ("clientid", self.clientid), ("psessionid", self.psessionid)] headers = { "Referer": "https://d.web2.qq.com/cfproxy.html?v=20110331002&callback=1", "Origin": "http://d.web2.qq.com" } self.http.post(url, params, headers=headers, request_timeout=60.0, connect_timeout=60.0, callback=self.handle_msg) def handle_msg(self, resp): """ 处理消息 """ self.poll() data = resp.body try: msg = json.loads(data) if msg.get("retcode") in [121, 103]: logging.error(u"登录失败") return logging.info(u"获取消息: {0!r}".format(msg)) self.msg_dispatch.dispatch(msg) except ValueError: traceback.print_exc() logging.error(u"消息加载失败: %s", data) def heartbeat(self, delay=60): """ 开始心跳 url:http://web.qq.com/web2/get_msg_tip 方法: GET 参数: { uin // 固定为空 tp // 固定为1 rc // 固定为1 id // 固定位0 lv // 固定为2 t // 开始的心跳时间(int(time.time()) * 1000) } """ logging.info("心跳..") self.login_time = time.time() if not self.poll_and_heart: self.poll_and_heart = True url = "http://web.qq.com/web2/get_msg_tip" params = [("uin", ""), ("tp", 1), ("id", 0), ("retype", 1), ("rc", self.rc), ("lv", 3), ("t", int(self.hb_time * 1000))] self.rc += 1 self.http.get(url, params, callback=self.hb_next, delay=delay) def hb_next(self, resp): """ 持续心跳 """ self.heartbeat() def make_msg_content(self, content): """ 构造QQ消息的内容 """ self.msg_id += 1 return json.dumps([ content, [ "font", { "name": "Monospace", "size": 10, "style": [0, 0, 0], "color": "000000" } ] ]) def get_sess_group_sig(self, to_uin, callback): """ 获取临时消息组签名 URL: http://d.web2.qq.com/channel/get_c2cmsg_sig2 METHOD: GET PARAMS: id // 请求ID 固定为833193360 to_uin // 消息接受人uin( 消息的from_uin) service_type // 固定为0 clientid // 客户端id psessionid // session id t // 当前时间秒1370671760656 HEADERS: Referer:http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3 """ url = "http://d.web2.qq.com/channel/get_c2cmsg_sig2" params = (("id", 833193360), ("to_uin", to_uin), ("service_type", 0), ("clientid", self.clientid), ("psessionid", self.psessionid), ("t", time.time())) def callback(resp): data = resp.body r = json.loads(data) result = r.get("result") group_sig = result.get("value") if r.get("retcode") != 0: logging.warn(u"加载临时消息签名失败: {0}".format(group_sig)) return try: logging.info("加载临时消息签名 {0} for {1}".format(group_sig, to_uin)) except UnicodeError: return self.group_sig[to_uin] = group_sig callback() self.http.get(url, params, callback=callback, headers=self.base_header) def send_sess_msg(self, to_uin, content): """ 发送临时消息 URL:http://d.web2.qq.com/channel/send_sess_msg2 METHOD: POST PARAMS: r:{ to // 消息接收人 uin group_sig // 组签名 face // 固定为 564, content // 发送内容 msg_id // 消息id service_type // 固定为0, clientid // 客户端id psessionid // sessionid } clientid // 客户端id psessionid // sessionid Headers: self.base_header """ group_sig = self.group_sig.get(to_uin) if not group_sig: callback = partial(self.send_sess_msg, to_uin, content) return self.get_sess_group_sig(to_uin, callback) logging.info(u"发送临时消息 {0} 到 {1}".format(content, to_uin)) delay, n = self.get_delay(content) content = self.make_msg_content(content) url = "http://d.web2.qq.com/channel/send_sess_msg2" params = (("r", json.dumps({ "to": to_uin, "group_sig": group_sig, "face": 564, "content": content, "msg_id": self.msg_id, "service_type": 0, "clientid": self.clientid, "psessionid": self.psessionid })), ("clientid", self.clientid), ("psessionid", self.psessionid)) def callback(resp): self.last_msg_numbers -= n self.http.post(url, params, headers=self.base_header, callback=callback, delay=delay) def send_buddy_msg(self, to_uin, content): """ 发送好友消息 URL: http://d.web2.qq.com/channel/send_buddy_msg2 METHOD: POST PARAMS: { "r":{ "to" // 好友uin "face" // 固定为564 "content" // 发送内容 "msg_id" // 消息id, 每发一条递增 "clientid" // 客户端id "psessionid" // sessionid } "clientid":clientid, "psessionid": psessionid, } HEADERS: Referer:http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3 """ logging.info(u"发送好友消息 {0} 给 {1}".format(content, to_uin)) content = self.make_msg_content(content) url = "http://d.web2.qq.com/channel/send_buddy_msg2" r = { "to": to_uin, "face": 564, "content": content, "clientid": self.clientid, "msg_id": self.msg_id, "psessionid": self.psessionid } params = [("r", json.dumps(r)), ("clientid", self.clientid), ("psessionid", self.psessionid)] headers = {"Origin": "http://d.web2.qq.com"} headers.update(self.base_header) delay, n = self.get_delay(content) def callback(resp): self.last_msg_numbers -= n self.http.post(url, params, headers=headers, delay=delay, callback=callback) def send_group_msg(self, group_uin, content): """ 发送群消息 url:http://d.web2.qq.com/channel/send_qun_msg2 方法: POST 参数: { r:{ group_uin // gid content // 发送内容 msg_id // 消息id, 每次发送消息应该递增 clientid // 客户端id psessionid // sessionid } clientid psessionid } """ gid = self.group_info.get(group_uin, {}).get("gid") source = content content = self.make_msg_content(source) url = "http://d.web2.qq.com/channel/send_qun_msg2" r = { "group_uin": gid, "content": content, "msg_id": self.msg_id, "clientid": self.clientid, "psessionid": self.psessionid } params = [("r", json.dumps(r)), ("psessionid", self.psessionid), ("clientid", self.clientid)] delay, n = self.get_delay(content) callback = self.send_group_msg_back self.http.post(url, params, headers=self.base_header, callback=callback, args=(source, group_uin, n), delay=delay) def get_delay(self, content): MIN = MESSAGE_INTERVAL delay = 0 if time.time() - self.last_group_msg_time < MIN or\ self.last_msg_numbers > 0: delay = self.last_msg_numbers * MIN numbers = 1 if self.last_msg_content == content: delay += 0.5 self.last_msg_numbers += 1 numbers = 2 self.last_msg_numbers += 1 self.last_msg_content = content if delay: logging.info(u"有 {1} 个消息未投递将会在 {0} 秒后投递".format( delay, self.last_msg_numbers)) return delay, numbers def send_group_msg_back(self, content, group_uin, n, resp): logging.info(u"发送群消息 {0} 到 {1}".format(content, group_uin)) self.last_group_msg_time = time.time() if self.last_msg_numbers > 0: self.last_msg_numbers -= n def set_signature(self, signature, password, callback): """ 设置QQ签名, 可以通过发送好友消息设置签名, 消息应按照如下格式: 设置签名:[密码]|[签名内容] // 密码和签名内容不能包含分割符 url: http://s.web2.qq.com/api/set_long_nick2 method: POST params: r : { nlk // 签名内容 vfwebqq // 登录时获取的cookie值 } headers: Referer:http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=1 """ if password != Set_Password: return callback(u"你没有权限这么做") logging.info(u"设置QQ签名 {0}".format(signature)) url = "http://s.web2.qq.com/api/set_long_nick2" params = (("r", json.dumps({ "nlk": signature, "vfwebqq": self.vfwebqq })), ) headers = {"Origin": "http://s.web2.qq.com"} headers.update(self.base_header) def callback(resp): data = resp.body print data result = json.loads(data).get("retcode") if result == 0: callback(u"设置成功") else: callback(u"设置失败") self.http.post(url, params, headers=headers, callback=callback) def run(self): self.get_login_sig() self.http.start()
class WebQQ(object): def __init__(self, qid, pwd): self.qid = qid # QQ 号 self.__pwd = pwd # QQ密码 self.nickname = None # 初始化QQ昵称 self.http_stream = HTTPStream.instance() # HTTP 流 self.msg_dispatch = MessageDispatch(self) self.aid = 1003903 # aid 固定 self.clientid = random.randrange(11111111, 99999999) # 客户端id 随机固定 self.msg_id = random.randrange(1111111, 99999999) # 消息id, 随机初始化 self.require_check = False # 是否需要验证码 self.poll_and_heart = False # 开始拉取消息和心跳 # 初始化WebQQ登录期间需要保存的数据 self.check_code = None self.skey = None self.ptwebqq = None self.check_data = None # 初始化检查时返回的数据 self.blogin_data = None # 初始化登录前返回的数据 self.friend_info = {} # 初始化好友列表 self.group_info = {} # 初始化组列表 self.group_members_info = {} # 初始化组成员列表 self.hb_time = int(time.time() * 1000) self.login_time = None # 登录的时间 self.last_group_msg_time = time.time() self.last_msg_content = None self.last_msg_numbers = 0 # 剩余位发送的消息数量 def handle_pwd(self, password): """ 根据检查返回结果,调用回调生成密码和保存验证码 """ r, self._vcode, huin = eval("self." + self.check_data.rstrip(";")) pwd = md5(md5(password).digest() + huin).hexdigest().upper() return md5(pwd + self._vcode).hexdigest().upper() def ptui_checkVC(self, r, vcode, uin): """ 处理检查的回调 返回三个值 """ if int(r) == 0: logging.info("Has checked, don't need verification code ") self.check_code = vcode else: logging.warn("Has checked, need a verification code") self.check_code = self.get_check_img(vcode) self.require_check = True return r, self.check_code, uin def get_check_img(self, vcode): """ 获取验证图片 """ url = "https://ssl.captcha.qq.com/getimage" params = [("aid", self.aid), ("r", random.random()), ("uin", self.qid)] request = self.http_stream.make_get_request(url, params) cookie = urllib2.HTTPCookieProcessor(self.http_stream.cookiejar) opener = urllib2.build_opener(cookie) res = opener.open(request) path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "check.jpg") fp = open(path, 'wb') fp.write(res.read()) fp.close() if UPLOAD_CHECKIMG: res = upload_file("check.jpg", path) path = res.url print u"验证图片: {0}".format(path) check_code = "" while not check_code: check_code = raw_input("输入验证图片上的验证码: ") return check_code.strip().upper() def ptuiCB(self, scode, r, url, status, msg, nickname = None): """ 模拟JS登录之前的回调, 保存昵称 """ if int(scode) == 0: logging.info("Get the value of ptwebqq from cookie") self.skey = self.http_stream.cookie['.qq.com']['/']['skey'].value self.ptwebqq = self.http_stream.cookie['.qq.com']['/']['ptwebqq'].value else: logging.warn("There is no value of ptwebqq in cookie") if nickname: self.nickname = nickname def get_group_member_nick(self, gcode, uin): return self.group_members_info.get(gcode, {}).get(uin, {}).get("nick") def get_uptime(self): MIN = 60 HOUR = 60 * MIN DAY = 24 * HOUR up_time = datetime.fromtimestamp(self.login_time).strftime("%H:%M:%S") now = time.time() sub = now - self.login_time days = int(sub / DAY) hours = int(sub / HOUR) mins = int(sub / MIN) if mins: num = mins unit = "min" if hours: num = hours unit = "hours" if hours > 1 else "hour" if days: num = days unit = "days" if days > 1 else "day" if not days and not mins and not hours: num = int(sub) unit = "sec" return "{0} up {1} {2}".format(up_time, num, unit) def check(self, **kwargs): """ 检查是否需要验证码 url : http://check.ptlogin2.qq.com/check 方法: GET 参数: { uin // qq号 appid // 程序id 固定为1003903 r // 随机数 } 返回: ptui_checkVC('0','!PTH','\x00\x00\x00\x00\x64\x74\x8b\x05'); 第一个参数表示状态码, 0 不需要验证, 第二个为验证码, 第三个为uin """ logging.info("Check whether need verification code ") url = "http://check.ptlogin2.qq.com/check" params = {"uin":self.qid, "appid":self.aid, "r" : random.random()} request = self.http_stream.make_get_request(url, params) self.http_stream.add_request(request, self.before_login) # 获取SimSimi的cookie cookie_url = "http://www.simsimi.com/talk.htm?lc=ch" cookie_params = (("lc", "ch"),) req = self.http_stream.make_get_request(cookie_url, cookie_params) req.add_header("Referer", "http://www.simsimi.com/talk.htm") self.http_stream.add_request(req) req = self.http_stream.make_get_request("http://www.simsimi.com/func/langInfo", cookie_params) req.add_header("Referer", "http://www.simsimi.com/talk.htm?lc=ch") self.http_stream.add_request(req) def before_login(self, resp): """ 登录之前的操作 url: https://ssl.ptlogin2.qq.com/login 方法: GET 参数: { u // qq号码 p // 经过处理的密码 verifycode // 验证码 webqq_type // 固定为10 remember_uin // 是否记住qq号, 传1 即可 login2qq // 登录qq, 传1 aid // appid 固定为 1003903 u1 // 固定为 http://www.qq.com h // 固定为1 ptrediect // 固定为0 ptlang // 固定为2052 from_ui // 固定为 1 pttype // 固定为1 dumy // 固定为空 fp // 固定为loginerroralert ( 重要) mibao_css // 固定为 m_webqq t // 固定为1 g // 固定为 js_type // 固定为0 js_ver // 固定为10021 其他: 如果check步骤验证了需要验证码, 需加上 Referer头 值为: https://ui.ptlogin2.qq.com/cgi-bin/login?target=self&style=5&mibao_css=m_webqq&appid=1003903&enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fweb.qq.com%2Floginproxy.html&f_url=loginerroralert&strong_login=1&login_state=10&t=20130221001 接口返回: ptuiCB('0','0','http://www.qq.com','0','登录成功!', 'nickname'); 先检查是否需要验证码,不需要验证码则首先执行一次登录 然后获取Cookie里的ptwebqq,skey保存在实例里,供后面的接口调用 """ logging.info("Check is done, start to login1") self.check_data = resp.read().strip().rstrip(";") password = self.handle_pwd(self.__pwd) url = "https://ssl.ptlogin2.qq.com/login" params = [("u",self.qid), ("p",password), ("verifycode", self.check_code), ("webqq_type",10), ("remember_uin", 1),("login2qq",1), ("aid", self.aid), ("u1", "http://www.qq.com"), ("h", 1), ("ptredirect", 0), ("ptlang", 2052), ("from_ui", 1), ("pttype", 1), ("dumy", ""), ("fp", "loginerroralert"), ("mibao_css","m_webqq"), ("t",1), ("g",1), ("js_type",0), ("js_ver", 10021)] request = self.http_stream.make_get_request(url, params) if self.require_check: request.add_header("Referer", "https://ui.ptlogin2.qq.com/cgi-" "bin/login?target=self&style=5&mibao_css=m_" "webqq&appid=1003903&enable_qlogin=0&no_ver" "ifyimg=1&s_url=http%3A%2F%2Fweb.qq.com%2Fl" "oginproxy.html&f_url=loginerroralert&stron" "g_login=1&login_state=10&t=20130221001") return request, self.login def login(self, resp): """ 获取登录前的数据, 并进行登录 url: http://d.web2.qq.com/channel/login2 方法: POST 参数: { r : { status // 登录后的状态 ("online") ptwebqq // 上次请求返回的cookie passwd_sig // 固定为空 clientid // 随机的clientid psessionid // 传递 null } clientid // 客户端id psessionid // 传递null } 其他: 需加上 Referer和 Origin 头: "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3" "Origin": "http://d.web2.qq.com" 返回: {u'retcode': 0, u'result': { 'status': 'online', 'index': 1075, 'psessionid': '', u'user_state': 0, u'f': 0, u'uin': 1685359365, u'cip': 3673277226, u'vfwebqq': u'', u'port': 43332}} 保存result中的psessionid和vfwebqq供后面接口调用 """ logging.info("login1 done, start to login2") blogin_data = resp.read().decode("utf-8").strip().rstrip(";") eval("self." + blogin_data) url = "http://d.web2.qq.com/channel/login2" params = [("r", '{"status":"online","ptwebqq":"%s","passwd_sig":"",' '"clientid":"%d","psessionid":null}'\ % (self.ptwebqq, self.clientid)), ("clientid", self.clientid), ("psessionid", "null") ] request = self.http_stream.make_post_request(url, params) request.add_header("Referer", "http://d.web2.qq.com/proxy.html?" "v=20110331002&callback=1&id=3") request.add_header("Origin", "http://d.web2.qq.com") return request, self.update_friend def update_friend(self, resp, login = True): """ 更新好友列表 URL: http://s.web2.qq.com/api/get_user_friends2 METHOD: POST PARAMS: {r:{"h":"hello", "vfwebqq":""}} HEADER: Referer:http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=1 """ data = json.loads(resp.read()) if login: logging.info("WebQQ is logged in, start to update friend info") self.psessionid = data.get("result", {}).get("psessionid") self.vfwebqq = data.get("result", {}).get("vfwebqq") url = "http://s.web2.qq.com/api/get_user_friends2" params = [("r", json.dumps({"h":"hello", "vfwebqq":self.vfwebqq}))] request = self.http_stream.make_post_request(url, params) request.add_header( "Referer", "http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=1") read_back = partial(self.update_friend, login = False) return request, read_back else: logging.info("Friend info update") lst = data.get("result", {}).get("info", []) for info in lst: uin = info.get("uin") self.friend_info[uin] = info self.update_group() url = "http://s.web2.qq.com/api/get_user_friends2" params = [("r", json.dumps({"h":"hello", "vfwebqq":self.vfwebqq}))] request = self.http_stream.make_post_request(url, params) request.add_header( "Referer", "http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=1") read_back = partial(self.update_friend, login = False) return request, read_back, 300 def update_group(self, resp = None): """ 获取组列表, 并获取组成员 获取组列表: url: http://s.web2.qq.com/api/get_group_name_list_mask2 method: POST params: { r : { vfwebqq // 登录前返回的cookie值 } } """ logging.info("Fetch group list...") url = "http://s.web2.qq.com/api/get_group_name_list_mask2" params = [("r", '{"vfwebqq":"%s"}' % self.vfwebqq),] request = self.http_stream.make_post_request(url, params) request.add_header("Origin", "http://s.web2.qq.com") request.add_header("Referer", "http://s.web2.qq.com/proxy.ht" "ml?v=20110412001&callback=1&id=1") self.http_stream.add_request(request, self.group_members) def group_members(self, resp): """ 获取群列表, 获取群列表中的成员 url: http://s.web2.qq.com/api/get_group_info_ext2 method: GET params: { gcode // 群代码 vfwebqq // 登录前的cookie值 t // int(time.time()) } headers: "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3" """ logging.info("Fetch group list done") logging.info("Fetch group's members") data = json.loads(resp.read()) group_list = data.get("result", {}).get("gnamelist", []) for i, group in enumerate(group_list): gcode = group.get("code") url = "http://s.web2.qq.com/api/get_group_info_ext2" params = [("gcode", gcode),("vfwebqq", self.vfwebqq), ("t", int(time.time()))] request = self.http_stream.make_get_request(url, params) request.add_header( "Referer", "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3") read_back = self.do_group_members if i == len(group_list) -1 : read_back = partial(read_back, gcode = gcode, last = True) else: read_back = partial(read_back, gcode = gcode) self.http_stream.add_request(request, read_back) self.group_info[gcode] = group def do_group_members(self, resp, gcode, last = False): """ 获取群成员数据 """ data = json.loads(resp.read()) members = data.get("result", {}).get("minfo", []) self.group_members_info[gcode] = {} for m in members: uin = m.get("uin") self.group_members_info[gcode][uin] = m cards = data.get("result", {}).get("cards", []) for card in cards: uin = card.get("muin") group_name = card.get("card") self.group_members_info[gcode][uin]["nick"] = group_name if last and not self.poll_and_heart: logging.info("Fetch group's members done") self.poll() self.heartbeat() def poll(self): """ 建立长连接获取消息 url:http://d.web2.qq.com/channel/poll2 方法: POST 参数: { r:{ clientid // 客户端id psessionid // session id key // 固定为0 ids // 固定为 [] } clientid psessionid } 头部: "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=2" """ logging.info("Everything is ready, start to poll message") if not self.poll_and_heart: self.poll_and_heart = True url = "http://d.web2.qq.com/channel/poll2" rdic = {"clientid": self.clientid, "psessionid": self.psessionid, "key": 0, "ids":[]} params = [("r", json.dumps(rdic)), ("clientid", self.clientid), ("psessionid", self.psessionid)] request = self.http_stream.make_post_request(url, params) request.add_header( "Referer", "http://d.web2.qq.com/proxy.html?v=" "20110331002&callback=1&id=2") read_back = partial(self.handle_msg, next_req = request) self.http_stream.add_request(request, read_back) def handle_msg(self, resp, next_req): """ 处理消息 """ data = resp.read() try: msg = json.loads(data) if msg.get("retcode") == 121: self.restart() return logging.info(u"Got message {0!r}".format(msg)) self.msg_dispatch.dispatch(msg) except ValueError: traceback.print_exc() logging.error(u"Message can't loads: %s", data) return next_req, partial(self.handle_msg, next_req = next_req) def heartbeat(self): """ 开始心跳 url:http://web.qq.com/web2/get_msg_tip 方法: GET 参数: { uin // 固定为空 tp // 固定为1 rc // 固定为1 lv // 固定为2 t // 开始的心跳时间(int(time.time()) * 1000) } """ logging.info("Start heartbeat") self.login_time = time.time() if not self.poll_and_heart: self.poll_and_heart = True url = "http://web.qq.com/web2/get_msg_tip" params = [("uin", ""), ("tp", 1), ("id", 0), ("retype", 1), ("rc", 1), ("lv", 2), ("t", int(self.hb_time * 1000))] request = self.http_stream.make_get_request(url, params) read_back = partial(self.hb_next, next_req = request) self.http_stream.add_request(request, read_back) def hb_next(self, resp, next_req): """ 持续心跳 """ logging.info("Heartbeat is done, continue to wait for 60 seconds") return next_req, partial(self.hb_next, next_req = next_req), 60 def make_msg_content(self, content): """ 构造QQ消息的内容 """ return json.dumps([content, ["font", {"name":"Monospace", "size":10, "style":[0, 0, 0], "color":"000000"}]]) def send_buddy_msg(self, to_uin, content): """ 发送好友消息 URL: http://d.web2.qq.com/channel/send_buddy_msg2 METHOD: POST PARAMS: { "r":{ "to" // 好友uin "face" // 固定为564 "content" // 发送内容 "msg_id" // 消息id, 每发一条递增 "clientid" // 客户端id "psessionid" // sessionid } "clientid":clientid, "psessionid": psessionid, } HEADERS: Referer:http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3 """ logging.info(u"Send buddy message {0} to {1}".format(content, to_uin)) content = self.make_msg_content(content) url = "http://d.web2.qq.com/channel/send_buddy_msg2" r = {"to":to_uin, "face":564, "content":content, "clientid":self.clientid, "msg_id": self.msg_id, "psessionid": self.psessionid} self.msg_id += 1 params = [("r",json.dumps(r)), ("clientid",self.clientid), ("psessionid", self.psessionid)] request = self.http_stream.make_post_request(url, params) request.add_header( "Referer", "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3") request.add_header("Origin", "http://d.web2.qq.com") self.http_stream.add_request(request) def send_group_msg(self, group_uin, content): """ 发送群消息 url:http://d.web2.qq.com/channel/send_qun_msg2 方法: POST 参数: { r:{ group_uin // gid content // 发送内容 msg_id // 消息id, 每次发送消息应该递增 clientid // 客户端id psessionid // sessionid } clientid psessionid } """ gid = self.group_info.get(group_uin).get("gid") content = self.make_msg_content(content) url = "http://d.web2.qq.com/channel/send_qun_msg2" r = {"group_uin": gid, "content": content, "msg_id": self.msg_id, "clientid": self.clientid, "psessionid": self.psessionid} params = [("r", json.dumps(r)), ("psessionid", self.psessionid), ("clientid", self.clientid)] self.msg_id += 1 request = self.http_stream.make_post_request(url, params) request.add_header("Referer", "http://d.web2.qq.com/proxy.html" "?v=20110331002&callback=1&id=3") callback = partial(self.send_group_msg_back, content, group_uin) delay = 0 if time.time() - self.last_group_msg_time < 0.5 or\ self.last_msg_numbers > 0: delay = self.last_msg_numbers * 0.5 if self.last_msg_content == content: delay += 0.5 self.last_msg_numbers += 1 self.last_msg_numbers += 1 self.last_msg_content = content self.http_stream.add_delay_request(request, callback, delay) def send_group_msg_back(self, content, group_uin, resp): logging.info(u"Send group message {0} to {1}".format(content, group_uin)) self.last_group_msg_time = time.time() if self.last_msg_numbers > 0: self.last_msg_numbers -= 1 def run(self): self.check() self.http_stream.start() def stop(self): self.http_stream.ioloop.stop() def restart(self): logging.warn("Restart webqq") self.stop() self.run()
class WebQQ(object): def __init__(self, qid, pwd): self.qid = qid # QQ 号 self.__pwd = pwd # QQ密码 self.nickname = None # 初始化QQ昵称 self.http_stream = HTTPStream.instance() # HTTP 流 self.msg_dispatch = MessageDispatch(self) self.aid = 1003903 # aid 固定 self.clientid = random.randrange(11111111, 99999999) # 客户端id 随机固定 self.msg_id = random.randrange(1111111, 99999999) # 消息id, 随机初始化 self.require_check = False # 是否需要验证码 self.poll_and_heart = False # 开始拉取消息和心跳 # 初始化WebQQ登录期间需要保存的数据 self.check_code = None self.skey = None self.ptwebqq = None self.check_data = None # 初始化检查时返回的数据 self.blogin_data = None # 初始化登录前返回的数据 self.friend_info = {} # 初始化好友列表 self.group_info = {} # 初始化组列表 self.group_members_info = {} # 初始化组成员列表 self.hb_time = int(time.time() * 1000) self.login_time = None # 登录的时间 self.last_group_msg_time = time.time() self.last_msg_content = None self.last_msg_numbers = 0 # 剩余位发送的消息数量 def handle_pwd(self, password): """ 根据检查返回结果,调用回调生成密码和保存验证码 """ r, self._vcode, huin = eval("self." + self.check_data.rstrip(";")) pwd = md5(md5(password).digest() + huin).hexdigest().upper() return md5(pwd + self._vcode).hexdigest().upper() def ptui_checkVC(self, r, vcode, uin): """ 处理检查的回调 返回三个值 """ if int(r) == 0: logging.info("Has checked, don't need verification code ") self.check_code = vcode else: logging.warn("Has checked, need a verification code") self.check_code = self.get_check_img(vcode) self.require_check = True return r, self.check_code, uin def get_check_img(self, vcode): """ 获取验证图片 """ url = "https://ssl.captcha.qq.com/getimage" params = [("aid", self.aid), ("r", random.random()), ("uin", self.qid)] request = self.http_stream.make_get_request(url, params) cookie = urllib2.HTTPCookieProcessor(self.http_stream.cookiejar) opener = urllib2.build_opener(cookie) res = opener.open(request) path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "check.jpg") fp = open(path, 'wb') fp.write(res.read()) fp.close() if UPLOAD_CHECKIMG: res = upload_file("check.jpg", path) path = res.url print u"验证图片: {0}".format(path) check_code = "" while not check_code: check_code = raw_input("输入验证图片上的验证码: ") return check_code.strip().upper() def ptuiCB(self, scode, r, url, status, msg, nickname=None): """ 模拟JS登录之前的回调, 保存昵称 """ if int(scode) == 0: logging.info("Get the value of ptwebqq from cookie") self.skey = self.http_stream.cookie['.qq.com']['/']['skey'].value self.ptwebqq = self.http_stream.cookie['.qq.com']['/'][ 'ptwebqq'].value else: logging.warn("There is no value of ptwebqq in cookie") if nickname: self.nickname = nickname def get_group_member_nick(self, gcode, uin): return self.group_members_info.get(gcode, {}).get(uin, {}).get("nick") def get_uptime(self): MIN = 60 HOUR = 60 * MIN DAY = 24 * HOUR up_time = datetime.fromtimestamp(self.login_time).strftime("%H:%M:%S") now = time.time() sub = now - self.login_time days = int(sub / DAY) hours = int(sub / HOUR) mins = int(sub / MIN) if mins: num = mins unit = "min" if hours: num = hours unit = "hours" if hours > 1 else "hour" if days: num = days unit = "days" if days > 1 else "day" if not days and not mins and not hours: num = int(sub) unit = "sec" return "{0} up {1} {2}".format(up_time, num, unit) def check(self, **kwargs): """ 检查是否需要验证码 url : http://check.ptlogin2.qq.com/check 方法: GET 参数: { uin // qq号 appid // 程序id 固定为1003903 r // 随机数 } 返回: ptui_checkVC('0','!PTH','\x00\x00\x00\x00\x64\x74\x8b\x05'); 第一个参数表示状态码, 0 不需要验证, 第二个为验证码, 第三个为uin """ logging.info("Check whether need verification code ") url = "http://check.ptlogin2.qq.com/check" params = {"uin": self.qid, "appid": self.aid, "r": random.random()} request = self.http_stream.make_get_request(url, params) self.http_stream.add_request(request, self.before_login) # 获取SimSimi的cookie cookie_url = "http://www.simsimi.com/talk.htm?lc=ch" cookie_params = (("lc", "ch"), ) req = self.http_stream.make_get_request(cookie_url, cookie_params) req.add_header("Referer", "http://www.simsimi.com/talk.htm") self.http_stream.add_request(req) req = self.http_stream.make_get_request( "http://www.simsimi.com/func/langInfo", cookie_params) req.add_header("Referer", "http://www.simsimi.com/talk.htm?lc=ch") self.http_stream.add_request(req) def before_login(self, resp): """ 登录之前的操作 url: https://ssl.ptlogin2.qq.com/login 方法: GET 参数: { u // qq号码 p // 经过处理的密码 verifycode // 验证码 webqq_type // 固定为10 remember_uin // 是否记住qq号, 传1 即可 login2qq // 登录qq, 传1 aid // appid 固定为 1003903 u1 // 固定为 http://www.qq.com h // 固定为1 ptrediect // 固定为0 ptlang // 固定为2052 from_ui // 固定为 1 pttype // 固定为1 dumy // 固定为空 fp // 固定为loginerroralert ( 重要) mibao_css // 固定为 m_webqq t // 固定为1 g // 固定为 js_type // 固定为0 js_ver // 固定为10021 其他: 如果check步骤验证了需要验证码, 需加上 Referer头 值为: https://ui.ptlogin2.qq.com/cgi-bin/login?target=self&style=5&mibao_css=m_webqq&appid=1003903&enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fweb.qq.com%2Floginproxy.html&f_url=loginerroralert&strong_login=1&login_state=10&t=20130221001 接口返回: ptuiCB('0','0','http://www.qq.com','0','登录成功!', 'nickname'); 先检查是否需要验证码,不需要验证码则首先执行一次登录 然后获取Cookie里的ptwebqq,skey保存在实例里,供后面的接口调用 """ logging.info("Check is done, start to login1") self.check_data = resp.read().strip().rstrip(";") password = self.handle_pwd(self.__pwd) url = "https://ssl.ptlogin2.qq.com/login" params = [("u", self.qid), ("p", password), ("verifycode", self.check_code), ("webqq_type", 10), ("remember_uin", 1), ("login2qq", 1), ("aid", self.aid), ("u1", "http://www.qq.com"), ("h", 1), ("ptredirect", 0), ("ptlang", 2052), ("from_ui", 1), ("pttype", 1), ("dumy", ""), ("fp", "loginerroralert"), ("mibao_css", "m_webqq"), ("t", 1), ("g", 1), ("js_type", 0), ("js_ver", 10021)] request = self.http_stream.make_get_request(url, params) if self.require_check: request.add_header( "Referer", "https://ui.ptlogin2.qq.com/cgi-" "bin/login?target=self&style=5&mibao_css=m_" "webqq&appid=1003903&enable_qlogin=0&no_ver" "ifyimg=1&s_url=http%3A%2F%2Fweb.qq.com%2Fl" "oginproxy.html&f_url=loginerroralert&stron" "g_login=1&login_state=10&t=20130221001") return request, self.login def login(self, resp): """ 获取登录前的数据, 并进行登录 url: http://d.web2.qq.com/channel/login2 方法: POST 参数: { r : { status // 登录后的状态 ("online") ptwebqq // 上次请求返回的cookie passwd_sig // 固定为空 clientid // 随机的clientid psessionid // 传递 null } clientid // 客户端id psessionid // 传递null } 其他: 需加上 Referer和 Origin 头: "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3" "Origin": "http://d.web2.qq.com" 返回: {u'retcode': 0, u'result': { 'status': 'online', 'index': 1075, 'psessionid': '', u'user_state': 0, u'f': 0, u'uin': 1685359365, u'cip': 3673277226, u'vfwebqq': u'', u'port': 43332}} 保存result中的psessionid和vfwebqq供后面接口调用 """ logging.info("login1 done, start to login2") blogin_data = resp.read().decode("utf-8").strip().rstrip(";") eval("self." + blogin_data) url = "http://d.web2.qq.com/channel/login2" params = [("r", '{"status":"online","ptwebqq":"%s","passwd_sig":"",' '"clientid":"%d","psessionid":null}'\ % (self.ptwebqq, self.clientid)), ("clientid", self.clientid), ("psessionid", "null") ] request = self.http_stream.make_post_request(url, params) request.add_header( "Referer", "http://d.web2.qq.com/proxy.html?" "v=20110331002&callback=1&id=3") request.add_header("Origin", "http://d.web2.qq.com") return request, self.update_friend def update_friend(self, resp, login=True): """ 更新好友列表 URL: http://s.web2.qq.com/api/get_user_friends2 METHOD: POST PARAMS: {r:{"h":"hello", "vfwebqq":""}} HEADER: Referer:http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=1 """ data = json.loads(resp.read()) if login: logging.info("WebQQ is logged in, start to update friend info") self.psessionid = data.get("result", {}).get("psessionid") self.vfwebqq = data.get("result", {}).get("vfwebqq") url = "http://s.web2.qq.com/api/get_user_friends2" params = [("r", json.dumps({ "h": "hello", "vfwebqq": self.vfwebqq }))] request = self.http_stream.make_post_request(url, params) request.add_header( "Referer", "http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=1" ) read_back = partial(self.update_friend, login=False) return request, read_back else: logging.info("Friend info update") lst = data.get("result", {}).get("info", []) for info in lst: uin = info.get("uin") self.friend_info[uin] = info self.update_group() url = "http://s.web2.qq.com/api/get_user_friends2" params = [("r", json.dumps({ "h": "hello", "vfwebqq": self.vfwebqq }))] request = self.http_stream.make_post_request(url, params) request.add_header( "Referer", "http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=1" ) read_back = partial(self.update_friend, login=False) return request, read_back, 300 def update_group(self, resp=None): """ 获取组列表, 并获取组成员 获取组列表: url: http://s.web2.qq.com/api/get_group_name_list_mask2 method: POST params: { r : { vfwebqq // 登录前返回的cookie值 } } """ logging.info("Fetch group list...") url = "http://s.web2.qq.com/api/get_group_name_list_mask2" params = [ ("r", '{"vfwebqq":"%s"}' % self.vfwebqq), ] request = self.http_stream.make_post_request(url, params) request.add_header("Origin", "http://s.web2.qq.com") request.add_header( "Referer", "http://s.web2.qq.com/proxy.ht" "ml?v=20110412001&callback=1&id=1") self.http_stream.add_request(request, self.group_members) def group_members(self, resp): """ 获取群列表, 获取群列表中的成员 url: http://s.web2.qq.com/api/get_group_info_ext2 method: GET params: { gcode // 群代码 vfwebqq // 登录前的cookie值 t // int(time.time()) } headers: "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3" """ logging.info("Fetch group list done") logging.info("Fetch group's members") data = json.loads(resp.read()) group_list = data.get("result", {}).get("gnamelist", []) for i, group in enumerate(group_list): gcode = group.get("code") url = "http://s.web2.qq.com/api/get_group_info_ext2" params = [("gcode", gcode), ("vfwebqq", self.vfwebqq), ("t", int(time.time()))] request = self.http_stream.make_get_request(url, params) request.add_header( "Referer", "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3" ) read_back = self.do_group_members if i == len(group_list) - 1: read_back = partial(read_back, gcode=gcode, last=True) else: read_back = partial(read_back, gcode=gcode) self.http_stream.add_request(request, read_back) self.group_info[gcode] = group def do_group_members(self, resp, gcode, last=False): """ 获取群成员数据 """ data = json.loads(resp.read()) members = data.get("result", {}).get("minfo", []) self.group_members_info[gcode] = {} for m in members: uin = m.get("uin") self.group_members_info[gcode][uin] = m cards = data.get("result", {}).get("cards", []) for card in cards: uin = card.get("muin") group_name = card.get("card") self.group_members_info[gcode][uin]["nick"] = group_name if last and not self.poll_and_heart: logging.info("Fetch group's members done") self.poll() self.heartbeat() def poll(self): """ 建立长连接获取消息 url:http://d.web2.qq.com/channel/poll2 方法: POST 参数: { r:{ clientid // 客户端id psessionid // session id key // 固定为0 ids // 固定为 [] } clientid psessionid } 头部: "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=2" """ logging.info("Everything is ready, start to poll message") if not self.poll_and_heart: self.poll_and_heart = True url = "http://d.web2.qq.com/channel/poll2" rdic = { "clientid": self.clientid, "psessionid": self.psessionid, "key": 0, "ids": [] } params = [("r", json.dumps(rdic)), ("clientid", self.clientid), ("psessionid", self.psessionid)] request = self.http_stream.make_post_request(url, params) request.add_header( "Referer", "http://d.web2.qq.com/proxy.html?v=" "20110331002&callback=1&id=2") read_back = partial(self.handle_msg, next_req=request) self.http_stream.add_request(request, read_back) def handle_msg(self, resp, next_req): """ 处理消息 """ data = resp.read() try: msg = json.loads(data) if msg.get("retcode") == 121: self.restart() return logging.info(u"Got message {0!r}".format(msg)) self.msg_dispatch.dispatch(msg) except ValueError: traceback.print_exc() logging.error(u"Message can't loads: %s", data) return next_req, partial(self.handle_msg, next_req=next_req) def heartbeat(self): """ 开始心跳 url:http://web.qq.com/web2/get_msg_tip 方法: GET 参数: { uin // 固定为空 tp // 固定为1 rc // 固定为1 lv // 固定为2 t // 开始的心跳时间(int(time.time()) * 1000) } """ logging.info("Start heartbeat") self.login_time = time.time() if not self.poll_and_heart: self.poll_and_heart = True url = "http://web.qq.com/web2/get_msg_tip" params = [("uin", ""), ("tp", 1), ("id", 0), ("retype", 1), ("rc", 1), ("lv", 2), ("t", int(self.hb_time * 1000))] request = self.http_stream.make_get_request(url, params) read_back = partial(self.hb_next, next_req=request) self.http_stream.add_request(request, read_back) def hb_next(self, resp, next_req): """ 持续心跳 """ logging.info("Heartbeat is done, continue to wait for 60 seconds") return next_req, partial(self.hb_next, next_req=next_req), 60 def make_msg_content(self, content): """ 构造QQ消息的内容 """ return json.dumps([ content, [ "font", { "name": "Monospace", "size": 10, "style": [0, 0, 0], "color": "000000" } ] ]) def send_buddy_msg(self, to_uin, content): """ 发送好友消息 URL: http://d.web2.qq.com/channel/send_buddy_msg2 METHOD: POST PARAMS: { "r":{ "to" // 好友uin "face" // 固定为564 "content" // 发送内容 "msg_id" // 消息id, 每发一条递增 "clientid" // 客户端id "psessionid" // sessionid } "clientid":clientid, "psessionid": psessionid, } HEADERS: Referer:http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3 """ logging.info(u"Send buddy message {0} to {1}".format(content, to_uin)) content = self.make_msg_content(content) url = "http://d.web2.qq.com/channel/send_buddy_msg2" r = { "to": to_uin, "face": 564, "content": content, "clientid": self.clientid, "msg_id": self.msg_id, "psessionid": self.psessionid } self.msg_id += 1 params = [("r", json.dumps(r)), ("clientid", self.clientid), ("psessionid", self.psessionid)] request = self.http_stream.make_post_request(url, params) request.add_header( "Referer", "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3") request.add_header("Origin", "http://d.web2.qq.com") self.http_stream.add_request(request) def send_group_msg(self, group_uin, content): """ 发送群消息 url:http://d.web2.qq.com/channel/send_qun_msg2 方法: POST 参数: { r:{ group_uin // gid content // 发送内容 msg_id // 消息id, 每次发送消息应该递增 clientid // 客户端id psessionid // sessionid } clientid psessionid } """ gid = self.group_info.get(group_uin).get("gid") content = self.make_msg_content(content) url = "http://d.web2.qq.com/channel/send_qun_msg2" r = { "group_uin": gid, "content": content, "msg_id": self.msg_id, "clientid": self.clientid, "psessionid": self.psessionid } params = [("r", json.dumps(r)), ("psessionid", self.psessionid), ("clientid", self.clientid)] self.msg_id += 1 request = self.http_stream.make_post_request(url, params) request.add_header( "Referer", "http://d.web2.qq.com/proxy.html" "?v=20110331002&callback=1&id=3") callback = partial(self.send_group_msg_back, content, group_uin) delay = 0 if time.time() - self.last_group_msg_time < 0.5 or\ self.last_msg_numbers > 0: delay = self.last_msg_numbers * 0.5 if self.last_msg_content == content: delay += 0.5 self.last_msg_numbers += 1 self.last_msg_numbers += 1 self.last_msg_content = content self.http_stream.add_delay_request(request, callback, delay) def send_group_msg_back(self, content, group_uin, resp): logging.info(u"Send group message {0} to {1}".format( content, group_uin)) self.last_group_msg_time = time.time() if self.last_msg_numbers > 0: self.last_msg_numbers -= 1 def run(self): self.check() self.http_stream.start() def stop(self): self.http_stream.ioloop.stop() def restart(self): logging.warn("Restart webqq") self.stop() self.run()
class WebQQ(object): def __init__(self, qid, pwd, handler = None): self.qid = qid # QQ 号 self.__pwd = pwd # QQ密码 self.nickname = None # 初始化QQ昵称 self.http = TornadoHTTPClient() self.http.set_user_agent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/28.0.1500.71 Chrome/28.0.1500.71 Safari/537.36") self.http.debug = TRACE self.http.validate_cert = False self.http.set_global_headers({"Accept-Charset": "UTF-8,*;q=0.5"}) self.msg_dispatch = MessageDispatch(self) self.rc = random.randrange(0, 100) self.aid = 1003903 # aid 固定 self.clientid = random.randrange(11111111, 99999999) # 客户端id 随机固定 self.msg_id = random.randrange(11111111, 99999999) # 消息id, 随机初始化 self.require_check = False # 是否需要验证码 self.poll_and_heart = False # 开始拉取消息和心跳 # 初始化WebQQ登录期间需要保存的数据 self.check_code = None self.ptwebqq = None self.require_check_time = None # 需要验证码的时间 self.checkimg_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "check.jpg") self.friend_info = {} # 初始化好友列表 self.group_info = {} # 初始化组列表 self.group_sig = {} # 组签名映射, 用作发送临时消息(sess_message) self.group_members_info = {} # 初始化组成员列表 self.mark_to_uin = {} # 备注名->uin的映射 self.daid = 164 self.login_sig = None self.login_time = None # 登录的时间 self.last_msg_time = time.time() self.last_msg_content = None self.last_msg_numbers = 0 # 剩余位发送的消息数量 self.status_callback = None self.poll_stoped = True # 获取消息是否停止 self.hThread = None # 心跳线程 #self.base_header = {"Referer":"https://d.web2.qq.com/cfproxy.html?v=20110331002&callback=1"} self.base_header = {"Referer":"http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=3"} def ptuiCB(self, scode, r, url, status, msg, nickname = None): """ 模拟JS登录之前的回调, 保存昵称 """ if int(scode) == 0: logging.info("从Cookie中获取ptwebqq的值") old_value = self.ptwebqq try: self.ptwebqq = self.http.cookie['.qq.com']['/']['ptwebqq'].value except: logging.error("从Cookie中获取ptwebqq的值失败, 使用旧值尝试") self.ptwebqq = old_value self.logined = True elif int(scode) == 4: logging.error(msg) if self.status_callback: self.status_callback(False, msg) self.check() else: logging.error(u"server response: {0}".format(msg.decode('utf-8'))) self.check() if nickname: self.nickname = nickname.decode('utf-8') def get_group_member_nick(self, gcode, uin): return self.group_members_info.get(gcode, {}).get(uin, {}).get("nick") def get_uptime(self): MIN = 60 HOUR = 60 * MIN DAY = 24 * HOUR if not isinstance(self.login_time, (int, float)): self.login_time = time.time() up_time = datetime.fromtimestamp(self.login_time).strftime("%H:%M:%S") now = time.time() sub = now - self.login_time days = int(sub / DAY) hours = int(sub / HOUR) mins = int(sub / MIN) if mins: num = mins unit = "min" if hours: num = hours unit = "hours" if hours > 1 else "hour" if days: num = days unit = "days" if days > 1 else "day" if not days and not mins and not hours: num = int(sub) unit = "sec" return "{0} up {1} {2}".format(up_time, num, unit) def get_login_sig(self, handler = None): self.handler = handler # 启用HTTP_CHECKIMG的Handler self.stop_poll_heartbeat = False with open("wait", 'w'): pass logging.info("获取 login_sig...") url = "https://ui.ptlogin2.qq.com/cgi-bin/login" params = [("daid", self.daid), ("target", "self"), ("style", 5), ("mibao_css", "m_webqq"), ("appid", self.aid), ("enable_qlogin", 0), ("no_verifyimg", 1), ("s_url", "http://web2.qq.com/loginproxy.html"), ("f_url", "loginerroralert"), ("strong_login", 1), ("login_state", 10), ("t", "20130723001")] self.http.get(url, params, callback = self._get_login_sig) self.http.get("http://web2.qq.com") def _get_login_sig(self, resp): sigs = SIG_RE.findall(resp.body) if len(sigs) == 1: self.login_sig = sigs[0] logging.info(u"获取Login Sig: {0}".format(self.login_sig)) else: logging.warn(u"没有获取到 Login Sig, 后续操作可能失败") self.login_sig = "" self.check() def clean(self): if os.path.exists("lock"): os.remove("lock") if os.path.exists("wait"): os.remove("wait") def check(self): """ 检查是否需要验证码 url : https://ssl.ptlogin2.qq.com/check 方法: GET 参数: { uin // qq号 appid // 程序id 固定为1003903 r // 随机数 u1 // http://web2.qq.com/loginproxy.html js_ver // 10040 js_type // 0 } 返回: ptui_checkVC('0','!PTH','\x00\x00\x00\x00\x64\x74\x8b\x05'); 第一个参数表示状态码, 0 不需要验证, 第二个为验证码, 第三个为uin """ self.clean() self.poll_stoped = True # 检查时停止轮询消息 logging.info(u"检查是否需要验证码...") #url = "https://ssl.ptlogin2.qq.com/check" url = "http://check.ptlogin2.qq.com/check" params = {"uin":self.qid, "appid":self.aid, "u1": "http://web2.qq.com/loginproxy.html", "login_sig":self.login_sig, "js_ver":10040, "js_type":0, "r" : random.random()} headers = {"Referer":"https://ui.ptlogin2.qq.com/cgi-bin/login?daid=" "164&target=self&style=5&mibao_css=m_webqq&appid=1003903&" "enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fweb2.q" "q.com%2Floginproxy.html&f_url=loginerroralert&strong_log" "in=1&login_state=10&t=20130723001"} self.http.get(url, params, headers = headers, callback = self.handle_verify) def handle_pwd(self, r, vcode, huin): """ 根据检查返回结果,调用回调生成密码和保存验证码 """ pwd = md5(md5(self.__pwd).digest() + huin).hexdigest().upper() pwd = md5(pwd + vcode).hexdigest().upper() return pwd def get_check_img(self, r, vcode, uin): """ 获取验证图片 """ def callback(resp): path = self.checkimg_path fp = open(path, 'wb') fp.write(resp.body) fp.close() if UPLOAD_CHECKIMG: logging.info(u"正在上传验证码..") res = upload_file("check.jpg", path) path = res.read() check_code = "" if not HTTP_CHECKIMG: print u"验证图片: {0}".format(path) while not check_code: check_code = raw_input("输入验证图片上的验证码: ") ccode = check_code.strip().lower() self.check_code = ccode pwd = self.handle_pwd(r, ccode.upper(), uin) self.before_login(pwd) else: if os.path.exists("wait"): os.remove("wait") logging.info(u"请打开http://{0}:{1} 提交验证码" .format(HTTP_LISTEN, HTTP_PORT)) self.handler.r = r self.handler.uin = uin self.handler.next_callback = self.before_login self.require_check_time = time.time() if EMAIL_NOTICE: if send_notice_email(): logging.info(u"已成功发送邮件提醒") else: logging.warn(u"发送邮件提醒失败") url = "https://ssl.captcha.qq.com/getimage" params = [("aid", self.aid), ("r", random.random()), ("uin", self.qid)] self.http.get(url, params, callback = callback) def handle_verify(self, resp): ptui_checkVC = lambda r, v, u: (r, v, u) data = resp.body if not data: self.check() # 没有数据重新检查 return r, vcode, uin = eval(data.strip().rstrip(";")) logging.info("R:{0} vcode:{1} uin:{2}".format(r, vcode, uin)) if int(r) == 0: logging.info("验证码检查完毕, 不需要验证码") password = self.handle_pwd(r, vcode, uin) self.check_code = vcode self.clean() self.before_login(password) else: logging.warn("验证码检查完毕, 需要验证码") self.get_check_img(r, vcode, uin) self.require_check = True def before_login(self, password, callback = None): """ 登录之前的操作 url: https://ssl.ptlogin2.qq.com/login 方法: GET 参数: { u // qq号码 p // 经过处理的密码 verifycode // 验证码 webqq_type // 固定为10 remember_uin // 是否记住qq号, 传1 即可 login2qq // 登录qq, 传1 aid // appid 固定为 1003903 u1 // 固定为 http://www.qq.com h // 固定为1 ptrediect // 固定为0 ptlang // 固定为2052 from_ui // 固定为 1 pttype // 固定为1 dumy // 固定为空 fp // 固定为loginerroralert ( 重要) mibao_css // 固定为 m_webqq t // 固定为1 g // 固定为 js_type // 固定为0 js_ver // 固定为10021 其他: 如果check步骤验证了需要验证码, 需加上 Referer头 值为: https://ui.ptlogin2.qq.com/cgi-bin/login?target=self&style=5&mibao_css=m_webqq&appid=1003903&enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fweb.qq.com%2Floginproxy.html&f_url=loginerroralert&strong_login=1&login_state=10&t=20130221001 接口返回: ptuiCB('0','0','http://www.qq.com','0','登录成功!', 'nickname'); 先检查是否需要验证码,不需要验证码则首先执行一次登录 然后获取Cookie里的ptwebqq保存在实例里,供后面的接口调用 """ with open("lock", 'w'): pass if callback: self.status_callback = callback url = "https://ssl.ptlogin2.qq.com/login" params = [("u",self.qid), ("p",password), ("verifycode", self.check_code), ("webqq_type",10), ("remember_uin", 1),("login2qq",1), ("aid", self.aid), ("u1", "http://www.qq.com/loginproxy.h" "tml?login2qq=1&webqq_type=10"), ("h", 1), ("action", 4-5-8246), ("ptredirect", 0), ("ptlang", 2052), ("from_ui", 1), ("daid", self.daid), ("pttype", 1), ("dumy", ""), ("fp", "loginerroralert"), ("mibao_css","m_webqq"), ("t",1), ("g",1), ("js_type",0), ("js_ver", 10040), ("login_sig", self.login_sig)] headers = {} if self.require_check: headers.update(Referer = "https://ui.ptlogin2.qq.com/cgi-" "bin/login?target=self&style=5&mibao_css=m_" "webqq&appid=1003903&enable_qlogin=0&no_ver" "ifyimg=1&s_url=http%3A%2F%2Fweb.qq.com%2Fl" "oginproxy.html&f_url=loginerroralert&stron" "g_login=1&login_state=10&t=20130221001") else: headers.update(Referfer = "https://ui.ptlogin2.qq.com/cgi-bin/login?daid=164&target=self&style=5&mibao_css=m_webqq&appid=1003903&enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fweb2.qq.com%2Floginproxy.html&f_url=loginerroralert&strong_login=1&login_state=10&t=20130903001") logging.info("检查完毕, 开始登录前准备") self.http.get(url, params, headers = headers, callback = self.login0) def login0(self, resp): logging.info("开始登录前准备...") blogin_data = resp.body.decode("utf-8").strip().rstrip(";") eval("self." + blogin_data) try: location1 = re.findall(r'ptuiCB\(\'0\'\,\'0\'\,\'(.*)\'\,\'0\'\,', blogin_data)[0] params = [] header = {"Referer": "https://ui.ptlogin2.qq.com/cgi-bin/login?d" "aid=164&target=self&style=5&mibao_css=m_webqq&appid=1" "003903&enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2" "F%2Fweb2.qq.com%2Floginproxy.html&f_url=loginerrorale" "rt&strong_login=1&login_state=10&t=20130723001"} self.http.get(location1, params, headers = header, callback = self.get_location1) except: pass def get_location1(self, resp): if os.path.exists(self.checkimg_path): os.remove(self.checkimg_path) if os.path.exists("lock"): os.remove("lock") logging.info("准备完毕, 开始登录") self.login() def login(self): """ 获取登录前的数据, 并进行登录 url: http://d.web2.qq.com/channel/login2 方法: POST 参数: { r : { status // 登录后的状态 ("online") ptwebqq // 上次请求返回的cookie passwd_sig // 固定为空 clientid // 随机的clientid psessionid // 传递 null } clientid // 客户端id psessionid // 传递null } 其他: 需加上 Referer和 Origin 头: "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3" "Origin": "http://d.web2.qq.com" 返回: {u'retcode': 0, u'result': { 'status': 'online', 'index': 1075, 'psessionid': '', u'user_state': 0, u'f': 0, u'uin': 1685359365, u'cip': 3673277226, u'vfwebqq': u'', u'port': 43332}} 保存result中的psessionid和vfwebqq供后面接口调用 """ #time.sleep(4) url = "http://d.web2.qq.com/channel/login2" params = [("r", '{"status":"online","ptwebqq":"%s","passwd_sig":"",' '"clientid":"%d","psessionid":null}'\ % (self.ptwebqq, self.clientid)), ("clientid", self.clientid), ("psessionid", "null") ] headers = { "Origin": "http://d.web2.qq.com"} headers.update(self.base_header) logging.info("登录准备完毕, 开始登录") self.http.post(url, params, headers = headers, callback= self.login_back) def login_back(self, resp): self.require_check_time = None if not resp.body: logging.error(u"没有获取到数据, 登录失败") if self.status_callback: self.status_callback(False, "登录失败 没有数据返回") return self.check() data = json.loads(resp.body) if data.get("retcode") != 0: if self.status_callback: self.status_callback(False, "登录失败 {0}".format(data)) logging.error("登录失败 {0!r}".format(data)) return self.vfwebqq = data.get("result", {}).get("vfwebqq") self.psessionid = data.get("result", {}).get("psessionid") logging.info("登录成功") if not DEBUG and not HTTP_CHECKIMG: aw = "" while aw.lower() not in ["y", "yes", "n", "no"]: aw = raw_input("是否将程序至于后台[y] ") if not aw: aw = "y" if aw in ["y", "yes"]: run_daemon(self.update_friend) return self.update_friend() def _hash(self): """ 获取列表时的Hash """ return _hash.webqq_hash(self.qid, self.ptwebqq) # l = len(e) # # 将qq号码转换成整形列表 # b, k, d = 0, -1, 0 # for d in a: # d = int(d) # b += d # b %= l # f = 0 # if b + 4 > l: # g = 4 + b - l # for h in range(4): # f |= h < g and ( # ord(e[b + h]) & 255) << (3 - h) * 8 or ( # ord(e[h - g]) & 255) << (3 - h) * 8 # else: # for h in range(4): # f |= (ord(e[b + h]) & 255) << (3 - h) * 8 # k ^= f # c = [k >> 24 & 255, k >> 16 & 255, k >> 8 & 255, k & 255] # import string # k = list(string.digits) + ['A', 'B', 'C', 'D', 'E', 'F'] # d = [k[b >> 4 & 15] + k[b & 15] for b in c] # return ''.join(d) def update_friend(self, resp = None, call_status = True): """ 更新好友列表 URL: http://s.web2.qq.com/api/get_user_friends2 METHOD: POST PARAMS: {r:{"h":"hello", "vfwebqq":""}} HEADER: Referer:http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=1 """ url = "http://s.web2.qq.com/api/get_user_friends2" params = [("r", json.dumps({"h":"hello", "hash":self._hash(), "vfwebqq":self.vfwebqq}))] headers = {"Referer": "http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=1"} callback = self.update_friend if resp is None: self.poll_stoped = False # 可以开始轮询消息 self.http.post(url, params, headers = headers, callback = callback) else: if not resp.body: if self.status_callback and call_status: self.status_callback(False, u"更新好友信息失败") return data = json.loads(resp.body) if data.get("retcode") != 0 and call_status: self.status_callback(False, u"好友列表加载失败, 错误代码:{0}" .format(data.get("retcode"))) return lst = data.get("result", {}).get("info", []) for info in lst: uin = info.get("uin") self.friend_info[uin] = info marknames = data.get("result", {}).get("marknames", []) [self.mark_to_uin.update({minfo.get("markname"): minfo.get("uin")}) for minfo in marknames] logging.debug("加载好友信息 {0!r}".format(self.friend_info)) logging.info(data) if self.status_callback and call_status: self.status_callback(True) self.update_group() self.http.post(url, params, headers = self.base_header, delay = 3600, callback = callback, kwargs = {"call_status":False}) def update_group(self, resp = None): """ 获取组列表, 并获取组成员 获取组列表: url: http://s.web2.qq.com/api/get_group_name_list_mask2 method: POST params: { r : { vfwebqq // 登录前返回的cookie值 } } """ logging.info("获取群列表") url = "http://s.web2.qq.com/api/get_group_name_list_mask2" params = [("r", '{"vfwebqq":"%s"}' % self.vfwebqq),] headers = {"Origin": "http://s.web2.qq.com"} headers.update(self.base_header) self.http.post(url, params, headers = headers, callback = self.group_members) def group_members(self, resp): """ 获取群列表, 获取群列表中的成员 url: http://s.web2.qq.com/api/get_group_info_ext2 method: GET params: { gcode // 群代码 vfwebqq // 登录前的cookie值 t // int(time.time()) } headers: "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3" """ logging.info("加载组成员信息") data = json.loads(resp.body) logging.debug(u"群信息 {0!r}".format(data)) group_list = data.get("result", {}).get("gnamelist", []) logging.debug(u"群列表: {0!r}".format(group_list)) if not group_list and not self.poll_and_heart: self.heartbeat(0) self.poll() for i, group in enumerate(group_list): gcode = group.get("code") url = "http://s.web2.qq.com/api/get_group_info_ext2" params = [("gcode", gcode),("vfwebqq", self.vfwebqq), ("cb", "undefined"), ("t", int(time.time()))] kwargs = dict(gcode = gcode) if i == 0: kwargs.update(poll = True) self.http.get(url, params, headers = self.base_header, callback = self.do_group_members, kwargs = kwargs, delay = i * 300) self.group_info[gcode] = group def do_group_members(self, resp, gcode, poll = False): """ 获取群成员数据 """ data = json.loads(resp.body) logging.debug(u"获取群成员信息 {0!r}".format(data)) members = data.get("result", {}).get("minfo", []) self.group_members_info[gcode] = {} for m in members: uin = m.get("uin") self.group_members_info[gcode][uin] = m cards = data.get("result", {}).get("cards", []) for card in cards: uin = card.get("muin") group_name = card.get("card") self.group_members_info[gcode][uin]["nick"] = group_name logging.debug(u"群成员信息: {0!r}".format(self.group_members_info)) if poll and not self.poll_and_heart: logging.info("开始拉取信息和心跳") self.login_time = time.time() self.poll() self.heartbeat(0) def poll(self): """ 建立长连接获取消息 url:http://d.web2.qq.com/channel/poll2 方法: POST 参数: { r:{ clientid // 客户端id psessionid // session id key // 固定为0 ids // 固定为 [] } clientid psessionid } 头部: "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=2" """ if not self.poll_and_heart: self.poll_and_heart = True url = "http://d.web2.qq.com/channel/poll2" rdic = {"clientid": self.clientid, "psessionid": self.psessionid, "key": 0, "ids":[]} params = [("r", json.dumps(rdic)), ("clientid", self.clientid), ("psessionid", self.psessionid)] headers = {"Referer":"https://d.web2.qq.com/cfproxy.html?v=20110331002&callback=1", "Origin":"http://d.web2.qq.com"} self.http.post(url, params, headers = headers, request_timeout = 60.0, connect_timeout = 60.0, callback = self.handle_msg) def handle_msg(self, resp): """ 处理消息 """ if self.poll_stoped: return self.poll() if not resp.body: return data = resp.body try: msg = json.loads(data) if msg.get("retcode") in [121, 100006]: logging.error(u"获取消息异常 {0!r}".format(data)) exit() logging.info(u"获取消息: {0!r}".format(msg)) self.msg_dispatch.dispatch(msg) except ValueError: if DEBUG: traceback.print_exc() logging.error(u"消息加载失败: %s", data) def heartbeat(self, delay = 60): """ 开始心跳 url:http://web.qq.com/web2/get_msg_tip 方法: GET 参数: { uin // 固定为空 tp // 固定为1 rc // 固定为1 id // 固定位0 lv // 固定为2 t // 开始的心跳时间(int(time.time()) * 1000) } """ if not self.poll_and_heart: self.poll_and_heart = True self.hThread = threading.Thread(name="heartThead#1", target= self._heartbeat) self.hThread.setDaemon(True) self.hThread.start() def _heartbeat(self): i = self.rc url = "http://web.qq.com/web2/get_msg_tip" params = dict([("uin", ""), ("tp", 1), ("id", 0), ("retype", 1), ("rc", i), ("lv", 3), ("t", int(time.time() * 1000))]) def callback(resp): logging.info("心跳..") while True: try: self.http.get(url, params, callback = callback, connect_timeout = 1.0, request_timeout = 1.0) except: pass i += 1 params["rc"] = i time.sleep(60) def make_msg_content(self, content): """ 构造QQ消息的内容 """ self.msg_id += 1 return json.dumps([content, ["font", {"name":"Monospace", "size":10, "style":[0, 0, 0], "color":"000000"}]]) def get_sess_group_sig(self, to_uin, callback): """ 获取临时消息组签名 URL: http://d.web2.qq.com/channel/get_c2cmsg_sig2 METHOD: GET PARAMS: id // 请求ID 固定为833193360 to_uin // 消息接受人uin( 消息的from_uin) service_type // 固定为0 clientid // 客户端id psessionid // session id t // 当前时间秒1370671760656 HEADERS: Referer:http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3 """ url = "http://d.web2.qq.com/channel/get_c2cmsg_sig2" params = (("id", 833193360), ("to_uin", to_uin), ("service_type", 0), ("clientid", self.clientid), ("psessionid", self.psessionid), ("t", time.time())) def callback(resp): data = resp.body r = json.loads(data) result = r.get("result") group_sig = result.get("value") if r.get("retcode") != 0: logging.warn(u"加载临时消息签名失败: {0}".format(group_sig)) return try: logging.info("加载临时消息签名 {0} for {1}".format(group_sig, to_uin)) except UnicodeError: return self.group_sig[to_uin] = group_sig callback() self.http.get(url, params, callback = callback, headers = self.base_header) def send_sess_msg(self, to_uin, content): """ 发送临时消息 URL:http://d.web2.qq.com/channel/send_sess_msg2 METHOD: POST PARAMS: r:{ to // 消息接收人 uin group_sig // 组签名 face // 固定为 564, content // 发送内容 msg_id // 消息id service_type // 固定为0, clientid // 客户端id psessionid // sessionid } clientid // 客户端id psessionid // sessionid Headers: self.base_header """ group_sig = self.group_sig.get(to_uin) if not group_sig: callback = partial(self.send_sess_msg, to_uin, content) return self.get_sess_group_sig(to_uin, callback) logging.info(u"发送临时消息 {0} 到 {1}".format(content, to_uin)) delay, n = self.get_delay(content) content = self.make_msg_content(content) url = "http://d.web2.qq.com/channel/send_sess_msg2" params = (("r", json.dumps({"to":to_uin, "group_sig":group_sig, "face":564, "content":content, "msg_id": self.msg_id, "service_type":0, "clientid":self.clientid, "psessionid":self.psessionid})), ("clientid", self.clientid), ("psessionid", self.psessionid)) def callback(resp): self.last_msg_numbers -= n self.last_msg_time = time.time() self.http.post(url, params, headers = self.base_header, callback = callback, delay = delay) def send_buddy_msg(self, to_uin, content, callback = None): """ 发送好友消息 URL: http://d.web2.qq.com/channel/send_buddy_msg2 METHOD: POST PARAMS: { "r":{ "to" // 好友uin "face" // 固定为564 "content" // 发送内容 "msg_id" // 消息id, 每发一条递增 "clientid" // 客户端id "psessionid" // sessionid } "clientid":clientid, "psessionid": psessionid, } HEADERS: Referer:http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3 """ content = self.make_msg_content(content) url = "http://d.web2.qq.com/channel/send_buddy_msg2" r = {"to":to_uin, "face":564, "content":content, "clientid":self.clientid, "msg_id": self.msg_id, "psessionid": self.psessionid} params = [("r",json.dumps(r)), ("clientid",self.clientid), ("psessionid", self.psessionid)] headers = {"Origin": "http://d.web2.qq.com"} headers.update(self.base_header) delay, n = self.get_delay(content) def _callback(resp): logging.info(u"发送好友消息 {0} 给 {1} 成功".format(content, to_uin)) if callback: callback(True) self.last_msg_numbers -= n self.last_msg_time = time.time() logging.info(u"发送好友消息 {0} 给 {1} ...".format(content, to_uin)) self.http.post(url, params, headers = headers, delay = delay, callback = _callback) def send_group_msg(self, group_uin, content): """ 发送群消息 url:http://d.web2.qq.com/channel/send_qun_msg2 方法: POST 参数: { r:{ group_uin // gid content // 发送内容 msg_id // 消息id, 每次发送消息应该递增 clientid // 客户端id psessionid // sessionid } clientid psessionid } """ gid = self.group_info.get(group_uin, {}).get("gid") source = content content = self.make_msg_content(source) url = "http://d.web2.qq.com/channel/send_qun_msg2" r = {"group_uin": gid, "content": content, "msg_id": self.msg_id, "clientid": self.clientid, "psessionid": self.psessionid} params = [("r", json.dumps(r)), ("psessionid", self.psessionid), ("clientid", self.clientid)] print params delay, n = self.get_delay(content) callback = self.send_group_msg_back logging.info(u"发送群消息 {0} 到 {1}...".format(content, group_uin)) headers = {"Origin": "http://d.web2.qq.com", "Referer":"http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=2"} self.http.post(url, params, headers = headers, callback = callback, args = (source, group_uin, n), delay = delay) def get_delay(self, content): MIN = MESSAGE_INTERVAL delay = 0 sub = time.time() - self.last_msg_time if self.last_msg_numbers < 0: self.last_msg_numbers = 0 # 不足最小间隔就补足最小间隔 if sub < MIN: delay = MIN logging.debug(u"间隔 %s 小于 %s, 设置延迟为%s", sub, MIN, delay) # 如果间隔是已有消息间隔的2倍, 则清除已有消息数 #print "sub", sub, "n:", self.last_msg_numbers if self.last_msg_numbers > 0 and sub / (MIN * self.last_msg_numbers)> 1: self.last_msg_numbers = 0 # 如果还有消息未发送, 则加上他们的间隔 if self.last_msg_numbers > 0: delay += MIN * self.last_msg_numbers logging.info(u"有%s条消息未发送, 延迟为 %s", self.last_msg_numbers, delay) n = 1 # 如果这条消息和上条消息一致, 保险起见再加上一个最小间隔 if self.last_msg_content == content and sub < MIN: delay += MIN self.last_msg_numbers += 1 n = 2 self.last_msg_numbers += 1 self.last_msg_content = content if delay: logging.info(u"有 {1} 个消息未投递将会在 {0} 秒后投递" .format(delay, self.last_msg_numbers)) # 返回消息累加个数, 在消息发送后减去相应的数目 return delay, n def send_group_msg_back(self, content, group_uin, n, resp): logging.info(u"发送群消息 {0} 到 {1} 成功".format(content, group_uin)) self.last_msg_time = time.time() if self.last_msg_numbers > 0: self.last_msg_numbers -= n def set_signature(self, signature, password, callback): """ 设置QQ签名, 可以通过发送好友消息设置签名, 消息应按照如下格式: 设置签名:[密码]|[签名内容] // 密码和签名内容不能包含分割符 url: http://s.web2.qq.com/api/set_long_nick2 method: POST params: r : { nlk // 签名内容 vfwebqq // 登录时获取的cookie值 } headers: Referer:http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=1 """ if password != Set_Password: return callback(u"你没有权限这么做") logging.info(u"设置QQ签名 {0}".format(signature)) url = "http://s.web2.qq.com/api/set_long_nick2" params = (("r", json.dumps({"nlk":signature, "vfwebqq":self.vfwebqq})),) headers = {"Origin":"http://s.web2.qq.com"} headers.update(self.base_header) def callback(resp): data = resp.body print data result = json.loads(data).get("retcode") if result == 0: callback(u"设置成功") else: callback(u"设置失败") self.http.post(url, params, headers = headers, callback = callback) def run(self): self.get_login_sig() self.http.start() def real_stop(self): self.http.stop() def stop(self): self.stop_poll = True def send_msg_with_markname(self, markname, message, callback): """ 使用备注发送消息 """ uin = self.mark_to_uin.get(markname) if not uin: return False self.send_buddy_msg(uin, message, callback) return True def accept_and_set_mark(self, uin, qq_num): """ 确认添加并更改备注 """ url = "http://s.web2.qq.com/api/allow_and_add2" params = [("r","{\"account\":%d, \"gid\":0, \"mname\":\"%d\"," " \"vfwebqq\":\"%s\"}" % (qq_num, qq_num, self.vfwebqq)),] headers = {"Origin":"http://s.web2.qq.com"} headers.update(self.base_header) def _callback(resp): data = json.loads(resp.body) logging.info(data) logging.info(params) if data.get("retcode") == 0: logging.info(u"添加 {0} 成功".format(qq_num)) self.mark_to_uin[uin] = qq_num else: logging.info(u"添加 {0} 失败".format(qq_num)) self.http.post(url, params, headers = headers, callback = _callback)
class WebQQ(object): def __init__(self, qid, pwd): self.qid = qid # QQ 号 self.__pwd = pwd # QQ密码 self.nickname = None # 初始化QQ昵称 self.http = TornadoHTTPClient() self.http.set_user_agent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/28.0.1500.71 Chrome/28.0.1500.71 Safari/537.36") self.http.debug = DEBUG self.http.validate_cert = False self.http.set_global_headers({"Accept-Charset": "UTF-8,*;q=0.5"}) self.msg_dispatch = MessageDispatch(self) self.rc = random.randrange(0, 100) self.aid = 1003903 # aid 固定 self.clientid = random.randrange(11111111, 99999999) # 客户端id 随机固定 self.msg_id = random.randrange(1111111, 99999999) # 消息id, 随机初始化 self.require_check = False # 是否需要验证码 self.poll_and_heart = False # 开始拉取消息和心跳 # 初始化WebQQ登录期间需要保存的数据 self.check_code = None self.ptwebqq = None self.check_data = None # 初始化检查时返回的数据 self.blogin_data = None # 初始化登录前返回的数据 self.friend_info = {} # 初始化好友列表 self.group_info = {} # 初始化组列表 self.group_sig = {} # 组签名映射, 用作发送临时消息(sess_message) self.group_members_info = {} # 初始化组成员列表 self.hb_time = int(time.time() * 1000) self.daid = 164 self.login_sig = None self.login_time = None # 登录的时间 self.last_group_msg_time = time.time() self.last_msg_content = None self.last_msg_numbers = 0 # 剩余位发送的消息数量 self.base_header = {"Referer":"https://d.web2.qq.com/cfproxy.html?v=20110331002&callback=1"} def ptuiCB(self, scode, r, url, status, msg, nickname = None): """ 模拟JS登录之前的回调, 保存昵称 """ if int(scode) == 0: logging.info("从Cookie中获取ptwebqq的值") self.ptwebqq = self.http.cookie['.qq.com']['/']['ptwebqq'].value self.logined = True elif int(scode) == 4: logging.error(msg) self.check() else: logging.error(u"server response: {0}".format(msg.decode('utf-8'))) exit(2) if nickname: self.nickname = nickname def get_group_member_nick(self, gcode, uin): return self.group_members_info.get(gcode, {}).get(uin, {}).get("nick") def get_uptime(self): MIN = 60 HOUR = 60 * MIN DAY = 24 * HOUR up_time = datetime.fromtimestamp(self.login_time).strftime("%H:%M:%S") now = time.time() sub = now - self.login_time days = int(sub / DAY) hours = int(sub / HOUR) mins = int(sub / MIN) if mins: num = mins unit = "min" if hours: num = hours unit = "hours" if hours > 1 else "hour" if days: num = days unit = "days" if days > 1 else "day" if not days and not mins and not hours: num = int(sub) unit = "sec" return "{0} up {1} {2}".format(up_time, num, unit) def get_login_sig(self): logging.info("获取 login_sig...") url = "https://ui.ptlogin2.qq.com/cgi-bin/login" params = [("daid", self.daid), ("target", "self"), ("style", 5), ("mibao_css", "m_webqq"), ("appid", self.aid), ("enable_qlogin", 0), ("no_verifyimg", 1), ("s_url", "http://web2.qq.com/loginproxy.html"), ("f_url", "loginerroralert"), ("strong_login", 1), ("login_state", 10), ("t", "20130723001")] self.http.get(url, params, callback = self._get_login_sig) self.http.get("http://web2.qq.com") def _get_login_sig(self, resp): sigs = SIG_RE.findall(resp.body) if len(sigs) == 1: self.login_sig = sigs[0] logging.info(u"获取Login Sig: {0}".format(self.login_sig)) else: logging.warn(u"没有获取到 Login Sig, 后续操作可能失败") self.login_sig = "" self.check() def check(self): """ 检查是否需要验证码 url : https://ssl.ptlogin2.qq.com/check 方法: GET 参数: { uin // qq号 appid // 程序id 固定为1003903 r // 随机数 u1 // http://web2.qq.com/loginproxy.html js_ver // 10040 js_type // 0 } 返回: ptui_checkVC('0','!PTH','\x00\x00\x00\x00\x64\x74\x8b\x05'); 第一个参数表示状态码, 0 不需要验证, 第二个为验证码, 第三个为uin """ logging.info(u"检查是否需要验证码...") #url = "https://ssl.ptlogin2.qq.com/check" url = "http://check.ptlogin2.qq.com/check" params = {"uin":self.qid, "appid":self.aid, "u1": "http://web2.qq.com/loginproxy.html", "login_sig":self.login_sig, "js_ver":10040, "js_type":0, "r" : random.random()} headers = {"Referer":"https://ui.ptlogin2.qq.com/cgi-bin/login?daid=" "164&target=self&style=5&mibao_css=m_webqq&appid=1003903&" "enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fweb2.q" "q.com%2Floginproxy.html&f_url=loginerroralert&strong_log" "in=1&login_state=10&t=20130723001"} self.http.get(url, params, headers = headers, callback = self.handle_verify) cookie_url = "http://www.simsimi.com/talk.htm?lc=ch" cookie_params = (("lc", "ch"),) headers = {"Referer": "http://www.simsimi.com/talk.htm"} self.http.get(cookie_url, cookie_params, headers = headers) headers = {"Referer": "http://www.simsimi.com/talk.htm?lc=ch"} self.http.get("http://www.simsimi.com/func/langInfo", cookie_params, headers = headers) def handle_pwd(self, r, vcode, huin): """ 根据检查返回结果,调用回调生成密码和保存验证码 """ pwd = md5(md5(self.__pwd).digest() + huin).hexdigest().upper() pwd = md5(pwd + vcode).hexdigest().upper() return pwd def get_check_img(self, r, vcode, uin): """ 获取验证图片 """ def callback(resp): path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "check.jpg") fp = open(path, 'wb') fp.write(resp.body) fp.close() if UPLOAD_CHECKIMG: res = upload_file("check.jpg", path) path = res.url print u"验证图片: {0}".format(path) check_code = "" while not check_code: check_code = raw_input("输入验证图片上的验证码: ") ccode = check_code.strip().lower() self.check_code = ccode pwd = self.handle_pwd(r, ccode.upper(), uin) self.before_login(pwd) url = "https://ssl.captcha.qq.com/getimage" params = [("aid", self.aid), ("r", random.random()), ("uin", self.qid)] self.http.get(url, params, callback = callback) def handle_verify(self, resp): ptui_checkVC = lambda r, v, u: (r, v, u) r, vcode, uin = eval(resp.body.strip().rstrip(";")) if int(r) == 0: logging.info("验证码检查完毕, 不需要验证码") password = self.handle_pwd(r, vcode, uin) self.before_login(password) self.check_code = vcode else: logging.warn("验证码检查完毕, 需要验证码") self.get_check_img(r, vcode, uin) self.require_check = True def before_login(self, password): """ 登录之前的操作 url: https://ssl.ptlogin2.qq.com/login 方法: GET 参数: { u // qq号码 p // 经过处理的密码 verifycode // 验证码 webqq_type // 固定为10 remember_uin // 是否记住qq号, 传1 即可 login2qq // 登录qq, 传1 aid // appid 固定为 1003903 u1 // 固定为 http://www.qq.com h // 固定为1 ptrediect // 固定为0 ptlang // 固定为2052 from_ui // 固定为 1 pttype // 固定为1 dumy // 固定为空 fp // 固定为loginerroralert ( 重要) mibao_css // 固定为 m_webqq t // 固定为1 g // 固定为 js_type // 固定为0 js_ver // 固定为10021 其他: 如果check步骤验证了需要验证码, 需加上 Referer头 值为: https://ui.ptlogin2.qq.com/cgi-bin/login?target=self&style=5&mibao_css=m_webqq&appid=1003903&enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fweb.qq.com%2Floginproxy.html&f_url=loginerroralert&strong_login=1&login_state=10&t=20130221001 接口返回: ptuiCB('0','0','http://www.qq.com','0','登录成功!', 'nickname'); 先检查是否需要验证码,不需要验证码则首先执行一次登录 然后获取Cookie里的ptwebqq保存在实例里,供后面的接口调用 """ url = "https://ssl.ptlogin2.qq.com/login" params = [("u",self.qid), ("p",password), ("verifycode", self.check_code), ("webqq_type",10), ("remember_uin", 1),("login2qq",1), ("aid", self.aid), ("u1", "http://www.qq.com/loginproxy.h" "tml?login2qq=1&webqq_type=10"), ("h", 1), ("action", 4-5-8246), ("ptredirect", 0), ("ptlang", 2052), ("from_ui", 1), ("daid", self.daid), ("pttype", 1), ("dumy", ""), ("fp", "loginerroralert"), ("mibao_css","m_webqq"), ("t",1), ("g",1), ("js_type",0), ("js_ver", 10040), ("login_sig", self.login_sig)] headers = {} if self.require_check: headers.update(Referer = "https://ui.ptlogin2.qq.com/cgi-" "bin/login?target=self&style=5&mibao_css=m_" "webqq&appid=1003903&enable_qlogin=0&no_ver" "ifyimg=1&s_url=http%3A%2F%2Fweb.qq.com%2Fl" "oginproxy.html&f_url=loginerroralert&stron" "g_login=1&login_state=10&t=20130221001") logging.info("检查完毕, 开始登录前准备") self.http.get(url, params, headers = headers, callback = self.login0) def login0(self, resp): logging.info("开始登录前准备...") blogin_data = resp.body.decode("utf-8").strip().rstrip(";") eval("self." + blogin_data) location1 = re.findall(r'ptuiCB\(\'0\'\,\'0\'\,\'(.*)\'\,\'0\'\,', blogin_data)[0] params = [] header = {"Referer": "https://ui.ptlogin2.qq.com/cgi-bin/login?d" "aid=164&target=self&style=5&mibao_css=m_webqq&appid=1" "003903&enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2" "F%2Fweb2.qq.com%2Floginproxy.html&f_url=loginerrorale" "rt&strong_login=1&login_state=10&t=20130723001"} self.http.get(location1, params, headers = header, callback = self.get_location1) def get_location1(self, resp): if resp.code == 302: location2 = resp.headers.get("Location") params = [] header = {} self.http.get(location2, params, headers = header, callback = self.get_location1) else: logging.info("准备完毕, 开始登录") self.login() def login(self): """ 获取登录前的数据, 并进行登录 url: http://d.web2.qq.com/channel/login2 方法: POST 参数: { r : { status // 登录后的状态 ("online") ptwebqq // 上次请求返回的cookie passwd_sig // 固定为空 clientid // 随机的clientid psessionid // 传递 null } clientid // 客户端id psessionid // 传递null } 其他: 需加上 Referer和 Origin 头: "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3" "Origin": "http://d.web2.qq.com" 返回: {u'retcode': 0, u'result': { 'status': 'online', 'index': 1075, 'psessionid': '', u'user_state': 0, u'f': 0, u'uin': 1685359365, u'cip': 3673277226, u'vfwebqq': u'', u'port': 43332}} 保存result中的psessionid和vfwebqq供后面接口调用 """ time.sleep(4) url = "http://d.web2.qq.com/channel/login2" params = [("r", '{"status":"online","ptwebqq":"%s","passwd_sig":"",' '"clientid":"%d","psessionid":null}'\ % (self.ptwebqq, self.clientid)), ("clientid", self.clientid), ("psessionid", "null") ] headers = { "Origin": "http://d.web2.qq.com"} headers.update(self.base_header) logging.info("登录准备完毕, 开始登录") self.http.post(url, params, headers = headers, callback= self.update_friend) def _hash(self): a = str(self.qid) e = self.ptwebqq l = len(e) # 将qq号码转换成整形列表 b, k, d = 0, -1, 0 for d in a: d = int(d) b += d b %= l f = 0 if b + 4 > l: g = 4 + b - l for h in range(4): f |= h < g and ( ord(e[b + h]) & 255) << (3 - h) * 8 or ( ord(e[h - g]) & 255) << (3 - h) * 8 else: for h in range(4): f |= (ord(e[b + h]) & 255) << (3 - h) * 8 k ^= f c = [k >> 24 & 255, k >> 16 & 255, k >> 8 & 255, k & 255] import string k = list(string.digits) + ['A', 'B', 'C', 'D', 'E', 'F'] d = [k[b >> 4 & 15] + k[b & 15] for b in c] return ''.join(d) def update_friend(self, resp, login = True): """ 更新好友列表 URL: http://s.web2.qq.com/api/get_user_friends2 METHOD: POST PARAMS: {r:{"h":"hello", "vfwebqq":""}} HEADER: Referer:http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=1 """ data = json.loads(resp.body) if login: if data.get("retcode") != 0: logging.error("登录失败") exit(2) self.vfwebqq = data.get("result", {}).get("vfwebqq") self.psessionid = data.get("result", {}).get("psessionid") url = "http://s.web2.qq.com/api/get_user_friends2" params = [("r", json.dumps({"h":"hello", "hash":self._hash(), "vfwebqq":self.vfwebqq}))] headers = {"Referer": "http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=1"} callback = self.update_friend if login: logging.info("登录成功") if not DEBUG: aw = "" while aw.lower() not in ["y", "yes", "n", "no"]: aw = raw_input("是否将程序至于后台[y] ") if not aw: aw = "y" if aw in ["y", "yes"]: run_daemon(self.http.post, args = (url, params), kwargs = dict(headers = headers, kwargs = dict(login = False), callback = callback)) return self.http.post(url, params, headers = headers, callback = callback, kwargs = dict(login = False)) else: logging.info("加载好友信息") lst = data.get("result", {}).get("info", []) for info in lst: uin = info.get("uin") self.friend_info[uin] = info self.update_group() self.http.post(url, params, headers = self.base_header, delay = 300, callback = callback) def update_group(self, resp = None): """ 获取组列表, 并获取组成员 获取组列表: url: http://s.web2.qq.com/api/get_group_name_list_mask2 method: POST params: { r : { vfwebqq // 登录前返回的cookie值 } } """ logging.info("获取群列表") url = "http://s.web2.qq.com/api/get_group_name_list_mask2" params = [("r", '{"vfwebqq":"%s"}' % self.vfwebqq),] headers = {"Origin": "http://s.web2.qq.com"} headers.update(self.base_header) self.http.post(url, params, headers = headers, callback = self.group_members) def group_members(self, resp): """ 获取群列表, 获取群列表中的成员 url: http://s.web2.qq.com/api/get_group_info_ext2 method: GET params: { gcode // 群代码 vfwebqq // 登录前的cookie值 t // int(time.time()) } headers: "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3" """ logging.info("加载组成员信息") data = json.loads(resp.body) group_list = data.get("result", {}).get("gnamelist", []) if not group_list: self.heartbeat(0) self.poll() for i, group in enumerate(group_list): gcode = group.get("code") url = "http://s.web2.qq.com/api/get_group_info_ext2" params = [("gcode", gcode),("vfwebqq", self.vfwebqq), ("t", int(time.time()))] callback = self.do_group_members if i == len(group_list) -1 : kwargs = dict(gcode = gcode, last = True) else: kwargs = dict(gcode = gcode) self.http.get(url, params, headers = self.base_header, callback = callback, kwargs = kwargs) self.group_info[gcode] = group def do_group_members(self, resp, gcode, last = False): """ 获取群成员数据 """ data = json.loads(resp.body) members = data.get("result", {}).get("minfo", []) self.group_members_info[gcode] = {} for m in members: uin = m.get("uin") self.group_members_info[gcode][uin] = m cards = data.get("result", {}).get("cards", []) for card in cards: uin = card.get("muin") group_name = card.get("card") self.group_members_info[gcode][uin]["nick"] = group_name if last and not self.poll_and_heart: logging.info("万事具备,开始拉取信息和心跳") self.poll() self.heartbeat(0) def poll(self): """ 建立长连接获取消息 url:http://d.web2.qq.com/channel/poll2 方法: POST 参数: { r:{ clientid // 客户端id psessionid // session id key // 固定为0 ids // 固定为 [] } clientid psessionid } 头部: "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=2" """ if not self.poll_and_heart: self.poll_and_heart = True url = "http://d.web2.qq.com/channel/poll2" rdic = {"clientid": self.clientid, "psessionid": self.psessionid, "key": 0, "ids":[]} params = [("r", json.dumps(rdic)), ("clientid", self.clientid), ("psessionid", self.psessionid)] headers = {"Referer":"https://d.web2.qq.com/cfproxy.html?v=20110331002&callback=1", "Origin":"http://d.web2.qq.com"} self.http.post(url, params, headers = headers, request_timeout = 60.0, connect_timeout = 60.0, callback = self.handle_msg) def handle_msg(self, resp): """ 处理消息 """ self.poll() data = resp.body try: msg = json.loads(data) if msg.get("retcode") in [121, 103]: logging.error(u"登录失败") return logging.info(u"获取消息: {0!r}".format(msg)) self.msg_dispatch.dispatch(msg) except ValueError: traceback.print_exc() logging.error(u"消息加载失败: %s", data) def heartbeat(self, delay = 60): """ 开始心跳 url:http://web.qq.com/web2/get_msg_tip 方法: GET 参数: { uin // 固定为空 tp // 固定为1 rc // 固定为1 id // 固定位0 lv // 固定为2 t // 开始的心跳时间(int(time.time()) * 1000) } """ logging.info("心跳..") self.login_time = time.time() if not self.poll_and_heart: self.poll_and_heart = True url = "http://web.qq.com/web2/get_msg_tip" params = [("uin", ""), ("tp", 1), ("id", 0), ("retype", 1), ("rc", self.rc), ("lv", 3), ("t", int(self.hb_time * 1000))] self.rc += 1 self.http.get(url, params, callback = self.hb_next, delay = delay) def hb_next(self, resp): """ 持续心跳 """ self.heartbeat() def make_msg_content(self, content): """ 构造QQ消息的内容 """ self.msg_id += 1 return json.dumps([content, ["font", {"name":"Monospace", "size":10, "style":[0, 0, 0], "color":"000000"}]]) def get_sess_group_sig(self, to_uin, callback): """ 获取临时消息组签名 URL: http://d.web2.qq.com/channel/get_c2cmsg_sig2 METHOD: GET PARAMS: id // 请求ID 固定为833193360 to_uin // 消息接受人uin( 消息的from_uin) service_type // 固定为0 clientid // 客户端id psessionid // session id t // 当前时间秒1370671760656 HEADERS: Referer:http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3 """ url = "http://d.web2.qq.com/channel/get_c2cmsg_sig2" params = (("id", 833193360), ("to_uin", to_uin), ("service_type", 0), ("clientid", self.clientid), ("psessionid", self.psessionid), ("t", time.time())) def callback(resp): data = resp.body r = json.loads(data) result = r.get("result") group_sig = result.get("value") if r.get("retcode") != 0: logging.warn(u"加载临时消息签名失败: {0}".format(group_sig)) return try: logging.info("加载临时消息签名 {0} for {1}".format(group_sig, to_uin)) except UnicodeError: return self.group_sig[to_uin] = group_sig callback() self.http.get(url, params, callback = callback, headers = self.base_header) def send_sess_msg(self, to_uin, content): """ 发送临时消息 URL:http://d.web2.qq.com/channel/send_sess_msg2 METHOD: POST PARAMS: r:{ to // 消息接收人 uin group_sig // 组签名 face // 固定为 564, content // 发送内容 msg_id // 消息id service_type // 固定为0, clientid // 客户端id psessionid // sessionid } clientid // 客户端id psessionid // sessionid Headers: self.base_header """ group_sig = self.group_sig.get(to_uin) if not group_sig: callback = partial(self.send_sess_msg, to_uin, content) return self.get_sess_group_sig(to_uin, callback) logging.info(u"发送临时消息 {0} 到 {1}".format(content, to_uin)) delay, n = self.get_delay(content) content = self.make_msg_content(content) url = "http://d.web2.qq.com/channel/send_sess_msg2" params = (("r", json.dumps({"to":to_uin, "group_sig":group_sig, "face":564, "content":content, "msg_id": self.msg_id, "service_type":0, "clientid":self.clientid, "psessionid":self.psessionid})), ("clientid", self.clientid), ("psessionid", self.psessionid)) def callback(resp): self.last_msg_numbers -= n self.http.post(url, params, headers = self.base_header, callback = callback, delay = delay) def send_buddy_msg(self, to_uin, content): """ 发送好友消息 URL: http://d.web2.qq.com/channel/send_buddy_msg2 METHOD: POST PARAMS: { "r":{ "to" // 好友uin "face" // 固定为564 "content" // 发送内容 "msg_id" // 消息id, 每发一条递增 "clientid" // 客户端id "psessionid" // sessionid } "clientid":clientid, "psessionid": psessionid, } HEADERS: Referer:http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3 """ logging.info(u"发送好友消息 {0} 给 {1}".format(content, to_uin)) content = self.make_msg_content(content) url = "http://d.web2.qq.com/channel/send_buddy_msg2" r = {"to":to_uin, "face":564, "content":content, "clientid":self.clientid, "msg_id": self.msg_id, "psessionid": self.psessionid} params = [("r",json.dumps(r)), ("clientid",self.clientid), ("psessionid", self.psessionid)] headers = {"Origin": "http://d.web2.qq.com"} headers.update(self.base_header) delay, n = self.get_delay(content) def callback(resp): self.last_msg_numbers -= n self.http.post(url, params, headers = headers, delay = delay, callback = callback) def send_group_msg(self, group_uin, content): """ 发送群消息 url:http://d.web2.qq.com/channel/send_qun_msg2 方法: POST 参数: { r:{ group_uin // gid content // 发送内容 msg_id // 消息id, 每次发送消息应该递增 clientid // 客户端id psessionid // sessionid } clientid psessionid } """ gid = self.group_info.get(group_uin, {}).get("gid") source = content content = self.make_msg_content(source) url = "http://d.web2.qq.com/channel/send_qun_msg2" r = {"group_uin": gid, "content": content, "msg_id": self.msg_id, "clientid": self.clientid, "psessionid": self.psessionid} params = [("r", json.dumps(r)), ("psessionid", self.psessionid), ("clientid", self.clientid)] delay, n = self.get_delay(content) callback = self.send_group_msg_back self.http.post(url, params, headers = self.base_header, callback = callback, args = (source, group_uin, n), delay = delay) def get_delay(self, content): MIN = MESSAGE_INTERVAL delay = 0 if time.time() - self.last_group_msg_time < MIN or\ self.last_msg_numbers > 0: delay = self.last_msg_numbers * MIN numbers = 1 if self.last_msg_content == content: delay += 0.5 self.last_msg_numbers += 1 numbers = 2 self.last_msg_numbers += 1 self.last_msg_content = content if delay: logging.info(u"有 {1} 个消息未投递将会在 {0} 秒后投递" .format(delay, self.last_msg_numbers)) return delay, numbers def send_group_msg_back(self, content, group_uin, n, resp): logging.info(u"发送群消息 {0} 到 {1}".format(content, group_uin)) self.last_group_msg_time = time.time() if self.last_msg_numbers > 0: self.last_msg_numbers -= n def set_signature(self, signature, password, callback): """ 设置QQ签名, 可以通过发送好友消息设置签名, 消息应按照如下格式: 设置签名:[密码]|[签名内容] // 密码和签名内容不能包含分割符 url: http://s.web2.qq.com/api/set_long_nick2 method: POST params: r : { nlk // 签名内容 vfwebqq // 登录时获取的cookie值 } headers: Referer:http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=1 """ if password != Set_Password: return callback(u"你没有权限这么做") logging.info(u"设置QQ签名 {0}".format(signature)) url = "http://s.web2.qq.com/api/set_long_nick2" params = (("r", json.dumps({"nlk":signature, "vfwebqq":self.vfwebqq})),) headers = {"Origin":"http://s.web2.qq.com"} headers.update(self.base_header) def callback(resp): data = resp.body print data result = json.loads(data).get("retcode") if result == 0: callback(u"设置成功") else: callback(u"设置失败") self.http.post(url, params, headers = headers, callback = callback) def run(self): self.get_login_sig() self.http.start()
class WebQQ(object): def __init__(self, qid, pwd, handler=None): self.qid = qid # QQ 号 self.__pwd = pwd # QQ密码 self.nickname = None # 初始化QQ昵称 self.http = TornadoHTTPClient() self.http.set_user_agent( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/28.0.1500.71 Chrome/28.0.1500.71 Safari/537.36" ) self.http.debug = TRACE self.http.validate_cert = False self.http.set_global_headers({"Accept-Charset": "UTF-8,*;q=0.5"}) self.msg_dispatch = MessageDispatch(self) self.rc = random.randrange(0, 100) self.aid = 1003903 # aid 固定 self.clientid = random.randrange(11111111, 99999999) # 客户端id 随机固定 self.msg_id = random.randrange(11111111, 99999999) # 消息id, 随机初始化 self.require_check = False # 是否需要验证码 self.poll_and_heart = False # 开始拉取消息和心跳 # 初始化WebQQ登录期间需要保存的数据 self.check_code = None self.ptwebqq = None self.require_check_time = None # 需要验证码的时间 self.checkimg_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "check.jpg") self.friend_info = {} # 初始化好友列表 self.group_info = {} # 初始化组列表 self.group_sig = {} # 组签名映射, 用作发送临时消息(sess_message) self.group_members_info = {} # 初始化组成员列表 self.mark_to_uin = {} # 备注名->uin的映射 self.daid = 164 self.login_sig = None self.login_time = None # 登录的时间 self.last_msg_time = time.time() self.last_msg_content = None self.last_msg_numbers = 0 # 剩余位发送的消息数量 self.status_callback = None self.poll_stoped = True # 获取消息是否停止 self.hThread = None # 心跳线程 # self.base_header = {"Referer":"https://d.web2.qq.com/cfproxy.html?v=20110331002&callback=1"} self.base_header = {"Referer": "http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=3"} def ptuiCB(self, scode, r, url, status, msg, nickname=None): """ 模拟JS登录之前的回调, 保存昵称 """ if int(scode) == 0: logging.info("从Cookie中获取ptwebqq的值") old_value = self.ptwebqq try: self.ptwebqq = self.http.cookie[".qq.com"]["/"]["ptwebqq"].value except: logging.error("从Cookie中获取ptwebqq的值失败, 使用旧值尝试") self.ptwebqq = old_value self.logined = True elif int(scode) == 4: logging.error(msg) if self.status_callback: self.status_callback(False, msg) self.check() else: logging.error(u"server response: {0}".format(msg.decode("utf-8"))) self.check() if nickname: self.nickname = nickname.decode("utf-8") def get_group_member_nick(self, gcode, uin): return self.group_members_info.get(gcode, {}).get(uin, {}).get("nick") def get_uptime(self): MIN = 60 HOUR = 60 * MIN DAY = 24 * HOUR if not isinstance(self.login_time, (int, float)): self.login_time = time.time() up_time = datetime.fromtimestamp(self.login_time).strftime("%H:%M:%S") now = time.time() sub = now - self.login_time days = int(sub / DAY) hours = int(sub / HOUR) mins = int(sub / MIN) if mins: num = mins unit = "min" if hours: num = hours unit = "hours" if hours > 1 else "hour" if days: num = days unit = "days" if days > 1 else "day" if not days and not mins and not hours: num = int(sub) unit = "sec" return "{0} up {1} {2}".format(up_time, num, unit) def get_login_sig(self, handler=None): self.handler = handler # 启用HTTP_CHECKIMG的Handler self.stop_poll_heartbeat = False with open("wait", "w"): pass logging.info("获取 login_sig...") url = "https://ui.ptlogin2.qq.com/cgi-bin/login" params = [ ("daid", self.daid), ("target", "self"), ("style", 5), ("mibao_css", "m_webqq"), ("appid", self.aid), ("enable_qlogin", 0), ("no_verifyimg", 1), ("s_url", "http://web2.qq.com/loginproxy.html"), ("f_url", "loginerroralert"), ("strong_login", 1), ("login_state", 10), ("t", "20130723001"), ] self.http.get(url, params, callback=self._get_login_sig) self.http.get("http://web2.qq.com") def _get_login_sig(self, resp): sigs = SIG_RE.findall(resp.body) if len(sigs) == 1: self.login_sig = sigs[0] logging.info(u"获取Login Sig: {0}".format(self.login_sig)) else: logging.warn(u"没有获取到 Login Sig, 后续操作可能失败") self.login_sig = "" self.check() def clean(self): if os.path.exists("lock"): os.remove("lock") if os.path.exists("wait"): os.remove("wait") def check(self): """ 检查是否需要验证码 url : https://ssl.ptlogin2.qq.com/check 方法: GET 参数: { uin // qq号 appid // 程序id 固定为1003903 r // 随机数 u1 // http://web2.qq.com/loginproxy.html js_ver // 10040 js_type // 0 } 返回: ptui_checkVC('0','!PTH','\x00\x00\x00\x00\x64\x74\x8b\x05'); 第一个参数表示状态码, 0 不需要验证, 第二个为验证码, 第三个为uin """ self.clean() self.poll_stoped = True # 检查时停止轮询消息 logging.info(u"检查是否需要验证码...") # url = "https://ssl.ptlogin2.qq.com/check" url = "http://check.ptlogin2.qq.com/check" params = { "uin": self.qid, "appid": self.aid, "u1": "http://web2.qq.com/loginproxy.html", "login_sig": self.login_sig, "js_ver": 10040, "js_type": 0, "r": random.random(), } headers = { "Referer": "https://ui.ptlogin2.qq.com/cgi-bin/login?daid=" "164&target=self&style=5&mibao_css=m_webqq&appid=1003903&" "enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fweb2.q" "q.com%2Floginproxy.html&f_url=loginerroralert&strong_log" "in=1&login_state=10&t=20130723001" } self.http.get(url, params, headers=headers, callback=self.handle_verify) def handle_pwd(self, r, vcode, huin): """ 根据检查返回结果,调用回调生成密码和保存验证码 """ pwd = md5(md5(self.__pwd).digest() + huin).hexdigest().upper() pwd = md5(pwd + vcode).hexdigest().upper() return pwd def get_check_img(self, r, vcode, uin): """ 获取验证图片 """ def callback(resp): path = self.checkimg_path fp = open(path, "wb") fp.write(resp.body) fp.close() if UPLOAD_CHECKIMG: logging.info(u"正在上传验证码..") res = upload_file("check.jpg", path) path = res.read() check_code = "" if not HTTP_CHECKIMG: print u"验证图片: {0}".format(path) while not check_code: check_code = raw_input("输入验证图片上的验证码: ") ccode = check_code.strip().lower() self.check_code = ccode pwd = self.handle_pwd(r, ccode.upper(), uin) self.before_login(pwd) else: if os.path.exists("wait"): os.remove("wait") logging.info(u"请打开http://{0}:{1} 提交验证码".format(HTTP_LISTEN, HTTP_PORT)) self.handler.r = r self.handler.uin = uin self.handler.next_callback = self.before_login self.require_check_time = time.time() if EMAIL_NOTICE: if send_notice_email(): logging.info(u"已成功发送邮件提醒") else: logging.warn(u"发送邮件提醒失败") url = "https://ssl.captcha.qq.com/getimage" params = [("aid", self.aid), ("r", random.random()), ("uin", self.qid)] self.http.get(url, params, callback=callback) def handle_verify(self, resp): ptui_checkVC = lambda r, v, u: (r, v, u) data = resp.body if not data: self.check() # 没有数据重新检查 return r, vcode, uin = eval(data.strip().rstrip(";")) logging.info("R:{0} vcode:{1} uin:{2}".format(r, vcode, uin)) if int(r) == 0: logging.info("验证码检查完毕, 不需要验证码") password = self.handle_pwd(r, vcode, uin) self.check_code = vcode self.clean() self.before_login(password) else: logging.warn("验证码检查完毕, 需要验证码") self.get_check_img(r, vcode, uin) self.require_check = True def before_login(self, password, callback=None): """ 登录之前的操作 url: https://ssl.ptlogin2.qq.com/login 方法: GET 参数: { u // qq号码 p // 经过处理的密码 verifycode // 验证码 webqq_type // 固定为10 remember_uin // 是否记住qq号, 传1 即可 login2qq // 登录qq, 传1 aid // appid 固定为 1003903 u1 // 固定为 http://www.qq.com h // 固定为1 ptrediect // 固定为0 ptlang // 固定为2052 from_ui // 固定为 1 pttype // 固定为1 dumy // 固定为空 fp // 固定为loginerroralert ( 重要) mibao_css // 固定为 m_webqq t // 固定为1 g // 固定为 js_type // 固定为0 js_ver // 固定为10021 其他: 如果check步骤验证了需要验证码, 需加上 Referer头 值为: https://ui.ptlogin2.qq.com/cgi-bin/login?target=self&style=5&mibao_css=m_webqq&appid=1003903&enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fweb.qq.com%2Floginproxy.html&f_url=loginerroralert&strong_login=1&login_state=10&t=20130221001 接口返回: ptuiCB('0','0','http://www.qq.com','0','登录成功!', 'nickname'); 先检查是否需要验证码,不需要验证码则首先执行一次登录 然后获取Cookie里的ptwebqq保存在实例里,供后面的接口调用 """ with open("lock", "w"): pass if callback: self.status_callback = callback url = "https://ssl.ptlogin2.qq.com/login" params = [ ("u", self.qid), ("p", password), ("verifycode", self.check_code), ("webqq_type", 10), ("remember_uin", 1), ("login2qq", 1), ("aid", self.aid), ("u1", "http://www.qq.com/loginproxy.h" "tml?login2qq=1&webqq_type=10"), ("h", 1), ("action", 4 - 5 - 8246), ("ptredirect", 0), ("ptlang", 2052), ("from_ui", 1), ("daid", self.daid), ("pttype", 1), ("dumy", ""), ("fp", "loginerroralert"), ("mibao_css", "m_webqq"), ("t", 1), ("g", 1), ("js_type", 0), ("js_ver", 10040), ("login_sig", self.login_sig), ] headers = {} if self.require_check: headers.update( Referer="https://ui.ptlogin2.qq.com/cgi-" "bin/login?target=self&style=5&mibao_css=m_" "webqq&appid=1003903&enable_qlogin=0&no_ver" "ifyimg=1&s_url=http%3A%2F%2Fweb.qq.com%2Fl" "oginproxy.html&f_url=loginerroralert&stron" "g_login=1&login_state=10&t=20130221001" ) else: headers.update( Referfer="https://ui.ptlogin2.qq.com/cgi-bin/login?daid=164&target=self&style=5&mibao_css=m_webqq&appid=1003903&enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2F%2Fweb2.qq.com%2Floginproxy.html&f_url=loginerroralert&strong_login=1&login_state=10&t=20130903001" ) logging.info("检查完毕, 开始登录前准备") self.http.get(url, params, headers=headers, callback=self.login0) def login0(self, resp): logging.info("开始登录前准备...") blogin_data = resp.body.decode("utf-8").strip().rstrip(";") eval("self." + blogin_data) try: location1 = re.findall(r"ptuiCB\(\'0\'\,\'0\'\,\'(.*)\'\,\'0\'\,", blogin_data)[0] params = [] header = { "Referer": "https://ui.ptlogin2.qq.com/cgi-bin/login?d" "aid=164&target=self&style=5&mibao_css=m_webqq&appid=1" "003903&enable_qlogin=0&no_verifyimg=1&s_url=http%3A%2" "F%2Fweb2.qq.com%2Floginproxy.html&f_url=loginerrorale" "rt&strong_login=1&login_state=10&t=20130723001" } self.http.get(location1, params, headers=header, callback=self.get_location1) except: pass def get_location1(self, resp): if os.path.exists(self.checkimg_path): os.remove(self.checkimg_path) if os.path.exists("lock"): os.remove("lock") logging.info("准备完毕, 开始登录") self.login() def login(self): """ 获取登录前的数据, 并进行登录 url: http://d.web2.qq.com/channel/login2 方法: POST 参数: { r : { status // 登录后的状态 ("online") ptwebqq // 上次请求返回的cookie passwd_sig // 固定为空 clientid // 随机的clientid psessionid // 传递 null } clientid // 客户端id psessionid // 传递null } 其他: 需加上 Referer和 Origin 头: "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3" "Origin": "http://d.web2.qq.com" 返回: {u'retcode': 0, u'result': { 'status': 'online', 'index': 1075, 'psessionid': '', u'user_state': 0, u'f': 0, u'uin': 1685359365, u'cip': 3673277226, u'vfwebqq': u'', u'port': 43332}} 保存result中的psessionid和vfwebqq供后面接口调用 """ # time.sleep(4) url = "http://d.web2.qq.com/channel/login2" params = [ ( "r", '{"status":"online","ptwebqq":"%s","passwd_sig":"",' '"clientid":"%d","psessionid":null}' % (self.ptwebqq, self.clientid), ), ("clientid", self.clientid), ("psessionid", "null"), ] headers = {"Origin": "http://d.web2.qq.com"} headers.update(self.base_header) logging.info("登录准备完毕, 开始登录") self.http.post(url, params, headers=headers, callback=self.login_back) def login_back(self, resp): self.require_check_time = None if not resp.body: logging.error(u"没有获取到数据, 登录失败") if self.status_callback: self.status_callback(False, "登录失败 没有数据返回") return self.check() data = json.loads(resp.body) if data.get("retcode") != 0: if self.status_callback: self.status_callback(False, "登录失败 {0}".format(data)) logging.error("登录失败 {0!r}".format(data)) return self.vfwebqq = data.get("result", {}).get("vfwebqq") self.psessionid = data.get("result", {}).get("psessionid") logging.info("登录成功") if not DEBUG and not HTTP_CHECKIMG: aw = "" while aw.lower() not in ["y", "yes", "n", "no"]: aw = raw_input("是否将程序至于后台[y] ") if not aw: aw = "y" if aw in ["y", "yes"]: run_daemon(self.update_friend) return self.update_friend() def _hash(self): """ 获取列表时的Hash """ return _hash.webqq_hash(self.qid, self.ptwebqq) # l = len(e) # # 将qq号码转换成整形列表 # b, k, d = 0, -1, 0 # for d in a: # d = int(d) # b += d # b %= l # f = 0 # if b + 4 > l: # g = 4 + b - l # for h in range(4): # f |= h < g and ( # ord(e[b + h]) & 255) << (3 - h) * 8 or ( # ord(e[h - g]) & 255) << (3 - h) * 8 # else: # for h in range(4): # f |= (ord(e[b + h]) & 255) << (3 - h) * 8 # k ^= f # c = [k >> 24 & 255, k >> 16 & 255, k >> 8 & 255, k & 255] # import string # k = list(string.digits) + ['A', 'B', 'C', 'D', 'E', 'F'] # d = [k[b >> 4 & 15] + k[b & 15] for b in c] # return ''.join(d) def update_friend(self, resp=None, call_status=True): """ 更新好友列表 URL: http://s.web2.qq.com/api/get_user_friends2 METHOD: POST PARAMS: {r:{"h":"hello", "vfwebqq":""}} HEADER: Referer:http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=1 """ url = "http://s.web2.qq.com/api/get_user_friends2" params = [("r", json.dumps({"h": "hello", "hash": self._hash(), "vfwebqq": self.vfwebqq}))] headers = {"Referer": "http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=1"} callback = self.update_friend if resp is None: self.poll_stoped = False # 可以开始轮询消息 self.http.post(url, params, headers=headers, callback=callback) else: if not resp.body: if self.status_callback and call_status: self.status_callback(False, u"更新好友信息失败") return data = json.loads(resp.body) if data.get("retcode") != 0 and call_status: self.status_callback(False, u"好友列表加载失败, 错误代码:{0}".format(data.get("retcode"))) return lst = data.get("result", {}).get("info", []) for info in lst: uin = info.get("uin") self.friend_info[uin] = info marknames = data.get("result", {}).get("marknames", []) [self.mark_to_uin.update({minfo.get("markname"): minfo.get("uin")}) for minfo in marknames] logging.debug("加载好友信息 {0!r}".format(self.friend_info)) logging.info(data) if self.status_callback and call_status: self.status_callback(True) self.update_group() self.http.post( url, params, headers=self.base_header, delay=3600, callback=callback, kwargs={"call_status": False} ) def update_group(self, resp=None): """ 获取组列表, 并获取组成员 获取组列表: url: http://s.web2.qq.com/api/get_group_name_list_mask2 method: POST params: { r : { vfwebqq // 登录前返回的cookie值 } } """ logging.info("获取群列表") url = "http://s.web2.qq.com/api/get_group_name_list_mask2" params = [("r", '{"vfwebqq":"%s"}' % self.vfwebqq)] headers = {"Origin": "http://s.web2.qq.com"} headers.update(self.base_header) self.http.post(url, params, headers=headers, callback=self.group_members) def group_members(self, resp): """ 获取群列表, 获取群列表中的成员 url: http://s.web2.qq.com/api/get_group_info_ext2 method: GET params: { gcode // 群代码 vfwebqq // 登录前的cookie值 t // int(time.time()) } headers: "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3" """ logging.info("加载组成员信息") data = json.loads(resp.body) logging.debug(u"群信息 {0!r}".format(data)) group_list = data.get("result", {}).get("gnamelist", []) logging.debug(u"群列表: {0!r}".format(group_list)) if not group_list and not self.poll_and_heart: self.heartbeat(0) self.poll() for i, group in enumerate(group_list): gcode = group.get("code") url = "http://s.web2.qq.com/api/get_group_info_ext2" params = [("gcode", gcode), ("vfwebqq", self.vfwebqq), ("cb", "undefined"), ("t", int(time.time()))] kwargs = dict(gcode=gcode) if i == 0: kwargs.update(poll=True) self.http.get( url, params, headers=self.base_header, callback=self.do_group_members, kwargs=kwargs, delay=i * 300 ) self.group_info[gcode] = group def do_group_members(self, resp, gcode, poll=False): """ 获取群成员数据 """ data = json.loads(resp.body) logging.debug(u"获取群成员信息 {0!r}".format(data)) members = data.get("result", {}).get("minfo", []) self.group_members_info[gcode] = {} for m in members: uin = m.get("uin") self.group_members_info[gcode][uin] = m cards = data.get("result", {}).get("cards", []) for card in cards: uin = card.get("muin") group_name = card.get("card") self.group_members_info[gcode][uin]["nick"] = group_name logging.debug(u"群成员信息: {0!r}".format(self.group_members_info)) if poll and not self.poll_and_heart: logging.info("开始拉取信息和心跳") self.login_time = time.time() self.poll() self.heartbeat(0) def poll(self): """ 建立长连接获取消息 url:http://d.web2.qq.com/channel/poll2 方法: POST 参数: { r:{ clientid // 客户端id psessionid // session id key // 固定为0 ids // 固定为 [] } clientid psessionid } 头部: "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=2" """ if not self.poll_and_heart: self.poll_and_heart = True url = "http://d.web2.qq.com/channel/poll2" rdic = {"clientid": self.clientid, "psessionid": self.psessionid, "key": 0, "ids": []} params = [("r", json.dumps(rdic)), ("clientid", self.clientid), ("psessionid", self.psessionid)] headers = { "Referer": "https://d.web2.qq.com/cfproxy.html?v=20110331002&callback=1", "Origin": "http://d.web2.qq.com", } self.http.post( url, params, headers=headers, request_timeout=60.0, connect_timeout=60.0, callback=self.handle_msg ) def handle_msg(self, resp): """ 处理消息 """ if self.poll_stoped: return self.poll() if not resp.body: return data = resp.body try: msg = json.loads(data) if msg.get("retcode") in [121, 100006]: logging.error(u"获取消息异常 {0!r}".format(data)) exit() logging.info(u"获取消息: {0!r}".format(msg)) self.msg_dispatch.dispatch(msg) except ValueError: if DEBUG: traceback.print_exc() logging.error(u"消息加载失败: %s", data) def heartbeat(self, delay=60): """ 开始心跳 url:http://web.qq.com/web2/get_msg_tip 方法: GET 参数: { uin // 固定为空 tp // 固定为1 rc // 固定为1 id // 固定位0 lv // 固定为2 t // 开始的心跳时间(int(time.time()) * 1000) } """ if not self.poll_and_heart: self.poll_and_heart = True self.hThread = threading.Thread(name="heartThead#1", target=self._heartbeat) self.hThread.setDaemon(True) self.hThread.start() def _heartbeat(self): i = self.rc url = "http://web.qq.com/web2/get_msg_tip" params = dict( [("uin", ""), ("tp", 1), ("id", 0), ("retype", 1), ("rc", i), ("lv", 3), ("t", int(time.time() * 1000))] ) def callback(resp): logging.info("心跳..") while True: try: self.http.get(url, params, callback=callback, connect_timeout=1.0, request_timeout=1.0) except: pass i += 1 params["rc"] = i time.sleep(60) def make_msg_content(self, content): """ 构造QQ消息的内容 """ self.msg_id += 1 return json.dumps([content, ["font", {"name": "Monospace", "size": 10, "style": [0, 0, 0], "color": "000000"}]]) def get_sess_group_sig(self, to_uin, callback): """ 获取临时消息组签名 URL: http://d.web2.qq.com/channel/get_c2cmsg_sig2 METHOD: GET PARAMS: id // 请求ID 固定为833193360 to_uin // 消息接受人uin( 消息的from_uin) service_type // 固定为0 clientid // 客户端id psessionid // session id t // 当前时间秒1370671760656 HEADERS: Referer:http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3 """ url = "http://d.web2.qq.com/channel/get_c2cmsg_sig2" params = ( ("id", 833193360), ("to_uin", to_uin), ("service_type", 0), ("clientid", self.clientid), ("psessionid", self.psessionid), ("t", time.time()), ) def callback(resp): data = resp.body r = json.loads(data) result = r.get("result") group_sig = result.get("value") if r.get("retcode") != 0: logging.warn(u"加载临时消息签名失败: {0}".format(group_sig)) return try: logging.info("加载临时消息签名 {0} for {1}".format(group_sig, to_uin)) except UnicodeError: return self.group_sig[to_uin] = group_sig callback() self.http.get(url, params, callback=callback, headers=self.base_header) def send_sess_msg(self, to_uin, content): """ 发送临时消息 URL:http://d.web2.qq.com/channel/send_sess_msg2 METHOD: POST PARAMS: r:{ to // 消息接收人 uin group_sig // 组签名 face // 固定为 564, content // 发送内容 msg_id // 消息id service_type // 固定为0, clientid // 客户端id psessionid // sessionid } clientid // 客户端id psessionid // sessionid Headers: self.base_header """ group_sig = self.group_sig.get(to_uin) if not group_sig: callback = partial(self.send_sess_msg, to_uin, content) return self.get_sess_group_sig(to_uin, callback) logging.info(u"发送临时消息 {0} 到 {1}".format(content, to_uin)) delay, n = self.get_delay(content) content = self.make_msg_content(content) url = "http://d.web2.qq.com/channel/send_sess_msg2" params = ( ( "r", json.dumps( { "to": to_uin, "group_sig": group_sig, "face": 564, "content": content, "msg_id": self.msg_id, "service_type": 0, "clientid": self.clientid, "psessionid": self.psessionid, } ), ), ("clientid", self.clientid), ("psessionid", self.psessionid), ) def callback(resp): self.last_msg_numbers -= n self.last_msg_time = time.time() self.http.post(url, params, headers=self.base_header, callback=callback, delay=delay) def send_buddy_msg(self, to_uin, content, callback=None): """ 发送好友消息 URL: http://d.web2.qq.com/channel/send_buddy_msg2 METHOD: POST PARAMS: { "r":{ "to" // 好友uin "face" // 固定为564 "content" // 发送内容 "msg_id" // 消息id, 每发一条递增 "clientid" // 客户端id "psessionid" // sessionid } "clientid":clientid, "psessionid": psessionid, } HEADERS: Referer:http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=3 """ content = self.make_msg_content(content) url = "http://d.web2.qq.com/channel/send_buddy_msg2" r = { "to": to_uin, "face": 564, "content": content, "clientid": self.clientid, "msg_id": self.msg_id, "psessionid": self.psessionid, } params = [("r", json.dumps(r)), ("clientid", self.clientid), ("psessionid", self.psessionid)] headers = {"Origin": "http://d.web2.qq.com"} headers.update(self.base_header) delay, n = self.get_delay(content) def _callback(resp): logging.info(u"发送好友消息 {0} 给 {1} 成功".format(content, to_uin)) if callback: callback(True) self.last_msg_numbers -= n self.last_msg_time = time.time() logging.info(u"发送好友消息 {0} 给 {1} ...".format(content, to_uin)) self.http.post(url, params, headers=headers, delay=delay, callback=_callback) def send_group_msg(self, group_uin, content): """ 发送群消息 url:http://d.web2.qq.com/channel/send_qun_msg2 方法: POST 参数: { r:{ group_uin // gid content // 发送内容 msg_id // 消息id, 每次发送消息应该递增 clientid // 客户端id psessionid // sessionid } clientid psessionid } """ gid = self.group_info.get(group_uin, {}).get("gid") source = content content = self.make_msg_content(source) url = "http://d.web2.qq.com/channel/send_qun_msg2" r = { "group_uin": gid, "content": content, "msg_id": self.msg_id, "clientid": self.clientid, "psessionid": self.psessionid, } params = [("r", json.dumps(r)), ("psessionid", self.psessionid), ("clientid", self.clientid)] print params delay, n = self.get_delay(content) callback = self.send_group_msg_back logging.info(u"发送群消息 {0} 到 {1}...".format(content, group_uin)) headers = { "Origin": "http://d.web2.qq.com", "Referer": "http://d.web2.qq.com/proxy.html?v=20110331002&callback=1&id=2", } self.http.post(url, params, headers=headers, callback=callback, args=(source, group_uin, n), delay=delay) def get_delay(self, content): MIN = MESSAGE_INTERVAL delay = 0 sub = time.time() - self.last_msg_time if self.last_msg_numbers < 0: self.last_msg_numbers = 0 # 不足最小间隔就补足最小间隔 if sub < MIN: delay = MIN logging.debug(u"间隔 %s 小于 %s, 设置延迟为%s", sub, MIN, delay) # 如果间隔是已有消息间隔的2倍, 则清除已有消息数 # print "sub", sub, "n:", self.last_msg_numbers if self.last_msg_numbers > 0 and sub / (MIN * self.last_msg_numbers) > 1: self.last_msg_numbers = 0 # 如果还有消息未发送, 则加上他们的间隔 if self.last_msg_numbers > 0: delay += MIN * self.last_msg_numbers logging.info(u"有%s条消息未发送, 延迟为 %s", self.last_msg_numbers, delay) n = 1 # 如果这条消息和上条消息一致, 保险起见再加上一个最小间隔 if self.last_msg_content == content and sub < MIN: delay += MIN self.last_msg_numbers += 1 n = 2 self.last_msg_numbers += 1 self.last_msg_content = content if delay: logging.info(u"有 {1} 个消息未投递将会在 {0} 秒后投递".format(delay, self.last_msg_numbers)) # 返回消息累加个数, 在消息发送后减去相应的数目 return delay, n def send_group_msg_back(self, content, group_uin, n, resp): logging.info(u"发送群消息 {0} 到 {1} 成功".format(content, group_uin)) self.last_msg_time = time.time() if self.last_msg_numbers > 0: self.last_msg_numbers -= n def set_signature(self, signature, password, callback): """ 设置QQ签名, 可以通过发送好友消息设置签名, 消息应按照如下格式: 设置签名:[密码]|[签名内容] // 密码和签名内容不能包含分割符 url: http://s.web2.qq.com/api/set_long_nick2 method: POST params: r : { nlk // 签名内容 vfwebqq // 登录时获取的cookie值 } headers: Referer:http://s.web2.qq.com/proxy.html?v=20110412001&callback=1&id=1 """ if password != Set_Password: return callback(u"你没有权限这么做") logging.info(u"设置QQ签名 {0}".format(signature)) url = "http://s.web2.qq.com/api/set_long_nick2" params = (("r", json.dumps({"nlk": signature, "vfwebqq": self.vfwebqq})),) headers = {"Origin": "http://s.web2.qq.com"} headers.update(self.base_header) def callback(resp): data = resp.body print data result = json.loads(data).get("retcode") if result == 0: callback(u"设置成功") else: callback(u"设置失败") self.http.post(url, params, headers=headers, callback=callback) def run(self): self.get_login_sig() self.http.start() def real_stop(self): self.http.stop() def stop(self): self.stop_poll = True def send_msg_with_markname(self, markname, message, callback): """ 使用备注发送消息 """ uin = self.mark_to_uin.get(markname) if not uin: return False self.send_buddy_msg(uin, message, callback) return True def accept_and_set_mark(self, uin, qq_num): """ 确认添加并更改备注 """ url = "http://s.web2.qq.com/api/allow_and_add2" params = [("r", '{"account":%d, "gid":0, "mname":"%d",' ' "vfwebqq":"%s"}' % (qq_num, qq_num, self.vfwebqq))] headers = {"Origin": "http://s.web2.qq.com"} headers.update(self.base_header) def _callback(resp): data = json.loads(resp.body) logging.info(data) logging.info(params) if data.get("retcode") == 0: logging.info(u"添加 {0} 成功".format(qq_num)) self.mark_to_uin[uin] = qq_num else: logging.info(u"添加 {0} 失败".format(qq_num)) self.http.post(url, params, headers=headers, callback=_callback)
class QXBot(EventHandler, XMPPFeatureHandler): def __init__(self): my_jid = JID(USER + '/Bot') self.my_jid = my_jid settings = XMPPSettings({ "software_name": "qxbot", "software_version": __version__, "software_os": "Linux", "tls_verify_peer": False, "starttls": True, "ipv6": False, "poll_interval": 10, }) settings["password"] = PASSWORD version_provider = VersionProvider(settings) event_queue = settings["event_queue"] self.webqq = WebQQ(QQ, event_queue) self.connected = False #self.mainloop = TornadoMainLoop(settings) self.mainloop = EpollMainLoop(settings) self.client = Client(my_jid, [self, version_provider], settings, self.mainloop) self.logger = get_logger() self.msg_dispatch = MessageDispatch(self, self.webqq, BRIDGES) self.xmpp_msg_queue = Queue.Queue() def run(self, timeout=None): self.client.connect() self.client.run(timeout) def disconnect(self): self.client.disconnect() while True: try: self.run(2) except: pass else: break @presence_stanza_handler("subscribe") def handle_presence_subscribe(self, stanza): self.logger.info(u"{0} join us".format(stanza.from_jid)) return stanza.make_accept_response() @presence_stanza_handler("subscribed") def handle_presence_subscribed(self, stanza): self.logger.info(u"{0!r} accepted our subscription request".format( stanza.from_jid)) return stanza.make_accept_response() @presence_stanza_handler("unsubscribe") def handle_presence_unsubscribe(self, stanza): self.logger.info(u"{0} canceled presence subscription".format( stanza.from_jid)) return stanza.make_accept_response() @presence_stanza_handler("unsubscribed") def handle_presence_unsubscribed(self, stanza): self.logger.info( u"{0!r} acknowledged our subscrption cancelation".format( stanza.from_jid)) @presence_stanza_handler(None) def handle_presence_available(self, stanza): self.logger.info(r"{0} has been online".format(stanza.from_jid)) @presence_stanza_handler("unavailable") def handle_presence_unavailable(self, stanza): self.logger.info(r"{0} has been offline".format(stanza.from_jid)) @message_stanza_handler() def handle_message(self, stanza): if self.webqq.connected: self.msg_dispatch.dispatch_xmpp(stanza) else: self.xmpp_msg_queue.put(stanza) @event_handler(DisconnectedEvent) def handle_disconnected(self, event): return QUIT @event_handler(ConnectedEvent) def handle_connected(self, event): pass @event_handler(RosterReceivedEvent) def handle_roster_received(self, event): """ 此处代表xmpp已经连接 开始连接QQ, 先将检查是否需要验证码的handler加入到mainloop """ checkhandler = CheckHandler(self.webqq) self.mainloop.add_handler(checkhandler) self.connected = True @event_handler(CheckedEvent) def handle_webqq_checked(self, event): """ 第一步已经完毕, 删除掉检查的handler, 将登录前handler加入mainloop""" bloginhandler = BeforeLoginHandler(self.webqq, password=QQ_PWD) self.mainloop.remove_handler(event.handler) self.mainloop.add_handler(bloginhandler) @event_handler(BeforeLoginEvent) def handle_webqq_blogin(self, event): """ 登录前完毕开始真正的登录 """ loginhandler = LoginHandler(self.webqq) self.mainloop.remove_handler(event.handler) self.mainloop.add_handler(loginhandler) @event_handler(WebQQLoginedEvent) def handle_webqq_logined(self, event): """ 登录后将获取群列表的handler放入mainloop """ self.mainloop.remove_handler(event.handler) self.mainloop.add_handler(GroupListHandler(self.webqq)) @event_handler(GroupListEvent) def handle_webqq_group_list(self, event): """ 获取群列表后""" self.mainloop.remove_handler(event.handler) data = event.data group_map = {} if data.get("retcode") == 0: group_list = data.get("result", {}).get("gnamelist", []) for group in group_list: gcode = group.get("code") group_map[gcode] = group self.webqq.group_map = group_map self.webqq.group_lst_updated = False # 开放添加GroupListHandler i = 1 for gcode in group_map: if i == len(group_map): self.mainloop.add_handler( GroupMembersHandler(self.webqq, gcode=gcode, done=True)) else: self.mainloop.add_handler( GroupMembersHandler(self.webqq, gcode=gcode, done=False)) i += 1 @event_handler(GroupMembersEvent) def handle_group_members(self, event): """ 获取所有群成员 """ self.mainloop.remove_handler(event.handler) members = event.data.get("result", {}).get("minfo", []) self.webqq.group_m_map[event.gcode] = {} for m in members: uin = m.get("uin") self.webqq.group_m_map[event.gcode][uin] = m cards = event.data.get("result", {}).get("cards", []) for card in cards: uin = card.get("muin") group_name = card.get("card") self.webqq.group_m_map[event.gcode][uin]["nick"] = group_name # 防止重复添加GroupListHandler if not self.webqq.group_lst_updated: self.webqq.group_lst_updated = True self.mainloop.add_handler(GroupListHandler(self.webqq, delay=300)) @event_handler(WebQQRosterUpdatedEvent) def handle_webqq_roster(self, event): """ 群成员都获取完毕后开启,Poll获取消息和心跳 """ self.mainloop.remove_handler(event.handler) self.msg_dispatch.get_map() if not self.webqq.polled: self.webqq.polled = True self.mainloop.add_handler(PollHandler(self.webqq)) if not self.webqq.heartbeated: self.webqq.heartbeated = True hb = HeartbeatHandler(self.webqq) self.mainloop.add_handler(hb) while True: try: stanza = self.xmpp_msg_queue.get_nowait() self.msg_dispatch.dispatch_xmpp(stanza) except Queue.Empty: break self.webqq.connected = True @event_handler(WebQQHeartbeatEvent) def handle_webqq_hb(self, event): """ 心跳完毕后, 延迟60秒在此触发此事件 重复心跳 """ self.mainloop.remove_handler(event.handler) self.mainloop.add_handler(HeartbeatHandler(self.webqq, delay=60)) @event_handler(WebQQPollEvent) def handle_webqq_poll(self, event): """ 延迟1秒重复触发此事件, 轮询获取消息 """ self.mainloop.remove_handler(event.handler) self.mainloop.add_handler(PollHandler(self.webqq)) @event_handler(WebQQMessageEvent) def handle_webqq_msg(self, event): """ 有消息到达, 处理消息 """ self.msg_dispatch.dispatch_qq(event.message) @event_handler(RetryEvent) def handle_retry(self, event): """ 有handler触发异常, 需重试 """ self.mainloop.remove_handler(event.handler) handler = event.cls(self.webqq, event.req, *event.args, **event.kwargs) self.mainloop.add_handler(handler) @event_handler(RemoveEvent) def handle_remove(self, event): """ 触发此事件, 移除handler """ self.mainloop.remove_handler(event.handler) def send_qq_group_msg(self, group_uin, content): """ 发送qq群消息 """ handler = GroupMsgHandler(self.webqq, group_uin=group_uin, content=content) self.mainloop.add_handler(handler) @property def roster(self): return self.client.roster @property def stream(self): return self.client.stream @event_handler() def handle_all(self, event): self.logger.info(u"-- {0}".format(event)) def make_message(self, to, typ, body): """ 构造消息 `to` - 接收人 JID `typ` - 消息类型 `body` - 消息主体 """ if typ not in ['normal', 'chat', 'groupchat', 'headline']: typ = 'chat' m = Message(from_jid=self.my_jid, to_jid=to, stanza_type=typ, body=body) return m def send_msg(self, to, body): if not isinstance(to, JID): to = JID(to) msg = self.make_message(to, 'chat', body) self.stream.send(msg)