Пример #1
0
    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  # 剩余位发送的消息数量
Пример #2
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()
Пример #3
0
    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"}
Пример #4
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()
Пример #5
0
    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    # 剩余位发送的消息数量
Пример #6
0
    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"
        }
Пример #7
0
    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"}
Пример #8
0
    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
Пример #9
0
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)
Пример #10
0
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()
Пример #11
0
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()
Пример #12
0
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()
Пример #13
0
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)
Пример #14
0
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()
Пример #15
0
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)
Пример #16
0
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)