예제 #1
0
파일: bot.py 프로젝트: GeorgeLex/sketal
class Bot:
    __slots__ = ("logger", "loop", "values", "server", "main_task",
                 "longpoll_request", "settings", "api", "handler",
                 "logger_file")

    def __init__(self,
                 settings,
                 logger=None,
                 handler=None,
                 loop=asyncio.get_event_loop()):
        self.logger = None
        self.init_logger(logger)

        self.logger.info("Initializing bot")

        self.loop = loop

        self.values = {}
        self.server = ""
        self.longpoll_request = None

        self.main_task = None
        self.settings = settings

        self.logger.info("Initializing vk clients")
        self.api = VkController(settings, logger=self.logger)

        self.logger.info("Loading plugins")
        if handler:
            self.handler = handler

        else:
            self.handler = MessageHandler(self,
                                          self.api,
                                          initiate_plugins=False)
            self.handler.initiate_plugins()

        signal.signal(signal.SIGINT, lambda x, y: self.stop_bot(True))

        self.logger.info("Bot succesfully initialized")

    def init_logger(self, logger):
        if not logger:
            logger = logging.Logger("sketal", level=logging.INFO)

        formatter = logging.Formatter(
            fmt=u'%(filename)-10s [%(asctime)s] %(levelname)-8s: %(message)s',
            datefmt='%y.%m.%d %H:%M:%S')

        file_handler = logging.FileHandler('logs.txt')
        file_handler.setLevel(logging.DEBUG)
        file_handler.setFormatter(formatter)

        self.logger_file = file_handler

        stream_handler = logging.StreamHandler()
        stream_handler.setLevel(logging.INFO)
        stream_handler.setFormatter(formatter)

        logger.addHandler(file_handler)
        logger.addHandler(stream_handler)

        self.logger = logger

    async def init_long_polling(self, update=0):
        result = None
        retries = 10
        for x in range(retries):
            result = await self.api(sender=self.api.target_client
                                    ).messages.getLongPollServer(use_ssl=1,
                                                                 lp_version=2)

            if result:
                break

            time.sleep(0.5)

        if not result:
            self.logger.error("Unable to connect to VK's long polling server")
            exit()

        last_ts = 0
        longpoll_key = ""

        if 'ts' in self.values:
            last_ts = self.values['ts']

        if 'key' in self.values:
            longpoll_key = self.values['key']

        if update == 0:
            self.server = "https://" + result['server']
            longpoll_key = result['key']
            last_ts = result['ts']

        elif update == 3:
            longpoll_key = result['key']
            last_ts = result['ts']

        elif update == 2:
            longpoll_key = result['key']

        self.values = {
            'act': 'a_check',
            'key': longpoll_key,
            'ts': last_ts,
            'wait': 20,
            'mode': 10,
            'version': 2
        }

    async def process_longpoll_event(self, new_event):
        if not new_event:
            return

        event_id = new_event[0]

        if event_id != 4:
            evnt = LongpollEvent(self.api, event_id, new_event)

            return await self.process_event(evnt)

        data = MessageEventData()
        data.msg_id = new_event[1]
        data.attaches = new_event[6]
        data.time = int(new_event[4])

        try:
            data.user_id = int(data.attaches['from'])
            data.chat_id = int(new_event[3]) - 2000000000
            data.is_multichat = True

            del data.attaches['from']

        except KeyError:
            data.user_id = int(new_event[3])
            data.is_multichat = False

        # https://vk.com/dev/using_longpoll_2
        flags = parse_msg_flags(new_event[2])

        if flags['outbox']:
            if not self.settings.READ_OUT:
                return

            data.is_out = True

        data.full_text = new_event[5].replace('<br>', '\n')

        if "fwd" in data.attaches:
            data.forwarded = MessageEventData.parse_brief_forwarded_messages_from_lp(
                data.attaches["fwd"])
            del data.attaches["fwd"]

        else:
            data.forwarded = []

        msg = Message(self.api, data)

        if await self.check_event(data.user_id, data.chat_id, data.attaches):
            msg.is_event = True

        await self.process_message(msg)

    async def longpoll_processor(self):
        await self.init_long_polling()

        session = aiohttp.ClientSession(loop=self.loop)

        while True:
            try:
                self.longpoll_request = session.get(self.server,
                                                    params=self.values)

                resp = await self.longpoll_request

            except aiohttp.ClientOSError:
                session = aiohttp.ClientSession(loop=self.loop)

            except (asyncio.TimeoutError, aiohttp.ServerDisconnectedError):
                self.logger.warning(
                    "Long polling server doesn't respond. Changing server")
                await self.init_long_polling()
                continue

            try:
                events = json.loads(await resp.text())
            except ValueError:
                continue

            failed = events.get('failed')

            if failed:
                err_num = int(failed)

                if err_num == 1:  # 1 - update timestamp
                    self.values['ts'] = events['ts']

                elif err_num in (2, 3):  # 2, 3 - new data for long polling
                    await self.init_long_polling(err_num)

                continue

            self.values['ts'] = events['ts']
            for event in events['updates']:
                asyncio.ensure_future(self.process_longpoll_event(event))

    async def callback_processor(self, request):
        try:
            data = await request.json()

        except (UnicodeDecodeError, json.decoder.JSONDecodeError):
            return web.Response(text="ok")

        data_type = data["type"]

        if data_type == "confirmation":
            return web.Response(text=self.settings.CONF_CODE)

        obj = data["object"]

        if "user_id" in obj:
            obj['user_id'] = int(obj['user_id'])

        if data_type == 'message_new':
            data = MessageEventData.from_message_body(obj)

            msg = Message(self.api, data)

            await self.process_message(msg)

        else:
            evnt = CallbackEvent(self.api, data_type, obj)
            await self.process_event(evnt)

        return web.Response(text="ok")

    def longpoll_run(self, custom_process=False):
        self.main_task = Task(self.longpoll_processor())

        if custom_process:
            return self.main_task

        self.logger.info("Started to process messages")

        try:
            self.loop.run_until_complete(self.main_task)

        except (KeyboardInterrupt, SystemExit):
            self.stop()

            self.logger.info("Stopped to process messages")

        except asyncio.CancelledError:
            pass

    def callback_run(self, custom_process=False):
        host = getenv('IP', '0.0.0.0')
        port = int(getenv('PORT', 8000))

        self.logger.info("Started to process messages")

        try:
            server_generator, handler, app = self.loop.run_until_complete(
                self.init_app(host, port, self.loop))
            server = self.loop.run_until_complete(server_generator)
        except OSError:
            self.logger.error("Address already in use: " + str(host) + ":" +
                              str(port))
            return

        self.main_task = Future()

        if custom_process:
            return self.main_task

        print("======== Running on http://{}:{} ========\n"
              "         (Press CTRL+C to quit)".format(
                  *server.sockets[0].getsockname()))

        def stop_server():
            server.close()

            if not self.loop.is_running():
                return

            self.loop.run_until_complete(server.wait_closed())
            self.loop.run_until_complete(app.shutdown())
            self.loop.run_until_complete(handler.shutdown(10))
            self.loop.run_until_complete(app.cleanup())

        try:
            self.loop.run_until_complete(self.main_task)
        except KeyboardInterrupt:
            self.stop()

            stop_server()

            self.loop.close()

        except asyncio.CancelledError:
            pass

        finally:
            stop_server()

            self.logger.info("Stopped to process messages")

    async def init_app(self, host, port, loop):
        app = web.Application()
        app.router.add_post('/', self.callback_processor)

        handler = app.make_handler()

        server_generator = loop.create_server(handler, host, port)
        return server_generator, handler, app

    def stop_bot(self, full=False):
        try:
            self.main_task.cancel()

        except:
            pass

        if full:
            self.stop()
            self.loop.stop()

        self.logger.info("Attempting to turn bot off")

    async def process_message(self, msg):
        asyncio.ensure_future(self.handler.process(msg), loop=self.loop)

    async def check_event(self, user_id, chat_id, attaches):
        if chat_id != 0 and "source_act" in attaches:
            photo = attaches.get("attach1_type") + attaches.get(
                "attach1") if "attach1" in attaches else None

            evnt = ChatChangeEvent(self.api, user_id, chat_id,
                                   attaches.get("source_act"),
                                   int(attaches.get("source_mid", 0)),
                                   attaches.get("source_text"),
                                   attaches.get("source_old_text"), photo,
                                   int(attaches.get("from", 0)))

            await self.process_event(evnt)

            return True

        return False

    async def process_event(self, evnt):
        asyncio.ensure_future(self.handler.process_event(evnt), loop=self.loop)

    def do(self, coroutine):
        if asyncio.iscoroutine(coroutine):
            return self.loop.run_until_complete(coroutine)

        return False

    @staticmethod
    def silent(func):
        try:
            func()
        except:
            pass

    def stop(self):
        self.handler.stop()
        self.api.stop()

        self.silent(self.main_task.cancel)

        self.logger.removeHandler(self.logger_file)
        self.logger_file.close()
예제 #2
0
파일: bot.py 프로젝트: n4sty001/n4styvkbot
class Bot:
    __slots__ = ("api", "handler", "logger", "logger_file", "loop", "tasks",
                 "sessions", "requests", "settings")

    def __init__(self, settings, logger=None, handler=None, loop=None):
        self.settings = settings

        if logger:
            self.logger = logger
        else:
            self.logger = self.init_logger()

        if loop:
            self.loop = loop
        else:
            self.loop = asyncio.get_event_loop()

        self.logger.info("Initializing bot")

        self.requests = []
        self.sessions = []
        self.tasks = []

        self.logger.info("Initializing vk clients")
        self.api = VkController(settings, logger=self.logger, loop=self.loop)

        self.logger.info("Loading plugins")
        if handler:
            self.handler = handler

        else:
            self.handler = MessageHandler(self,
                                          self.api,
                                          initiate_plugins=False)
            self.handler.initiate_plugins()

        self.logger.info("Bot successfully initialized")

    def init_logger(self):
        logger = logging.Logger(
            "sketal",
            level=logging.DEBUG if self.settings.DEBUG else logging.INFO)

        formatter = logging.Formatter(
            fmt=u'[%(asctime)s] %(levelname)-8s: %(message)s',
            datefmt='%y.%m.%d %H:%M:%S')

        file_handler = logging.FileHandler('logs.txt')
        file_handler.setLevel(logging.DEBUG)
        file_handler.setFormatter(formatter)

        self.logger_file = file_handler

        stream_handler = logging.StreamHandler()
        stream_handler.setLevel(
            level=logging.DEBUG if self.settings.DEBUG else logging.INFO)
        stream_handler.setFormatter(formatter)

        logger.addHandler(file_handler)
        logger.addHandler(stream_handler)

        return logger

    def add_task(self, task):
        for ctask in self.tasks[::]:
            if ctask.done() or ctask.cancelled():
                self.tasks.remove(ctask)

        if task.done() or task.cancelled():
            return

        self.tasks.append(task)

        return task

    async def init_long_polling(self, pack, update=0):
        result = None

        for _ in range(4):
            result = await self.api(sender=self.api.target_client). \
                messages.getLongPollServer(use_ssl=1, lp_version=2)

            if result:
                break

            time.sleep(0.5)

        if not result:
            self.logger.error("Unable to connect to VK's long polling server.")
            exit()

        if update == 0:
            pack[1] = "https://" + result['server']
            pack[0]['key'] = result['key']
            pack[0]['ts'] = result['ts']

        elif update == 2:
            pack[0]['key'] = result['key']

        elif update == 3:
            pack[0]['key'] = result['key']
            pack[0]['ts'] = result['ts']

    async def init_bots_long_polling(self, pack, update=0):
        result = None

        for _ in range(4):
            result = await self.api(sender=self.api.target_client).\
                groups.getLongPollServer(group_id=self.api.get_current_id())

            if result:
                break

            time.sleep(0.5)

        if not result:
            self.logger.error(
                "Unable to connect to VK's bots long polling server.")
            exit()

        if update == 0:
            pack[1] = result['server']
            pack[0]['key'] = result['key']
            pack[0]['ts'] = result['ts']

        elif update == 2:
            pack[0]['key'] = result['key']

        elif update == 3:
            pack[0]['key'] = result['key']
            pack[0]['ts'] = result['ts']

    async def process_longpoll_event(self, new_event):
        if not new_event:
            return

        event_id = new_event[0]

        if event_id != 4:
            evnt = LongpollEvent(self.api, event_id, new_event)

            return await self.process_event(evnt)

        data = MessageEventData()
        data.msg_id = new_event[1]
        data.attaches = new_event[6]
        data.time = int(new_event[4])

        if 'from' in data.attaches and len(new_event) > 3:
            data.user_id = int(data.attaches.pop('from'))
            data.chat_id = int(new_event[3]) - 2000000000
            data.is_multichat = True

        else:
            data.user_id = int(new_event[3])
            data.is_multichat = False

        # https://vk.com/dev/using_longpoll_2
        flags = parse_msg_flags(new_event[2])

        if flags['outbox']:
            if not self.settings.READ_OUT:
                return

            data.is_out = True

        data.full_text = new_event[5].replace('<br>', '\n')

        if "fwd" in data.attaches:
            data.forwarded = MessageEventData.\
                parse_brief_forwarded_messages_from_lp(data.attaches.pop("fwd"))

        else:
            data.forwarded = []

        msg = Message(self.api, data)

        if await self.check_event(data.user_id, data.chat_id, data.attaches):
            msg.is_event = True

        await self.process_message(msg)

    async def longpoll_processor(self):
        pack = [{
            'act': 'a_check',
            'key': '',
            'ts': 0,
            'wait': 25,
            'mode': 10,
            'version': 2
        }, ""]

        await self.init_long_polling(pack)

        session = aiohttp.ClientSession(loop=self.loop)
        self.sessions.append(session)

        while True:
            try:
                requ = session.get(pack[1], params=pack[0])
            except aiohttp.ClientOSError:
                await asyncio.sleep(0.5)
                continue

            self.requests.append(requ)

            try:
                events = json.loads(await (await requ).text())

            except aiohttp.ClientOSError:
                try:
                    self.sessions.remove(session)
                except ValueError:
                    pass

                await asyncio.sleep(0.5)

                session = aiohttp.ClientSession(loop=self.loop)
                self.sessions.append(session)
                continue

            except (asyncio.TimeoutError, aiohttp.ServerDisconnectedError):
                self.logger.warning(
                    "Long polling server doesn't respond. Changing server.")

                await asyncio.sleep(0.5)

                await self.init_long_polling(pack)
                continue

            except ValueError:
                await asyncio.sleep(0.5)
                continue

            finally:
                if requ in self.requests:
                    self.requests.remove(requ)

            failed = events.get('failed')

            if failed:
                err_num = int(failed)

                if err_num == 1:  # 1 - update timestamp
                    if 'ts' not in events:
                        await self.init_long_polling(pack)
                    else:
                        pack[0]['ts'] = events['ts']

                elif err_num in (2, 3):  # 2, 3 - new data for long polling
                    await self.init_long_polling(pack, err_num)

                continue

            pack[0]['ts'] = events['ts']

            for event in events['updates']:
                asyncio.ensure_future(self.process_longpoll_event(event))

    async def bots_longpoll_processor(self):
        pack = [{'act': 'a_check', 'key': '', 'ts': 0, 'wait': 25}, ""]

        await self.init_bots_long_polling(pack)

        session = aiohttp.ClientSession(loop=self.loop)
        self.sessions.append(session)

        while True:
            try:
                requ = session.get(pack[1], params=pack[0])
            except aiohttp.ClientOSError:
                await asyncio.sleep(0.5)
                continue

            self.requests.append(requ)

            try:
                events = json.loads(await (await requ).text())

            except aiohttp.ClientOSError:
                try:
                    self.sessions.remove(session)
                except ValueError:
                    pass

                await asyncio.sleep(0.5)

                session = aiohttp.ClientSession(loop=self.loop)
                self.sessions.append(session)
                continue

            except (asyncio.TimeoutError, aiohttp.ServerDisconnectedError):
                self.logger.warning(
                    "Long polling server doesn't respond. Changing server.")
                await asyncio.sleep(0.5)

                await self.init_bots_long_polling(pack)
                continue

            except ValueError:
                await asyncio.sleep(0.5)
                continue

            finally:
                if requ in self.requests:
                    self.requests.remove(requ)

            failed = events.get('failed')

            if failed:
                err_num = int(failed)

                if err_num == 1:  # 1 - update timestamp
                    if 'ts' not in events:
                        await self.init_bots_long_polling(pack)
                    else:
                        pack[0]['ts'] = events['ts']

                elif err_num in (2, 3):  # 2, 3 - new data for long polling
                    await self.init_bots_long_polling(pack, err_num)

                continue

            pack[0]['ts'] = events['ts']

            for event in events['updates']:
                if "type" not in event or "object" not in event:
                    continue

                data_type = event["type"]
                obj = event["object"]

                if "user_id" in obj:
                    obj['user_id'] = int(obj['user_id'])

                if data_type == 'message_new':
                    await self.process_message(
                        Message(self.api,
                                MessageEventData.from_message_body(obj)))

                else:
                    await self.process_event(
                        CallbackEvent(self.api, data_type, obj))

    async def callback_processor(self, request):
        try:
            data = await request.json()

            if "type" not in data or "object" not in data:
                raise ValueError("Damaged data received.")

        except (UnicodeDecodeError, ValueError):
            return web.Response(text="ok")

        data_type = data["type"]

        if data_type == "confirmation":
            return web.Response(text=self.settings.CONF_CODE)

        obj = data["object"]

        if "user_id" in obj:
            obj['user_id'] = int(obj['user_id'])

        if data_type == 'message_new':
            await self.process_message(
                Message(self.api, MessageEventData.from_message_body(obj)))

        else:
            await self.process_event(CallbackEvent(self.api, data_type, obj))

        return web.Response(text="ok")

    def longpoll_run(self, custom_process=False):
        task = self.add_task(Task(self.longpoll_processor()))

        if custom_process:
            return task

        self.logger.info("Started to process messages")

        try:
            self.loop.run_until_complete(task)

        except (KeyboardInterrupt, SystemExit):
            self.loop.run_until_complete(self.stop())

        except asyncio.CancelledError:
            pass

    def bots_longpoll_run(self, custom_process=False):
        task = self.add_task(Task(self.bots_longpoll_processor()))

        if custom_process:
            return task

        self.logger.info("Started to process messages")

        try:
            self.loop.run_until_complete(task)

        except (KeyboardInterrupt, SystemExit):
            self.loop.run_until_complete(self.stop())

        except asyncio.CancelledError:
            pass

    def callback_run(self, custom_process=False):
        host = getenv('IP', '127.0.0.1')
        port = int(getenv('PORT', 8000))

        self.logger.info("Started to process messages")

        try:
            app = web.Application()
            app.router.add_post('/', self.callback_processor)

            runner = web.AppRunner(app)
            self.loop.run_until_complete(runner.setup())

            site = web.TCPSite(runner, host, port)
            self.loop.run_until_complete(site.start())

        except OSError:
            self.logger.error("Address already in use: " + str(host) + ":" +
                              str(port))
            return

        task = self.add_task(Future())

        if custom_process:
            return task

        print("======== Running on http://{}:{} ========\n"
              "         (Press CTRL+C to quit)".format(
                  *server.sockets[0].getsockname()))

        try:
            self.loop.run_until_complete(task)

        except (KeyboardInterrupt, SystemExit):
            self.loop.run_until_complete(runner.cleanup())
            self.loop.run_until_complete(self.stop())

    async def process_message(self, msg):
        asyncio.ensure_future(self.handler.process(msg), loop=self.loop)

    async def check_event(self, user_id, chat_id, attaches):
        if chat_id != 0 and "source_act" in attaches:
            photo = attaches.get("attach1_type") + attaches.get(
                "attach1") if "attach1" in attaches else None

            evnt = ChatChangeEvent(self.api, user_id, chat_id,
                                   attaches.get("source_act"),
                                   int(attaches.get("source_mid", 0)),
                                   attaches.get("source_text"),
                                   attaches.get("source_old_text"), photo,
                                   int(attaches.get("from", 0)))

            await self.process_event(evnt)

            return True

        return False

    async def process_event(self, evnt):
        asyncio.ensure_future(self.handler.process_event(evnt), loop=self.loop)

    def coroutine_exec(self, coroutine):
        if asyncio.iscoroutine(coroutine) or isinstance(
                coroutine, asyncio.Future):
            return self.loop.run_until_complete(coroutine)

        return False

    async def stop_tasks(self):
        self.logger.info("Attempting stop bot")

        for task in self.tasks:
            try:
                task.cancel()
            except Exception:
                pass

        self.logger.info("Stopped to process messages")

    async def stop(self):
        self.logger.info("Attempting to turn bot off")

        for session in self.sessions:
            await session.close()

        await self.handler.stop()
        await self.api.stop()

        for task in self.tasks:
            try:
                task.cancel()
            except Exception:
                pass

        self.logger.removeHandler(self.logger_file)
        self.logger_file.close()

        self.logger.info("Stopped to process messages")