Exemple #1
0
class Channel(object):
    def __init__(self,
                 parent,
                 host='127.0.0.1',
                 port=6668,
                 name='undefined',
                 topic='undefined'):
        self.notify = Logger(
            'Channel')  # TODO - generate unique hashes for each channel?

        self.parent = parent

        self.host = host
        self.port = port
        self.name = name
        self.topic = topic

    def setup_channel(self):
        self.notify.warning('attempting to establish connection to server...')
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.connect((self.host, self.parent.port))
            s.sendall(bytes([1]))
            try:
                data = s.recv(4096)
                self.notify.debug('received data - {}'.format(data))
                self.handle_data(Packet(data))
            except Exception as e:
                raise Exception(e)

    def handle_data(self, packet):
        if packet.data[0] == 2:
            self.notify.info('successfully established connection to server!')
Exemple #2
0
 def load_from_file(cls, path: str):
     try:
         global_conf = load_from_file(path)
         cls.config.behavior = load_from_file(global_conf['behavior'])
         cls.config.waypoint = load_from_file(global_conf['waypoint'])
     except Exception as e:
         Logger.warning("Unable to load global config: 'global.json'")
Exemple #3
0
class CharacterHistoryService:
    CACHE_GROUP = "history"
    CACHE_MAX_AGE = 86400

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

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

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

        t = int(time.time())

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

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

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

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

    def get_pork_url(self, dimension, char_name):
        return "http://pork.budabot.jkbff.com/pork/history.php?server=%d&name=%s" % (
            dimension, char_name)
class JobScheduler:
    def __init__(self):
        self.logger = Logger("job_scheduler")
        self.jobs = []
        self.job_id_index = 0

    def inject(self, registry):
        pass

    def start(self):
        pass

    def check_for_scheduled_jobs(self, timestamp):
        while self.jobs and self.jobs[0]["time"] <= timestamp:
            try:
                job = self.jobs.pop(0)
                job["callback"](job["time"], *job["args"], **job["kwargs"])
            except Exception as e:
                self.logger.warning("Error processing scheduled job", e)

    def delayed_job(self, callback, delay, *args, **kwargs):
        return self.scheduled_job(callback, int(time.time()) + delay, *args, **kwargs)

    def scheduled_job(self, callback, scheduled_time, *args, **kwargs):
        job_id = self._get_next_job_id()
        new_job = {
            "id": job_id,
            "callback": callback,
            "args": args,
            "kwargs": kwargs,
            "time": scheduled_time
        }

        self._insert_job(new_job)
        return job_id

    def cancel_job(self, job_id):
        for index, job in enumerate(self.jobs):
            if job["id"] == job_id:
                return self.jobs.pop(index)
        return None

    def _insert_job(self, new_job):
        for index, job in enumerate(self.jobs):
            if job["time"] > new_job["time"]:
                self.jobs.insert(index, new_job)
                return
        self.jobs.append(new_job)

    def _get_next_job_id(self):
        self.job_id_index += 1
        return self.job_id_index
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
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:
            result = json.loads(cache_result)
        else:
            url = "http://pork.budabot.jkbff.com/pork/history.php?server=%d&name=%s" % (
                server_num, name)

            r = requests.get(url)
            try:
                result = r.json()
            except ValueError as e:
                self.logger.warning(
                    "Error marshalling value as json: %s" % r.text, 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
                result = self.cache_service.retrieve(self.CACHE_GROUP,
                                                     cache_key)

        if result:
            return map(lambda x: DictObject(x), result)
        else:
            return None
Exemple #7
0
        def do_request(**kwargs):

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

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

            from core.logger import Logger

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

            Logger.info("Executing request...")
            # Reintentamos 3 veces.
            for n in range(0, 3):
                try:
                    Logger.info("Try #{}".format(n + 1))
                    response = method(**kwargs).execute(num_retries=3)
                    return response
                except errors.HttpError, e:
                    Logger.info(e)
                    Logger.info("Execution failed...")
                    Logger.info("Status: {}".format(e.resp.status))
                    Logger.info("Reason: {}".format(e.resp.reason))
                    if e.resp.status in [403, 429, 503] or \
                            e.resp.reason in ["rateLimitExceeded", "userRateLimitExceeded"]:
                        Logger.warning("Error {}. Retrying".format(e.resp.status))
                        time.sleep((2 ** n) + random.randint(0, 1000) / 1000)
                    else:
                        Logger.error("Unknown error: {}".format(e))
                        raise e
Exemple #8
0
class DB:
    SQLITE = "sqlite"
    MYSQL = "mysql"

    def __init__(self):
        self.conn = None
        self.enhanced_like_regex = re.compile(
            r"(\s+)(\S+)\s+<EXTENDED_LIKE=(\d+)>\s+\?(\s*)", re.IGNORECASE)
        self.lastrowid = None
        self.logger = Logger(__name__)
        self.type = None
        self.transaction_level = 0

    def sqlite_row_factory(self, cursor: sqlite3.Cursor, row):
        d = {}
        for idx, col in enumerate(cursor.description):
            d[col[0]] = row[idx]
        return d

    def connect_mysql(self, host, port, username, password, database_name):
        self.type = self.MYSQL
        self.conn = mysql.connector.connect(user=username,
                                            password=password,
                                            host=host,
                                            port=port,
                                            database=database_name,
                                            charset="utf8",
                                            autocommit=True)
        self.exec("SET collation_connection = 'utf8_general_ci'")
        self.exec("SET sql_mode = 'TRADITIONAL,ANSI'")
        self.create_db_version_table()

    def connect_sqlite(self, filename):
        self.type = self.SQLITE
        self.conn = sqlite3.connect(filename,
                                    isolation_level=None,
                                    check_same_thread=False)
        self.conn.row_factory = self.sqlite_row_factory
        self.create_db_version_table()

    def create_db_version_table(self):
        self.exec(
            "CREATE TABLE IF NOT EXISTS db_version (file VARCHAR(255) NOT NULL, version VARCHAR(255) NOT NULL, verified SMALLINT NOT NULL)"
        )

    def _execute_wrapper(self, sql, params, callback):
        if self.type == self.MYSQL:
            # buffered=True - https://stackoverflow.com/a/33632767/280574
            cur = self.conn.cursor(dictionary=True, buffered=True)
        else:
            cur = self.conn.cursor()

        start_time = time.time()
        try:
            cur.execute(
                sql if self.type == self.SQLITE else sql.replace("?", "%s"),
                params)
        except Exception as e:
            raise SqlException(
                "SQL Error: '%s' for '%s' [%s]" %
                (str(e), sql, ", ".join(map(lambda x: str(x), params)))) from e

        elapsed = time.time() - start_time

        if elapsed > 0.5:
            self.logger.warning("slow query (%fs) '%s' for params: %s" %
                                (elapsed, sql, str(params)))

        result = callback(cur)
        cur.close()
        return result

    def query_single(self, sql, params=None, extended_like=False):
        if params is None:
            params = []

        if extended_like:
            sql, params = self.handle_extended_like(sql, params)

        sql, params = self.format_sql(sql, params)

        def map_result(cur):
            row = cur.fetchone()
            return DictObject(row) if row else None

        return self._execute_wrapper(sql, params, map_result)

    def query(self, sql, params=None, extended_like=False):
        if params is None:
            params = []

        if extended_like:
            sql, params = self.handle_extended_like(sql, params)

        sql, params = self.format_sql(sql, params)

        def map_result(cur):
            return list(map(lambda row: DictObject(row), cur.fetchall()))

        return self._execute_wrapper(sql, params, map_result)

    def exec(self, sql, params=None, extended_like=False):
        if params is None:
            params = []

        if extended_like:
            sql, params = self.handle_extended_like(sql, params)

        sql, params = self.format_sql(sql, params)

        def map_result(cur):
            return [cur.rowcount, cur.lastrowid]

        row_count, lastrowid = self._execute_wrapper(sql, params, map_result)
        self.lastrowid = lastrowid
        return row_count

    def last_insert_id(self):
        return self.lastrowid

    def format_sql(self, sql, params=None):
        # TODO check for AUTOINCREMENT in sql and log warning

        if self.type == self.SQLITE:
            sql = sql.replace("AUTO_INCREMENT", "AUTOINCREMENT")
            sql = sql.replace(" INT ", " INTEGER ")
            sql = sql.replace("INSERT IGNORE", "INSERT OR IGNORE")

        return sql, params

    def handle_extended_like(self, sql, params):
        original_params = params.copy()
        params = list(map(lambda x: [x], params))

        for match in self.enhanced_like_regex.finditer(sql):
            field = match.group(2)
            index = int(match.group(3))

            extra_sql, vals = self._get_extended_params(
                field, original_params[index].split(" "))

            sql = self.enhanced_like_regex.sub(
                match.group(1) + "(" + " AND ".join(extra_sql) + ")" +
                match.group(4), sql, 1)

            # remove current param and add generated params in its place
            del params[index]
            params.insert(index, vals)

        return sql, [item for sublist in params for item in sublist]

    def _get_extended_params(self, field, params):
        extra_sql = []
        vals = []
        for p in params:
            if p.startswith("-") and p != "-":
                vals.append("%" + p[1:] + "%")
                extra_sql.append(field + " NOT LIKE ?")
            else:
                vals.append("%" + p + "%")
                extra_sql.append(field + " LIKE ?")
        return extra_sql, vals

    def get_connection(self):
        return self.conn

    def load_sql_file(self, sqlfile, base_path):
        filename = base_path + os.sep + sqlfile

        db_version = self.get_db_version(filename)
        file_version = self.get_file_version(filename)

        if db_version:
            if parse_version(file_version) > parse_version(db_version):
                self.logger.debug("loading sql file '%s'" % sqlfile)
                self._load_file(filename)
            self.exec(
                "UPDATE db_version SET version = ?, verified = 1 WHERE file = ?",
                [int(file_version), filename])
        else:
            self.logger.debug("loading sql file '%s'" % sqlfile)
            self._load_file(filename)
            self.exec(
                "INSERT INTO db_version (file, version, verified) VALUES (?, ?, 1)",
                [filename, int(file_version)])

    def get_file_version(self, filename):
        return str(int(os.path.getmtime(filename)))

    def get_db_version(self, filename):
        row = self.query_single(
            "SELECT version FROM db_version WHERE file = ?", [filename])
        if row:
            return row.version
        else:
            return None

    def _load_file(self, filename):
        with open(filename, mode="r", encoding="UTF-8") as f:
            with self.transaction():
                cur = self.conn.cursor()
                line_num = 1
                for line in f.readlines():
                    try:
                        sql, _ = self.format_sql(line)
                        sql = sql.strip()
                        if sql and not sql.startswith("--"):
                            cur.execute(sql)
                    except Exception as e:
                        raise Exception(
                            "sql error in file '%s' on line %d: %s" %
                            (filename, line_num, str(e)))
                    line_num += 1
                cur.close()

    def get_type(self):
        return self.type

    # transaction support
    def transaction(self):
        return self

    def __enter__(self):
        # called when entering `with` code block
        self.begin_transaction()

    def __exit__(self, exc_type, exc_val, exc_tb):
        # called when exiting `with` code block
        # if exc_type, exc_val or exc_tb is not None, there was an exception
        # otherwise the code block exited normally
        if exc_type is None:
            self.commit_transaction()
        else:
            self.rollback_transaction()

        # False here indicates that if there was an exception, it should not be suppressed but instead propagated
        return False

    def begin_transaction(self):
        if self.transaction_level == 0:
            self.exec("BEGIN;")
        self.transaction_level += 1

    def commit_transaction(self):
        if self.transaction_level == 1:
            self.exec("COMMIT;")
        self.transaction_level -= 1

    def rollback_transaction(self):
        if self.transaction_level == 1:
            self.exec("ROLLBACK;")
        self.transaction_level -= 1
Exemple #9
0
class JobScheduler:
    def __init__(self):
        self.logger = Logger(__name__)
        self.jobs = []
        self.job_id_index = 0

    def check_for_scheduled_jobs(self, timestamp):
        while self.jobs and self.jobs[0]["time"] <= timestamp:
            try:
                job = self.jobs.pop(0)
                job["callback"](job["time"], *job["args"], **job["kwargs"])
            except Exception as e:
                self.logger.warning("Error processing scheduled job", e)

    def delayed_job(self, callback, delay, *args, **kwargs):
        """
        Args:
            callback: (time: Int, *args, *kwargs) -> void)
            delay: int
            *args
            **kwargs
        """

        return self.scheduled_job(callback, int(time.time()) + delay, *args, **kwargs)

    def scheduled_job(self, callback, scheduled_time, *args, **kwargs):
        """
        Args:
            callback: (time: Int, *args, *kwargs) -> void)
            scheduled_time: int
            *args
            **kwargs
        """

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

        job_id = self._get_next_job_id()
        new_job = {
            "id": job_id,
            "callback": callback,
            "args": args,
            "kwargs": kwargs,
            "time": scheduled_time
        }

        self._insert_job(new_job)
        return job_id

    def cancel_job(self, job_id):
        for index, job in enumerate(self.jobs):
            if job["id"] == job_id:
                return self.jobs.pop(index)
        return None

    def _insert_job(self, new_job):
        for index, job in enumerate(self.jobs):
            if job["time"] > new_job["time"]:
                self.jobs.insert(index, new_job)
                return
        self.jobs.append(new_job)

    def _get_next_job_id(self):
        self.job_id_index += 1
        return self.job_id_index
Exemple #10
0
class Tyrbot:
    CONNECT_EVENT = "connect"
    PRIVATE_MSG_EVENT = "private_msg"

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

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

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

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

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

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

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

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

        self.status = BotStatus.RUN

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

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

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

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

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

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

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

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

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

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

            if i == 0:
                self.primary_conn_id = _id

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

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

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

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

        return True

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return self.status

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

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

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

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

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

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

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

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

        return packet

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

        return packet

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

        return packet

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

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

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

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

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

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

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

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

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

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

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

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

    def is_ready(self):
        return self.ready

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

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

    def get_primary_conn_id(self):
        return self.primary_conn_id

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

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

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

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

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

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

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

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

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

        Args:
            event_type (str)
        """

        event_type = event_type.lower()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def get_event_types(self):
        return self.event_types

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

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

            return result

    def run_timer_events_at_startup(self):
        t = int(time.time())
        data = self.db.query(
            "SELECT e.event_type, e.event_sub_type, e.handler, t.next_run FROM timer_event t "
            "JOIN event_config e ON t.event_type = e.event_type AND t.handler = e.handler "
            "WHERE e.event_type = ? AND e.enabled = 1", ["timer"])
        for row in data:
            handler = self.handlers[row.handler]
            attrs = getattr(handler, "event")
            if attrs.get("run_at_startup", False):
                self.execute_timed_event(row, t)
Exemple #12
0
class PorkService:
    def __init__(self):
        self.logger = Logger(__name__)

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

    def pre_start(self):
        self.bot.register_packet_handler(server_packets.CharacterLookup.id,
                                         self.update)
        self.bot.register_packet_handler(server_packets.CharacterName.id,
                                         self.update)

    def start(self):
        self.db.exec(
            "CREATE TABLE IF NOT EXISTS player ( char_id BIGINT PRIMARY KEY, first_name VARCHAR(30) NOT NULL, name VARCHAR(20) NOT NULL, last_name VARCHAR(30) NOT NULL, "
            "level SMALLINT NOT NULL, breed VARCHAR(20) NOT NULL, gender VARCHAR(20) NOT NULL, faction VARCHAR(20) NOT NULL, profession VARCHAR(20) NOT NULL, "
            "profession_title VARCHAR(50) NOT NULL, ai_rank VARCHAR(20) NOT NULL, ai_level SMALLINT, org_id INT DEFAULT NULL, org_name VARCHAR(255) NOT NULL, "
            "org_rank_name VARCHAR(20) NOT NULL, org_rank_id SMALLINT NOT NULL, dimension SMALLINT NOT NULL, head_id INT NOT NULL, pvp_rating SMALLINT NOT NULL, "
            "pvp_title VARCHAR(20) NOT NULL, source VARCHAR(50) NOT NULL, last_updated INT NOT NULL )"
        )

    # forces a lookup from remote PoRK server
    # this should not be called directly unless you are requesting info for a char on a different server
    # since cache will not be used and the result will not update the cache automatically
    def request_char_info(self, char_name, server_num):
        url = self.get_pork_url(server_num, char_name)

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

        char_info = None
        if result:
            char_info_json = result[0]
            org_info_json = result[1] if result[1] else {}

            char_info = DictObject({
                "name":
                char_info_json["NAME"],
                "char_id":
                char_info_json["CHAR_INSTANCE"],
                "first_name":
                char_info_json["FIRSTNAME"],
                "last_name":
                char_info_json["LASTNAME"],
                "level":
                char_info_json["LEVELX"],
                "breed":
                char_info_json["BREED"],
                "dimension":
                char_info_json["CHAR_DIMENSION"],
                "gender":
                char_info_json["SEX"],
                "faction":
                char_info_json["SIDE"],
                "profession":
                char_info_json["PROF"],
                "profession_title":
                char_info_json["PROFNAME"],
                "ai_rank":
                char_info_json["RANK_name"],
                "ai_level":
                char_info_json["ALIENLEVEL"],
                "pvp_rating":
                char_info_json["PVPRATING"],
                "pvp_title":
                char_info_json["PVPTITLE"] or "",
                "head_id":
                char_info_json["HEADID"],
                "org_id":
                org_info_json.get("ORG_INSTANCE", 0),
                "org_name":
                org_info_json.get("NAME", ""),
                "org_rank_name":
                org_info_json.get("RANK_TITLE", ""),
                "org_rank_id":
                org_info_json.get("RANK", 0),
                "source":
                "people.anarchy-online.com",
                "cache_age":
                0
            })

        return char_info

    # standard method to get character pork data when character is on the same server
    def get_character_info(self, char_name_or_id, max_cache_age=86400):
        char_id = self.character_service.resolve_char_to_id(char_name_or_id)
        char_name = self.character_service.resolve_char_to_name(
            char_name_or_id)

        t = int(time.time())

        # if there is an entry in database and it is within the cache time, use that
        db_char_info = self.get_from_database(char_id=char_id,
                                              char_name=char_name)
        if db_char_info:
            db_char_info.cache_age = t - db_char_info.last_updated

            if db_char_info.cache_age < max_cache_age and db_char_info.source != "chat_server":
                return db_char_info

        # if we can't resolve to a char_name, we can't make a call to pork
        if not char_name:
            return db_char_info

        char_info = self.request_char_info(char_name, self.bot.dimension)

        if char_info and (char_id is None or char_info.char_id == char_id):
            self.save_character_info(char_info)

            return char_info
        else:
            # return cached info from database, even tho it's old, and set cache_age (if it exists)
            if db_char_info:
                db_char_info.cache_age = t - db_char_info.last_updated

            return db_char_info

    # forces a skeleton object into the player table in the case that PoRK does not return any data
    # call this method if you don't need the data now but want to ensure there is a record in the database
    def load_character_info(self, char_id, char_name=None):
        char_info = self.get_character_info(char_id)
        if not char_info and char_name:
            char_info = self.get_character_info(char_name)
        if not char_info:
            char_info = DictObject({
                "name": "Unknown:" + str(char_id),
                "char_id": char_id,
                "first_name": "",
                "last_name": "",
                "level": 0,
                "breed": "",
                "dimension": self.bot.dimension,
                "gender": "",
                "faction": "",
                "profession": "",
                "profession_title": "",
                "ai_rank": "",
                "ai_level": 0,
                "pvp_rating": 0,
                "pvp_title": "",
                "head_id": 0,
                "org_id": 0,
                "org_name": "",
                "org_rank_name": "",
                "org_rank_id": 6,
                "source": "stub"
            })
            self.save_character_info(char_info)

    def save_character_info(self, char_info):
        if char_info["dimension"] != self.bot.dimension:
            return

        self.db.exec("DELETE FROM player WHERE char_id = ?",
                     [char_info["char_id"]])

        insert_sql = """
            INSERT IGNORE INTO player ( char_id, name, first_name, last_name, level, breed, gender, faction, profession,
                profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, dimension, head_id,
                pvp_rating, pvp_title, source, last_updated)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """

        self.db.exec(insert_sql, [
            char_info["char_id"], char_info["name"], char_info["first_name"],
            char_info["last_name"], char_info["level"], char_info["breed"],
            char_info["gender"], char_info["faction"], char_info["profession"],
            char_info["profession_title"], char_info["ai_rank"],
            char_info["ai_level"], char_info["org_id"], char_info["org_name"],
            char_info["org_rank_name"], char_info["org_rank_id"],
            char_info["dimension"], char_info["head_id"],
            char_info["pvp_rating"], char_info["pvp_title"],
            char_info["source"],
            int(time.time())
        ])

    def get_from_database(self, char_id=None, char_name=None):
        if char_id:
            return self.db.query_single(
                "SELECT char_id, name, first_name, last_name, level, breed, gender, faction, profession, "
                "profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, "
                "dimension, head_id, pvp_rating, pvp_title, source, last_updated "
                "FROM player WHERE char_id = ?", [char_id])
        elif char_name:
            return self.db.query_single(
                "SELECT char_id, name, first_name, last_name, level, breed, gender, faction, profession, "
                "profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, "
                "dimension, head_id, pvp_rating, pvp_title, source, last_updated "
                "FROM player WHERE name = ?", [char_name])
        else:
            return None

    def update(self, conn, packet):
        # don't update if we didn't get a valid response
        if packet.char_id == 4294967295:
            return

        character = self.get_from_database(char_id=packet.char_id)

        if character:
            if character.name != packet.name:
                self.db.exec("UPDATE player SET name = ? WHERE char_id = ?",
                             [packet.name, packet.char_id])
        else:
            insert_sql = """
                INSERT IGNORE INTO player ( char_id, name, first_name, last_name, level, breed, gender, faction, profession,
                profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, dimension, head_id,
                pvp_rating, pvp_title, source, last_updated)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"""

            self.db.exec(insert_sql, [
                packet.char_id, packet.name, "", "", 0, "", "", "", "", "", "",
                0, 0, "", "", 6, self.bot.dimension, 0, 0, "", "chat_server",
                int(time.time())
            ])

    def find_orgs(self, search):
        return self.db.query(
            "SELECT DISTINCT org_name, org_id FROM player WHERE org_name <EXTENDED_LIKE=0> ?",
            [search],
            extended_like=True)

    def get_pork_url(self, dimension, char_name):
        return "http://people.anarchy-online.com/character/bio/d/%d/name/%s/bio.xml?data_type=json" % (
            dimension, char_name)
Exemple #13
0
class SettingService:
    def __init__(self):
        self.logger = Logger(__name__)
        self.settings = {}
        self.db_cache = {}

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

    def start(self):
        # process decorators
        for _, inst in Registry.get_all_instances().items():
            for name, method in get_attrs(inst).items():
                if hasattr(method, "setting"):
                    setting_name, value, description, obj = getattr(method, "setting")
                    handler = getattr(inst, name)
                    module = self.util.get_module_name(handler)
                    self.register(setting_name, value, description, obj, module)

    def register(self, name, value, description, setting: SettingType, module):
        name = name.lower()
        module = module.lower()
        setting.set_name(name)
        setting.set_description(description)

        if not description:
            self.logger.warning("No description specified for setting '%s'" % name)

        if " " in name:
            raise Exception("One or more spaces found in setting name '%s' for module '%s'" % (name, module))

        row = self.db.query_single("SELECT name, value, description FROM setting WHERE name = ?",
                                   [name])

        if row is None:
            self.logger.debug("Adding setting '%s'" % name)

            self.db.exec(
                "INSERT INTO setting (name, value, description, module, verified) VALUES (?, ?, ?, ?, ?)",
                [name, "", description, module, 1])

            # verify default value is a valid value, and is formatted appropriately
            setting.set_value(value)
        else:
            self.logger.debug("Updating setting '%s'" % name)
            self.db.exec(
                "UPDATE setting SET description = ?, verified = ?, module = ? WHERE name = ?",
                [description, 1, module, name])

        self.settings[name] = setting

    def get_value(self, name):
        # check cache first
        result = self.db_cache.get(name, None)
        if result:
            return result.value
        else:
            row = self.db.query_single("SELECT value FROM setting WHERE name = ?", [name])

            # store result in cache
            self.db_cache[name] = row

            return row.value if row else None

    def set_value(self, name, value):
        # clear cache
        self.db_cache[name] = None

        self.db.exec("UPDATE setting SET value = ? WHERE name = ?", [value, name])

    def get(self, name):
        name = name.lower()
        setting = self.settings.get(name, None)
        if setting:
            return setting
        else:
            return None
Exemple #14
0
class PorkService:
    def __init__(self):
        self.logger = Logger(__name__)

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

    def pre_start(self):
        self.bot.add_packet_handler(server_packets.CharacterLookup.id,
                                    self.update)
        self.bot.add_packet_handler(server_packets.CharacterName.id,
                                    self.update)

    def request_char_info(self, char_name, server_num):
        url = self.get_pork_url(server_num, char_name)

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

        char_info = None
        if result:
            char_info_json = result[0]
            org_info_json = result[1] if result[1] else {}

            char_info = DictObject({
                "name":
                char_info_json["NAME"],
                "char_id":
                char_info_json["CHAR_INSTANCE"],
                "first_name":
                char_info_json["FIRSTNAME"],
                "last_name":
                char_info_json["LASTNAME"],
                "level":
                char_info_json["LEVELX"],
                "breed":
                char_info_json["BREED"],
                "dimension":
                char_info_json["CHAR_DIMENSION"],
                "gender":
                char_info_json["SEX"],
                "faction":
                char_info_json["SIDE"],
                "profession":
                char_info_json["PROF"],
                "profession_title":
                char_info_json["PROFNAME"],
                "ai_rank":
                char_info_json["RANK_name"],
                "ai_level":
                char_info_json["ALIENLEVEL"],
                "pvp_rating":
                char_info_json["PVPRATING"],
                "pvp_title":
                char_info_json["PVPTITLE"] or "",
                "head_id":
                char_info_json["HEADID"],
                "org_id":
                org_info_json.get("ORG_INSTANCE", 0),
                "org_name":
                org_info_json.get("NAME", ""),
                "org_rank_name":
                org_info_json.get("RANK_TITLE", ""),
                "org_rank_id":
                org_info_json.get("RANK", 0),
                "source":
                "people.anarchy-online.com",
                "cache_age":
                0
            })

        return char_info

    def get_character_info(self, char, max_cache_age=86400):
        char_id = self.character_service.resolve_char_to_id(char)
        char_name = self.character_service.resolve_char_to_name(char)

        t = int(time.time())

        # if there is an entry in database and it is within the cache time, use that
        db_char_info = self.get_from_database(char_id=char_id,
                                              char_name=char_name)
        if db_char_info:
            db_char_info.cache_age = t - db_char_info.last_updated

            if db_char_info.cache_age < max_cache_age and db_char_info.source != "chat_server":
                return db_char_info

        # if we can't resolve to a char_name, we can't make a call to pork
        if not char_name:
            return db_char_info

        char_info = self.request_char_info(char_name, self.bot.dimension)

        if char_info and char_info.char_id == char_id:
            self.save_character_info(char_info)

            return char_info
        else:
            # return cached info from database, even tho it's old, and set cache_age (if it exists)
            if db_char_info:
                db_char_info.cache_age = t - db_char_info.last_updated

            return db_char_info

    def load_character_info(self, char_id):
        char_info = self.get_character_info(char_id)
        if not char_info:
            char_info = DictObject({
                "name": "Unknown:" + str(char_id),
                "char_id": char_id,
                "first_name": "",
                "last_name": "",
                "level": 0,
                "breed": "",
                "dimension": self.bot.dimension,
                "gender": "",
                "faction": "",
                "profession": "",
                "profession_title": "",
                "ai_rank": "",
                "ai_level": 0,
                "pvp_rating": 0,
                "pvp_title": "",
                "head_id": 0,
                "org_id": 0,
                "org_name": "",
                "org_rank_name": "",
                "org_rank_id": 6,
                "source": "stub"
            })
            self.save_character_info(char_info)

    def save_character_info(self, char_info):
        if char_info["dimension"] != self.bot.dimension:
            return

        self.db.exec("DELETE FROM player WHERE char_id = ?",
                     [char_info["char_id"]])

        insert_sql = """
            INSERT INTO player ( char_id, name, first_name, last_name, level, breed, gender, faction, profession,
                profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, dimension, head_id,
                pvp_rating, pvp_title, source, last_updated)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """

        self.db.exec(insert_sql, [
            char_info["char_id"], char_info["name"], char_info["first_name"],
            char_info["last_name"], char_info["level"], char_info["breed"],
            char_info["gender"], char_info["faction"], char_info["profession"],
            char_info["profession_title"], char_info["ai_rank"],
            char_info["ai_level"], char_info["org_id"], char_info["org_name"],
            char_info["org_rank_name"], char_info["org_rank_id"],
            char_info["dimension"], char_info["head_id"],
            char_info["pvp_rating"], char_info["pvp_title"],
            char_info["source"],
            int(time.time())
        ])

    def get_from_database(self, char_id=None, char_name=None):
        if char_id:
            return self.db.query_single(
                "SELECT char_id, name, first_name, last_name, level, breed, gender, faction, profession, "
                "profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, "
                "dimension, head_id, pvp_rating, pvp_title, source, last_updated "
                "FROM player WHERE char_id = ?", [char_id])
        elif char_name:
            return self.db.query_single(
                "SELECT char_id, name, first_name, last_name, level, breed, gender, faction, profession, "
                "profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, "
                "dimension, head_id, pvp_rating, pvp_title, source, last_updated "
                "FROM player WHERE name = ?", [char_name])
        else:
            return None

    def update(self, packet):
        # don't update if we didn't get a valid response
        if packet.char_id == 4294967295:
            return

        character = self.get_from_database(char_id=packet.char_id)

        if character:
            if character.name != packet.name:
                self.db.exec("UPDATE player SET name = ? WHERE char_id = ?",
                             [packet.name, packet.char_id])
        else:
            insert_sql = """
                INSERT INTO player ( char_id, name, first_name, last_name, level, breed, gender, faction, profession,
                profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, dimension, head_id,
                pvp_rating, pvp_title, source, last_updated)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"""

            self.db.exec(insert_sql, [
                packet.char_id, packet.name, "", "", 0, "", "", "", "", "", "",
                0, 0, "", "", 6, self.bot.dimension, 0, 0, "", "chat_server",
                int(time.time())
            ])

    def find_orgs(self, search):
        return self.db.query(
            "SELECT DISTINCT org_name, org_id FROM player WHERE org_name <EXTENDED_LIKE=0> ?",
            [search],
            extended_like=True)

    def get_pork_url(self, dimension, char_name):
        return "http://people.anarchy-online.com/character/bio/d/%d/name/%s/bio.xml?data_type=json" % (
            dimension, char_name)
Exemple #15
0
class TowerController:
    TOWER_ATTACK_EVENT = "tower_attack"
    TOWER_VICTORY_EVENT = "tower_victory"

    TOWER_BATTLE_OUTCOME_ID = 42949672962
    ALL_TOWERS_ID = 42949672960

    ATTACK_1 = [
        506, 12753364
    ]  # The %s organization %s just entered a state of war! %s attacked the %s organization %s's tower in %s at location (%d,%d).
    ATTACK_2 = re.compile(
        "^(.+) just attacked the (clan|neutral|omni) organization (.+)'s tower in (.+) at location \((\d+), (\d+)\).\n$"
    )

    VICTORY_1 = re.compile(
        "^Notum Wars Update: Victory to the (Clan|Neutral|Omni)s!!!$")
    VICTORY_2 = re.compile(
        "^The (Clan|Neutral|Omni) organization (.+) attacked the (Clan|Neutral|Omni) (.+) at their base in (.+). The attackers won!!$"
    )
    VICTORY_3 = [
        506, 147506468
    ]  # 'Notum Wars Update: The %s organization %s lost their base in %s.'

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

    def inject(self, registry):
        self.bot: Tyrbot = registry.get_instance("bot")
        self.db: DB = registry.get_instance("db")
        self.text: Text = registry.get_instance("text")
        self.event_service: EventService = registry.get_instance(
            "event_service")
        self.pork_service: PorkService = registry.get_instance("pork_service")
        self.playfield_controller: PlayfieldController = registry.get_instance(
            "playfield_controller")
        self.public_channel_service: PublicChannelService = registry.get_instance(
            "public_channel_service")

    def pre_start(self):
        self.event_service.register_event_type(self.TOWER_ATTACK_EVENT)
        self.event_service.register_event_type(self.TOWER_VICTORY_EVENT)
        self.bot.add_packet_handler(server_packets.PublicChannelMessage.id,
                                    self.handle_public_channel_message)

    @command(command="lc",
             params=[],
             access_level="all",
             description=
             "See a list of playfields containing land control tower sites")
    def lc_list_cmd(self, request):
        data = self.db.query(
            "SELECT * FROM playfields WHERE id IN (SELECT DISTINCT playfield_id FROM tower_site) ORDER BY short_name"
        )

        blob = ""
        for row in data:
            blob += "%s <highlight>%s<end>\n" % (self.text.make_chatcmd(
                row.long_name,
                "/tell <myname> lc %s" % row.short_name), row.short_name)

        return ChatBlob("Land Control Playfields", blob)

    @command(command="lc",
             params=[Any("playfield"),
                     Int("site_number", is_optional=True)],
             access_level="all",
             description=
             "See a list of land control tower sites in a particular playfield"
             )
    def lc_playfield_cmd(self, request, playfield_name, site_number):
        playfield = self.playfield_controller.get_playfield_by_name(
            playfield_name)
        if not playfield:
            return "Could not find playfield <highlight>%s<end>." % playfield_name

        if site_number:
            data = self.db.query(
                "SELECT t.*, p.short_name, p.long_name FROM tower_site t JOIN playfields p ON t.playfield_id = p.id WHERE t.playfield_id = ? AND site_number = ?",
                [playfield.id, site_number])
        else:
            data = self.db.query(
                "SELECT t.*, p.short_name, p.long_name FROM tower_site t JOIN playfields p ON t.playfield_id = p.id WHERE t.playfield_id = ?",
                [playfield.id])

        if not data:
            if site_number:
                return "Could not find tower info for <highlight>%s %d<end>." % (
                    playfield.long_name, site_number)
            else:
                return "Could not find tower info for <highlight>%s<end>." % playfield.long_name

        blob = ""
        for row in data:
            blob += "<pagebreak>" + self.format_site_info(row) + "\n\n"

        if site_number:
            title = "Tower Info: %s %d" % (playfield.long_name, site_number)
        else:
            title = "Tower Info: %s" % playfield.long_name

        return ChatBlob(title, blob)

    @event(event_type="connect",
           description="Check if All Towers channel is available")
    def handle_connect_event(self, event_type, event_data):
        if self.public_channel_service.org_id and not self.public_channel_service.get_channel_id(
                "All Towers"):
            self.logger.warning(
                "bot is a member of an org but does not have access to 'All Towers' channel and therefore will not receive tower attack messages"
            )

    def format_site_info(self, row):
        blob = "Short name: <highlight>%s %d<end>\n" % (row.short_name,
                                                        row.site_number)
        blob += "Long name: <highlight>%s, %s<end>\n" % (row.site_name,
                                                         row.long_name)
        blob += "Level range: <highlight>%d-%d<end>\n" % (row.min_ql,
                                                          row.max_ql)
        blob += "Coordinates: %s\n" % self.text.make_chatcmd(
            "%dx%d" % (row.x_coord, row.y_coord), "/waypoint %d %d %d" %
            (row.x_coord, row.y_coord, row.playfield_id))

        return blob

    def handle_public_channel_message(
            self, packet: server_packets.PublicChannelMessage):
        if packet.channel_id == self.TOWER_BATTLE_OUTCOME_ID:
            victory = self.get_victory_event(packet)

            if victory:
                # self.logger.debug("tower victory packet: %s" % str(packet))

                # lookup playfield
                playfield_name = victory.location.playfield.long_name
                victory.location.playfield = self.playfield_controller.get_playfield_by_name(
                    playfield_name) or DictObject()
                victory.location.playfield.long_name = playfield_name

                self.event_service.fire_event(self.TOWER_VICTORY_EVENT,
                                              victory)
        elif packet.channel_id == self.ALL_TOWERS_ID:
            attack = self.get_attack_event(packet)

            if attack:
                # self.logger.debug("tower attack packet: %s" % str(packet))

                # lookup playfield
                playfield_name = attack.location.playfield.long_name
                attack.location.playfield = self.playfield_controller.get_playfield_by_name(
                    playfield_name) or DictObject()
                attack.location.playfield.long_name = playfield_name

                # lookup attacker
                name = attack.attacker.name
                faction = attack.attacker.faction
                org_name = attack.attacker.org_name
                attack.attacker = self.pork_service.get_character_info(
                    name) or DictObject()
                attack.attacker.faction = faction or attack.attacker.get(
                    "faction", "Unknown")
                attack.attacker.org_name = org_name

                self.event_service.fire_event(self.TOWER_ATTACK_EVENT, attack)

    def get_attack_event(self, packet: server_packets.PublicChannelMessage):
        if packet.extended_message and [
                packet.extended_message.category_id,
                packet.extended_message.instance_id
        ] == self.ATTACK_1:
            params = packet.extended_message.params
            return DictObject({
                "attacker": {
                    "name": params[2],
                    "faction": params[0].capitalize(),
                    "org_name": params[1]
                },
                "defender": {
                    "faction": params[3].capitalize(),
                    "org_name": params[4]
                },
                "location": {
                    "playfield": {
                        "long_name": params[5]
                    },
                    "x_coord": params[6],
                    "y_coord": params[7]
                }
            })
        else:
            match = self.ATTACK_2.match(packet.message)
            if match:
                return DictObject({
                    "attacker": {
                        "name": match.group(1),
                        "faction": "",
                        "org_name": ""
                    },
                    "defender": {
                        "faction": match.group(2).capitalize(),
                        "org_name": match.group(3)
                    },
                    "location": {
                        "playfield": {
                            "long_name": match.group(4)
                        },
                        "x_coord": match.group(5),
                        "y_coord": match.group(6)
                    }
                })

        # Unknown attack
        self.logger.warning("Unknown tower attack: " + str(packet))
        return None

    def get_victory_event(self, packet: server_packets.PublicChannelMessage):
        match = self.VICTORY_1.match(packet.message)
        if match:
            return None

        match = self.VICTORY_2.match(packet.message)
        if match:
            return DictObject({
                "type": "attack",
                "winner": {
                    "faction": match.group(1).capitalize(),
                    "org_name": match.group(2)
                },
                "loser": {
                    "faction": match.group(3).capitalize(),
                    "org_name": match.group(4)
                },
                "location": {
                    "playfield": {
                        "long_name": match.group(5)
                    }
                }
            })

        if packet.extended_message and [
                packet.extended_message.category_id,
                packet.extended_message.instance_id
        ] == self.VICTORY_3:
            params = packet.extended_message.params
            return DictObject({
                "type": "terminated",
                "winner": {
                    "faction": params[0].capitalize(),
                    "org_name": params[1]
                },
                "loser": {
                    "faction": params[0].capitalize(),
                    "org_name": params[1]
                },
                "location": {
                    "playfield": {
                        "long_name": params[2]
                    }
                }
            })

        # Unknown victory
        self.logger.warning("Unknown tower victory: " + str(packet))
        return None
Exemple #16
0
class PrivateChannelController:
    MESSAGE_SOURCE = "private_channel"
    MESSAGE_SOURCE_UPDATE = "private_channel_update"

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

    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.private_channel_service = registry.get_instance(
            "private_channel_service")
        self.character_service = registry.get_instance("character_service")
        self.job_scheduler = registry.get_instance("job_scheduler")
        self.access_service = registry.get_instance("access_service")
        self.message_hub_service = registry.get_instance("message_hub_service")
        self.ban_service = registry.get_instance("ban_service")
        self.log_controller = registry.get_instance(
            "log_controller",
            is_optional=True)  # TODO core module depending on standard module
        self.online_controller = registry.get_instance(
            "online_controller",
            is_optional=True)  # TODO core module depending on standard module
        self.text: Text = registry.get_instance("text")
        self.setting_service: SettingService = registry.get_instance(
            "setting_service")

    def pre_start(self):
        self.message_hub_service.register_message_source(self.MESSAGE_SOURCE)
        self.message_hub_service.register_message_source(
            self.MESSAGE_SOURCE_UPDATE)

    def start(self):
        self.setting_service.register(
            self.module_name, "private_channel_prefix", "[Priv]",
            TextSettingType(["[Priv]", "[Guest]"]),
            "The name to show for messages coming from the private channel")

        self.setting_service.register(
            self.module_name,
            "private_channel_conn",
            "",
            TextSettingType(allow_empty=True),
            "The conn id or name to use for the private channel",
            extended_description=
            "If empty, the bot will use the primary conn. You MUST restart the bot after changing this value for the change to take effect."
        )

        self.message_hub_service.register_message_destination(
            self.MESSAGE_SOURCE, self.handle_incoming_relay_message, [
                "org_channel", "org_channel_update", "discord",
                "websocket_relay", "broadcast", "raffle", "shutdown_notice",
                "raid", "timers", "alliance"
            ], [self.MESSAGE_SOURCE, self.MESSAGE_SOURCE_UPDATE])

    def handle_incoming_relay_message(self, ctx):
        self.bot.send_private_channel_message(ctx.formatted_message,
                                              conn=self.get_conn(None))

    @command(command="join",
             params=[],
             access_level="member",
             description="Join the private channel")
    def join_cmd(self, request):
        self.private_channel_service.invite(request.sender.char_id,
                                            self.get_conn(request.conn))

    @command(command="leave",
             params=[],
             access_level="all",
             description="Leave the private channel")
    def leave_cmd(self, request):
        self.private_channel_service.kick(request.sender.char_id,
                                          self.get_conn(request.conn))

    @command(command="invite",
             params=[Character("character")],
             access_level="all",
             description="Invite a character to the private channel")
    def invite_cmd(self, request, char):
        if char.char_id:
            conn = self.get_conn(request.conn)
            if char.char_id in conn.private_channel:
                return f"<highlight>{char.name}</highlight> is already in the private channel."
            else:
                self.bot.send_private_message(
                    char.char_id,
                    f"You have been invited to the private channel by <highlight>{request.sender.name}</highlight>.",
                    conn=conn)
                self.private_channel_service.invite(char.char_id, conn)
                return f"You have invited <highlight>{char.name}</highlight> to the private channel."
        else:
            return StandardMessage.char_not_found(char.name)

    @command(command="kick",
             params=[Character("character")],
             access_level="moderator",
             description="Kick a character from the private channel")
    def kick_cmd(self, request, char):
        if char.char_id:
            conn = self.get_conn(request.conn)
            if char.char_id not in conn.private_channel:
                return f"<highlight>{char.name}</highlight> is not in the private channel."
            else:
                # TODO use request.sender.access_level and char.access_level
                if self.access_service.has_sufficient_access_level(
                        request.sender.char_id, char.char_id):
                    self.bot.send_private_message(
                        char.char_id,
                        f"You have been kicked from the private channel by <highlight>{request.sender.name}</highlight>.",
                        conn=conn)
                    self.private_channel_service.kick(char.char_id, conn)
                    return f"You have kicked <highlight>{char.name}</highlight> from the private channel."
                else:
                    return f"You do not have the required access level to kick <highlight>{char.name}</highlight>."
        else:
            return StandardMessage.char_not_found(char.name)

    @command(command="kickall",
             params=[],
             access_level="moderator",
             description="Kick all characters from the private channel")
    def kickall_cmd(self, request):
        conn = self.get_conn(request.conn)
        self.bot.send_private_channel_message(
            f"Everyone will be kicked from this channel in 10 seconds. [by <highlight>{request.sender.name}</highlight>]",
            conn=conn)
        self.job_scheduler.delayed_job(
            lambda t: self.private_channel_service.kickall(conn), 10)

    @event(event_type="connect",
           description=
           "Load the conn ids as choice for private_channel_conn setting",
           is_system=True)
    def load_conns_into_setting_choice(self, event_type, event_data):
        options = []
        for _id, conn in self.bot.get_conns(lambda x: x.is_main == True):
            options.append(conn.char_name)

        setting = self.setting_service.get("private_channel_conn")
        setting.options = options

    @event(
        event_type=BanService.BAN_ADDED_EVENT,
        description="Kick characters from the private channel who are banned",
        is_system=True)
    def ban_added_event(self, event_type, event_data):
        self.private_channel_service.kick_from_all(event_data.char_id)

    @event(
        event_type=PrivateChannelService.PRIVATE_CHANNEL_MESSAGE_EVENT,
        description="Relay messages from the private channel to the relay hub",
        is_system=True)
    def handle_private_channel_message_event(self, event_type, event_data):
        if self.bot.get_conn_by_char_id(
                event_data.char_id) or self.ban_service.get_ban(
                    event_data.char_id):
            return

        sender = DictObject({
            "char_id": event_data.char_id,
            "name": event_data.name
        })
        self.message_hub_service.send_message(
            self.MESSAGE_SOURCE, sender, self.get_private_channel_prefix(),
            event_data.message)

    @event(event_type=PrivateChannelService.JOINED_PRIVATE_CHANNEL_EVENT,
           description="Notify when a character joins the private channel")
    def handle_private_channel_joined_event(self, event_type, event_data):
        if self.online_controller:
            char_info = self.online_controller.get_char_info_display(
                event_data.char_id, event_data.conn)
        else:
            char_info = self.character_service.resolve_char_to_name(
                event_data.char_id)

        msg = f"{char_info} has joined the private channel."
        if self.log_controller:
            msg += " " + self.log_controller.get_logon(event_data.char_id)

        self.bot.send_private_channel_message(msg, conn=event_data.conn)
        self.message_hub_service.send_message(
            self.MESSAGE_SOURCE_UPDATE, None,
            self.get_private_channel_prefix(), msg)

    @event(event_type=PrivateChannelService.LEFT_PRIVATE_CHANNEL_EVENT,
           description="Notify when a character leaves the private channel")
    def handle_private_channel_left_event(self, event_type, event_data):
        msg = f"<highlight>{event_data.name}</highlight> has left the private channel."
        if self.log_controller:
            msg += " " + self.log_controller.get_logoff(event_data.char_id)

        self.bot.send_private_channel_message(msg, conn=event_data.conn)
        self.message_hub_service.send_message(
            self.MESSAGE_SOURCE_UPDATE, None,
            self.get_private_channel_prefix(), msg)

    @event(
        event_type=PrivateChannelService.PRIVATE_CHANNEL_COMMAND_EVENT,
        description="Relay commands from the private channel to the relay hub",
        is_system=True)
    def outgoing_private_channel_message_event(self, event_type, event_data):
        sender = None
        if event_data.name:
            sender = DictObject({
                "char_id": event_data.char_id,
                "name": event_data.name
            })

        if isinstance(event_data.message, ChatBlob):
            pages = self.text.paginate(
                ChatBlob(event_data.message.title, event_data.message.msg),
                event_data.conn,
                self.setting_service.get(
                    "org_channel_max_page_length").get_value())
            if len(pages) < 4:
                for page in pages:
                    self.message_hub_service.send_message(
                        self.MESSAGE_SOURCE, sender,
                        self.get_private_channel_prefix(), page)
            else:
                self.message_hub_service.send_message(
                    self.MESSAGE_SOURCE, sender,
                    self.get_private_channel_prefix(),
                    event_data.message.title)
        else:
            self.message_hub_service.send_message(
                self.MESSAGE_SOURCE, sender, self.get_private_channel_prefix(),
                event_data.message)

    def get_conn(self, conn):
        if self.private_channel_conn:
            return self.private_channel_conn

        conn_id = self.setting_service.get_value("private_channel_conn")
        if conn_id:
            for _id, conn in self.bot.get_conns(
                    lambda x: x.id == conn_id or x.char_name == conn_id):
                self.private_channel_conn = conn
                break

            if not self.private_channel_conn:
                self.logger.warning(
                    f"Could not find conn with id '{conn_id}', defaulting to primary conn"
                )
                self.private_channel_conn = self.bot.get_primary_conn()
        else:
            # use the primary conn if private_channel_conn is not set
            self.private_channel_conn = self.bot.get_primary_conn()

        return self.private_channel_conn

    def get_private_channel_prefix(self):
        return self.setting_service.get_value("private_channel_prefix")
Exemple #17
0
class Tyrbot:
    CONNECT_EVENT = "connect"
    PACKET_EVENT = "packet"
    PRIVATE_MSG_EVENT = "private_msg"

    OUTGOING_ORG_MESSAGE_EVENT = "outgoing_org_message"
    OUTGOING_PRIVATE_MESSAGE_EVENT = "outgoing_private_message"
    OUTGOING_PRIVATE_CHANNEL_MESSAGE_EVENT = "outgoing_private_channel_message"

    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.5-beta"
        self.incoming_queue = FifoQueue()
        self.mass_message_queue = None
        self.conns = DictObject()

    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, paths, mmdb_parser):
        self.mmdb_parser = mmdb_parser
        self.superadmin = config.superadmin.capitalize()
        self.dimension = config.server.dimension

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

        self.load_sql_files(paths)

        # 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.PACKET_EVENT)
        self.event_service.register_event_type(self.PRIVATE_MSG_EVENT)
        self.event_service.register_event_type(self.OUTGOING_ORG_MESSAGE_EVENT)
        self.event_service.register_event_type(self.OUTGOING_PRIVATE_MESSAGE_EVENT)
        self.event_service.register_event_type(self.OUTGOING_PRIVATE_CHANNEL_MESSAGE_EVENT)

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

        self.setting_service.register_new("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_new("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_new("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_new("core.system", "org_id", "", NumberSettingType(allow_empty=True), "Override the default org id",
                                          "This setting is is for development/debug purposes and should not be changed unless you understand the implications")
        self.setting_service.register_new("core.system", "org_name", "", TextSettingType(allow_empty=True), "The exact org name of the bot",
                                          "This setting is automatically set by the bot and should not be changed manually")

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

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

        self.setting_service.register_new("core.colors", "org_channel_color", "#89D2E8", ColorSettingType(), "Default org channel color")
        self.setting_service.register_new("core.colors", "private_channel_color", "#89D2E8", ColorSettingType(), "Default private channel color")
        self.setting_service.register_new("core.colors", "private_message_color", "#89D2E8", ColorSettingType(), "Default private message color")
        self.setting_service.register_new("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):
        conn = self.create_conn("main")
        conn.connect(config.server.host, config.server.port)
        packet = conn.login(config.username, config.password, config.character)
        if not packet:
            self.status = BotStatus.ERROR
            return False
        else:
            self.incoming_queue.put((conn, packet))

        self.mass_message_queue = FifoQueue()
        self.create_conn_thread(conn, self.mass_message_queue)

        if "slaves" in config:
            for i, slave in enumerate(config.slaves):
                conn = self.create_conn("slave" + str(i))
                conn.connect(config.server.host, config.server.port)

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

                self.create_conn_thread(conn, 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")

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

    # passthrough
    def send_packet(self, packet):
        self.conns["main"].send_packet(packet)

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

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

        # wait for flood of packets from login to stop sending
        time_waited = 0
        while time_waited < 5:
            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.logger.info("Connect events finished (%fs)" % (time.time() - start))

        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 (EOFError, OSError) as e:
                raise e
            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)
            elif isinstance(packet, server_packets.PublicChannelMessage):
                packet = self.public_channel_message_ext_msg_handling(packet)
            if isinstance(packet, server_packets.BuddyAdded):
                if packet.char_id == 0:
                    return

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

            self.event_service.fire_event("packet:" + str(packet.id), 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)
            self.logger.log_chat("SystemMessage", None, packet.extended_message.get_message())
        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, fire_outgoing_event=True, conn_id="main"):
        org_channel_id = self.public_channel_service.org_channel_id
        if org_channel_id is None:
            self.logger.debug("ignoring message to org channel 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, self.setting_service.get("org_channel_max_page_length").get_value())
            for page in pages:
                packet = client_packets.PublicChannelMessage(org_channel_id, color + page, "")
                self.conns[conn_id].add_packet_to_queue(packet)

            if fire_outgoing_event:
                self.event_service.fire_event(self.OUTGOING_ORG_MESSAGE_EVENT, DictObject({"org_channel_id": org_channel_id,
                                                                                           "message": msg}))

    def send_private_message(self, char, msg, add_color=True, fire_outgoing_event=True, conn_id="main"):
        char_id = self.character_service.resolve_char_to_id(char)
        if char_id is None:
            self.logger.warning("Could not send message to %s, could not find char id" % char)
        else:
            color = self.setting_service.get("private_message_color").get_font_color() if add_color else ""
            pages = self.get_text_pages(msg, self.setting_service.get("private_message_max_page_length").get_value())
            for page in pages:
                self.logger.log_tell("To", self.character_service.get_char_name(char_id), page)
                packet = client_packets.PrivateMessage(char_id, color + page, "\0")
                self.conns[conn_id].add_packet_to_queue(packet)

            if fire_outgoing_event:
                self.event_service.fire_event(self.OUTGOING_PRIVATE_MESSAGE_EVENT, DictObject({"char_id": char_id,
                                                                                               "message": msg}))

    def send_private_channel_message(self, msg, private_channel=None, add_color=True, fire_outgoing_event=True, conn_id="main"):
        if private_channel is None:
            private_channel_id = self.get_char_id()
        else:
            private_channel_id = self.character_service.resolve_char_to_id(private_channel)

        if private_channel_id is None:
            self.logger.warning("Could not send message to private channel %s, could not find private channel" % private_channel)
        else:
            color = self.setting_service.get("private_channel_color").get_font_color() if add_color else ""
            pages = self.get_text_pages(msg, 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")
                self.conns[conn_id].send_packet(packet)

            if fire_outgoing_event and private_channel_id == self.get_char_id():
                self.event_service.fire_event(self.OUTGOING_PRIVATE_CHANNEL_MESSAGE_EVENT, DictObject({"private_channel_id": private_channel_id,
                                                                                                       "message": msg}))

    def send_mass_message(self, char_id, msg, add_color=True):
        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, self.setting_service.get("private_message_max_page_length").get_value())
            for page in pages:
                # self.logger.log_tell("To", self.character_service.get_char_name(char_id), page)
                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.conns["main"].send_packet(packet)

    def handle_private_message(self, conn: Conn, packet: server_packets.PrivateMessage):
        if conn.id != "main":
            return

        self.logger.log_tell("From", self.character_service.get_char_name(packet.char_id), packet.message)
        self.event_service.fire_event(self.PRIVATE_MSG_EVENT, packet)

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

    def is_ready(self):
        return self.ready

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

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

    def load_sql_files(self, paths):
        dirs = flatmap(lambda x: os.walk(x), paths)
        dirs = filter(lambda y: not y[0].endswith("__pycache__"), dirs)

        def get_files(tup):
            return map(lambda x: os.path.join(tup[0], x), tup[2])

        # get files from subdirectories
        files = flatmap(get_files, dirs)
        files = filter(lambda z: z.endswith(".sql"), files)

        base_path = os.getcwd()
        for file in files:
            self.db.load_sql_file(file, base_path)

    def get_char_name(self):
        return self.conns["main"].char_name

    def get_char_id(self):
        return self.conns["main"].char_id
Exemple #18
0
class TowerMessagesController:
    MESSAGE_SOURCE = "tower_attacks"

    TOWER_ATTACK_EVENT = "tower_attack"
    TOWER_VICTORY_EVENT = "tower_victory"

    TOWER_BATTLE_OUTCOME_ID = 42949672962
    ALL_TOWERS_ID = 42949672960

    ATTACK_1 = [
        506, 12753364
    ]  # The %s organization %s just entered a state of war! %s attacked the %s organization %s's tower in %s at location (%d,%d).
    ATTACK_2 = re.compile(
        r"^(.+) just attacked the (clan|neutral|omni) organization (.+)'s tower in (.+) at location \((\d+), (\d+)\).\n$"
    )

    VICTORY_1 = re.compile(
        r"^Notum Wars Update: Victory to the (Clan|Neutral|Omni)s!!!$")
    VICTORY_2 = re.compile(
        r"^The (Clan|Neutral|Omni) organization (.+) attacked the (Clan|Neutral|Omni) (.+) at their base in (.+). The attackers won!!$"
    )
    VICTORY_3 = [
        506, 147506468
    ]  # 'Notum Wars Update: The %s organization %s lost their base in %s.'

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

    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.db = registry.get_instance("db")
        self.text = registry.get_instance("text")
        self.util = registry.get_instance("util")
        self.event_service = registry.get_instance("event_service")
        self.command_alias_service = registry.get_instance(
            "command_alias_service")
        self.pork_service = registry.get_instance("pork_service")
        self.message_hub_service = registry.get_instance("message_hub_service")
        self.playfield_controller: PlayfieldController = registry.get_instance(
            "playfield_controller")

    def pre_start(self):
        self.message_hub_service.register_message_source(self.MESSAGE_SOURCE)

        self.event_service.register_event_type(self.TOWER_ATTACK_EVENT)
        self.event_service.register_event_type(self.TOWER_VICTORY_EVENT)
        self.bot.register_packet_handler(
            server_packets.PublicChannelMessage.id,
            self.handle_public_channel_message)

    def start(self):
        self.db.exec(
            "CREATE TABLE IF NOT EXISTS tower_attacker (id INT PRIMARY KEY AUTO_INCREMENT, att_org_name VARCHAR(50) NOT NULL, att_faction VARCHAR(10) NOT NULL, "
            "att_char_id INT, att_char_name VARCHAR(20) NOT NULL, att_level INT NOT NULL, att_ai_level INT NOT NULL, att_profession VARCHAR(15) NOT NULL, "
            "x_coord INT NOT NULL, y_coord INT NOT NULL, is_victory SMALLINT NOT NULL, "
            "tower_battle_id INT NOT NULL, created_at INT NOT NULL)")
        self.db.exec(
            "CREATE TABLE IF NOT EXISTS tower_battle (id INT PRIMARY KEY AUTO_INCREMENT, playfield_id INT NOT NULL, site_number INT NOT NULL, "
            "def_org_name VARCHAR(50) NOT NULL, def_faction VARCHAR(10) NOT NULL, is_finished INT NOT NULL, battle_type VARCHAR(20) NOT NULL, last_updated INT NOT NULL)"
        )

        self.command_alias_service.add_alias("victory", "attacks")
        self.command_alias_service.add_alias("battles", "attacks")

    @command(command="attacks",
             params=[Const("battle"), Int("battle_id")],
             access_level="all",
             description="Show battle info for a specific battle")
    def attacks_battle_cmd(self, request, _, battle_id):
        battle = self.get_battle(battle_id)
        if not battle:
            return "Could not find battle with ID <highlight>%d</highlight>." % battle_id

        blob = self.check_for_all_towers_channel() + self.get_battle_blob(
            battle)

        return ChatBlob("Battle Info %d" % battle_id, blob)

    @command(command="attacks",
             params=[
                 Any("playfield", is_optional=True, allowed_chars="[a-z0-9 ]"),
                 Int("site_number", is_optional=True),
                 NamedParameters([
                     "defender_org", "attacker_org", "attacker", "victory",
                     "page"
                 ])
             ],
             access_level="all",
             description="Show recent tower attacks and victories",
             extended_description=
             "Victory param can be 'true', 'false', or 'all' (default)")
    def attacks_cmd(self, request, playfield_name, site_number, named_params):
        playfield = None
        if playfield_name:
            playfield = self.playfield_controller.get_playfield_by_name_or_id(
                playfield_name)
            if not playfield:
                return f"Could not find playfield <highlight>{playfield_name}</highlight>."

        page_number = int(named_params.page or "1")
        victory = "all" if named_params.victory == "" else named_params.victory

        command_str = "attacks"
        if playfield_name:
            command_str += " " + playfield_name
            if site_number:
                command_str += " " + str(site_number)

        page_size = 10
        offset = (page_number - 1) * page_size

        sql = "SELECT DISTINCT b.*, p.long_name, p.short_name FROM tower_battle b " \
              "LEFT JOIN playfields p ON b.playfield_id = p.id " \
              "LEFT JOIN tower_attacker a ON b.id = a.tower_battle_id " \
              "WHERE 1=1"
        params = []

        if playfield:
            sql += " AND b.playfield_id = ?"
            params.append(playfield.id)
            if site_number:
                sql += " AND b.site_number = ?"
                params.append(site_number)

        if victory != "all":
            command_str += f" --victory={victory}"
            sql += " AND b.is_finished = ?"
            if victory == "true":
                params.append(1)
            elif victory == "false":
                params.append(0)
            else:
                return "Named param <highlight>--victory</highlight> must be one of: 'true', 'false', or 'all' (default)."

        if named_params.defender_org:
            defender_org = named_params.defender_org
            command_str += f" --defender_org={defender_org}"
            sql += f" AND b.def_org_name <EXTENDED_LIKE={len(params)}> ?"
            params.append(defender_org)

        if named_params.attacker_org:
            attacker_org = named_params.attacker_org
            command_str += f" --attacker_org={attacker_org}"
            sql += f" AND a.att_org_name <EXTENDED_LIKE={len(params)}> ?"
            params.append(attacker_org)

        if named_params.attacker:
            attacker = named_params.attacker
            command_str += f" --attacker={attacker}"
            sql += f" AND a.att_char_name LIKE ? OR a.att_char_id = ?"
            params.append(attacker)
            params.append(attacker)

        sql += " ORDER BY b.last_updated DESC LIMIT ?, ?"
        params.append(offset)
        params.append(page_size)

        data = self.db.query(sql, params, extended_like=True)

        t = int(time.time())

        blob = self.check_for_all_towers_channel()
        blob += self.text.get_paging_links(command_str, page_number,
                                           page_size == len(data))
        blob += "\n\n"
        for row in data:
            blob += "\n<pagebreak>"
            blob += self.format_battle_info(row, t)
            blob += "<header2>Attackers:</header2>\n"
            sql2 = """SELECT a.*, COALESCE(a.att_level, 0) AS att_level, COALESCE(a.att_ai_level, 0) AS att_ai_level
                    FROM tower_attacker a
                    WHERE a.tower_battle_id = ?
                    ORDER BY created_at DESC"""
            data2 = self.db.query(sql2, [row.id])
            for row2 in data2:
                blob += "<tab>" + self.format_attacker(row2)
                if row2.is_victory:
                    blob += " - <notice>Winner!</notice>"
                blob += "\n"
            if not data2:
                blob += "<tab>Unknown attacker\n"

        title = "Tower Attacks"
        if playfield:
            title += f" ({playfield.long_name}"
            if site_number:
                title += " " + str(site_number)
            title += ")"

        return ChatBlob(title, blob)

    @event(event_type="connect",
           description="Check if All Towers channel is available",
           is_system=True)
    def handle_connect_event(self, event_type, event_data):
        conn = self.bot.get_primary_conn()
        if conn.org_id and self.ALL_TOWERS_ID not in conn.channels:
            self.logger.warning(
                "The primary bot is a member of an org but does not have access to 'All Towers' channel and therefore will not be able to record tower attacks"
            )

    def handle_public_channel_message(
            self, conn: Conn, packet: server_packets.PublicChannelMessage):
        # only listen to tower packets from first bot, to avoid triggering multiple times
        if conn != self.bot.get_primary_conn():
            return

        if packet.channel_id == self.TOWER_BATTLE_OUTCOME_ID:
            victory = self.get_victory_event(packet)
            if victory:
                self.fire_victory_event(victory)
        elif packet.channel_id == self.ALL_TOWERS_ID:
            attack = self.get_attack_event(packet)
            if attack:
                self.fire_attack_event(attack)

    def fire_victory_event(self, obj):
        # self.logger.debug("tower victory packet: %s" % str(packet))

        # lookup playfield
        playfield_name = obj.location.playfield.long_name
        obj.location.playfield = self.playfield_controller.get_playfield_by_name(
            playfield_name) or DictObject()
        obj.location.playfield.long_name = playfield_name

        t = int(time.time())
        is_victory = 1
        is_finished = 1

        if obj.type == "attack":
            # get battle id and site_number
            last_updated = t - (6 * 3600)
            row = self.get_last_attack(obj.winner.faction, obj.winner.org_name,
                                       obj.loser.faction, obj.loser.org_name,
                                       obj.location.playfield.id, last_updated)

            if row:
                obj.battle_id = row.battle_id
                obj.location.site_number = row.site_number

                self.db.exec(
                    "UPDATE tower_attacker SET is_victory = ? WHERE id = ?",
                    [is_victory, row.attack_id])
                self.db.exec(
                    "UPDATE tower_battle SET is_finished = ?, last_updated = ? WHERE id = ?",
                    [is_finished, t, row.battle_id])
            else:
                obj.location.site_number = 0
                self.db.exec(
                    "INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)",
                    [
                        obj.location.playfield.id, obj.location.site_number,
                        obj.loser.org_name, obj.loser.faction, is_finished,
                        obj.type, t
                    ])
                obj.battle_id = self.db.last_insert_id()

                attacker = obj.winner or {}
                self.db.exec(
                    "INSERT INTO tower_attacker (att_org_name, att_faction, att_char_id, att_char_name, att_level, att_ai_level, att_profession, "
                    "x_coord, y_coord, is_victory, tower_battle_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
                    [
                        attacker.get("org_name", ""),
                        attacker.get("faction", ""),
                        attacker.get("char_id", 0),
                        attacker.get("name", ""),
                        attacker.get("level", 0),
                        attacker.get("ai_level", 0),
                        attacker.get("profession", ""), 0, 0, is_victory,
                        obj.battle_id, t
                    ])

        elif obj.type == "terminated":
            obj.location.site_number = 0
            self.db.exec(
                "INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)",
                [
                    obj.location.playfield.id, obj.location.site_number,
                    obj.loser.org_name, obj.loser.faction, is_finished,
                    obj.type, t
                ])
            obj.battle_id = self.db.last_insert_id()
        else:
            raise Exception("Unknown victory event type: '%s'" % obj.type)

        self.event_service.fire_event(self.TOWER_VICTORY_EVENT, obj)

    def fire_attack_event(self, obj):
        # self.logger.debug("tower attack packet: %s" % str(packet))

        # lookup playfield
        playfield_name = obj.location.playfield.long_name
        obj.location.playfield = self.playfield_controller.get_playfield_by_name(
            playfield_name) or DictObject()
        obj.location.playfield.long_name = playfield_name

        # lookup attacker
        name = obj.attacker.name
        faction = obj.attacker.faction
        org_name = obj.attacker.org_name
        char_info = self.pork_service.get_character_info(name)
        obj.attacker = char_info or DictObject()
        obj.attacker.name = name
        obj.attacker.faction = faction or obj.attacker.get(
            "faction", "Unknown")
        obj.attacker.org_name = org_name

        obj.location.site_number = self.find_closest_site_number(
            obj.location.playfield.id, obj.location.x_coord,
            obj.location.y_coord)

        attacker = obj.attacker or {}
        defender = obj.defender
        location = obj.location

        t = int(time.time())
        battle = self.find_or_create_battle(obj.location.playfield.id,
                                            obj.location.site_number,
                                            defender.org_name,
                                            defender.faction, "attack", t)
        obj.battle_id = battle.id

        self.db.exec(
            "INSERT INTO tower_attacker (att_org_name, att_faction, att_char_id, att_char_name, att_level, att_ai_level, att_profession, "
            "x_coord, y_coord, is_victory, tower_battle_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
            [
                attacker.get("org_name", ""),
                attacker.get("faction", ""),
                attacker.get("char_id", 0),
                attacker.get("name", ""),
                attacker.get("level", 0),
                attacker.get("ai_level", 0),
                attacker.get("profession", ""), location.x_coord,
                location.y_coord, 0, battle.id, t
            ])
        attacker_id = self.db.last_insert_id()

        attacker_row = self.db.query_single(
            "SELECT * FROM tower_attacker WHERE id = ?", [attacker_id])
        more_info = self.text.paginate_single(
            ChatBlob(
                "More Info",
                self.text.make_tellcmd("More Info",
                                       f"attacks battle {battle.id}")),
            self.bot.get_primary_conn())
        msg = "%s attacked %s [%s] at %s %s %s" % (
            self.format_attacker(attacker_row), defender.org_name,
            self.text.get_formatted_faction(defender.faction),
            location.playfield.get("short_name",
                                   location.playfield.get("long_name")),
            obj.location.site_number or "?", more_info)
        self.message_hub_service.send_message(self.MESSAGE_SOURCE, None, None,
                                              msg)

        self.event_service.fire_event(self.TOWER_ATTACK_EVENT, obj)

    def get_attack_event(self, packet: server_packets.PublicChannelMessage):
        t = int(time.time())

        if packet.extended_message and [
                packet.extended_message.category_id,
                packet.extended_message.instance_id
        ] == self.ATTACK_1:
            params = packet.extended_message.params
            return DictObject({
                "timestamp": t,
                "attacker": {
                    "name": params[2],
                    "faction": params[0].capitalize(),
                    "org_name": params[1]
                },
                "defender": {
                    "faction": params[3].capitalize(),
                    "org_name": params[4]
                },
                "location": {
                    "playfield": {
                        "long_name": params[5]
                    },
                    "x_coord": params[6],
                    "y_coord": params[7]
                }
            })
        else:
            match = self.ATTACK_2.match(packet.message)
            if match:
                return DictObject({
                    "timestamp": t,
                    "attacker": {
                        "name": match.group(1),
                        "faction": "",
                        "org_name": ""
                    },
                    "defender": {
                        "faction": match.group(2).capitalize(),
                        "org_name": match.group(3)
                    },
                    "location": {
                        "playfield": {
                            "long_name": match.group(4)
                        },
                        "x_coord": match.group(5),
                        "y_coord": match.group(6)
                    }
                })

        # Unknown attack
        self.logger.warning("Unknown tower attack: " + str(packet))
        return None

    def get_victory_event(self, packet: server_packets.PublicChannelMessage):
        match = self.VICTORY_1.match(packet.message)
        if match:
            return None

        t = int(time.time())

        match = self.VICTORY_2.match(packet.message)
        if match:
            return DictObject({
                "type": "attack",
                "timestamp": t,
                "winner": {
                    "faction": match.group(1).capitalize(),
                    "org_name": match.group(2)
                },
                "loser": {
                    "faction": match.group(3).capitalize(),
                    "org_name": match.group(4)
                },
                "location": {
                    "playfield": {
                        "long_name": match.group(5)
                    }
                }
            })

        if packet.extended_message and [
                packet.extended_message.category_id,
                packet.extended_message.instance_id
        ] == self.VICTORY_3:
            params = packet.extended_message.params
            return DictObject({
                "type": "terminated",
                "timestamp": t,
                "winner": {
                    "faction": params[0].capitalize(),
                    "org_name": params[1]
                },
                "loser": {
                    "faction": params[0].capitalize(),
                    "org_name": params[1]
                },
                "location": {
                    "playfield": {
                        "long_name": params[2]
                    }
                }
            })

        # Unknown victory
        self.logger.warning("Unknown tower victory: " + str(packet))
        return None

    def format_attacker(self, row):
        level = ("%d/<green>%d</green>" % (row.att_level, row.att_ai_level)
                 ) if row.att_ai_level > 0 else "%d" % row.att_level
        org = row.att_org_name + " " if row.att_org_name else ""
        return "%s (%s %s) %s[%s]" % (
            row.att_char_name or "Unknown attacker", level, row.att_profession
            or "Unknown", org, self.text.get_formatted_faction(
                row.att_faction))

    def find_closest_site_number(self, playfield_id, x_coord, y_coord):
        sql = "SELECT site_number FROM tower_site_bounds " \
              "WHERE playfield_id = ? AND x_coord1 <= ? AND x_coord2 >= ? AND y_coord1 >= ? AND y_coord2 <= ? " \
              "LIMIT 1"
        row = self.db.query_single(
            sql, [playfield_id, x_coord, x_coord, y_coord, y_coord])
        if row:
            return row.site_number

        sql = """
            SELECT
                site_number,
                ((x_distance * x_distance) + (y_distance * y_distance)) radius
            FROM
                (SELECT
                    playfield_id,
                    site_number,
                    min_ql,
                    max_ql,
                    x_coord,
                    y_coord,
                    site_name,
                    (x_coord - ?) as x_distance,
                    (y_coord - ?) as y_distance
                FROM
                    tower_site
                WHERE
                    playfield_id = ?) t
            ORDER BY
                radius ASC
            LIMIT 1"""

        row = self.db.query_single(sql, [x_coord, y_coord, playfield_id])
        if row:
            return row.site_number
        else:
            return 0

    def find_or_create_battle(self, playfield_id, site_number, org_name,
                              faction, battle_type, t):
        last_updated = t - (8 * 3600)
        is_finished = 0

        sql = """
            SELECT
                id
            FROM
                tower_battle
            WHERE
                playfield_id = ?
                AND site_number = ?
                AND is_finished = ?
                AND def_org_name = ?
                AND def_faction = ?
                AND last_updated >= ?
        """

        battle = self.db.query_single(sql, [
            playfield_id, site_number, is_finished, org_name, faction,
            last_updated
        ])

        if battle:
            self.db.exec(
                "UPDATE tower_battle SET last_updated = ? WHERE id = ?",
                [t, battle.id])
            battle_id = battle.id
        else:
            self.db.exec(
                "INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)",
                [
                    playfield_id, site_number, org_name, faction, is_finished,
                    battle_type, t
                ])
            battle_id = self.db.last_insert_id()

        return self.get_battle(battle_id)

    def get_last_attack(self, att_faction, att_org_name, def_faction,
                        def_org_name, playfield_id, last_updated):
        is_finished = 0

        sql = """
            SELECT
                b.id AS battle_id,
                a.id AS attack_id,
                b.site_number
            FROM
                tower_battle b
                JOIN tower_attacker a ON
                    a.tower_battle_id = b.id 
            WHERE
                a.att_faction = ?
                AND a.att_org_name = ?
                AND b.def_faction = ?
                AND b.def_org_name = ?
                AND b.playfield_id = ?
                AND b.is_finished = ?
                AND b.last_updated >= ?
            ORDER BY
                last_updated DESC
            LIMIT 1"""

        return self.db.query_single(sql, [
            att_faction, att_org_name, def_faction, def_org_name, playfield_id,
            is_finished, last_updated
        ])

    def format_battle_info(self, row, t, verbose=False):
        blob = ""
        defeated = " - <notice>Defeated!</notice>" if row.is_finished else ""
        blob += f"Site: %s " % self.text.make_tellcmd(
            f"{row.short_name} {row.site_number}",
            f"lc {row.short_name} {row.site_number}")
        if not verbose:
            blob += self.text.make_tellcmd("More Info",
                                           "attacks battle %d" % row.id)
        blob += "\n"
        if verbose:
            if row.site_number:
                blob += f"Long name: <highlight>{row.site_name}, {row.long_name}</highlight>\n"
                blob += f"Level range: <highlight>{row.min_ql}-{row.max_ql}</highlight>\n"
                blob += "Coordinates: %s\n" % self.text.make_chatcmd(
                    f"{row.x_coord}x{row.y_coord}",
                    f"/waypoint {row.x_coord} {row.y_coord} {row.playfield_id}"
                )
            else:
                blob += f"Long name: Unknown\n"
                blob += f"Level range: Unknown\n"
                blob += "Coordinates: Unknown\n"
        blob += f"Defender: %s [%s]%s\n" % (row.def_org_name,
                                            self.text.get_formatted_faction(
                                                row.def_faction), defeated)
        blob += "Last Activity: %s\n" % self.format_timestamp(
            row.last_updated, t)
        return blob

    def format_timestamp(self, t, current_t):
        return "<highlight>%s</highlight> (%s ago)" % (
            self.util.format_datetime(t),
            self.util.time_to_readable(current_t - t))

    def check_for_all_towers_channel(self):
        if self.ALL_TOWERS_ID not in self.bot.get_primary_conn().channels:
            return "Notice: The primary bot must belong to an org and be promoted to a rank that is high enough to have the All Towers channel (e.g., Squad Commander) in order for this command to work correctly.\n\n"
        else:
            return ""

    def get_battle_blob(self, battle):
        t = int(time.time())

        attackers = self.db.query(
            "SELECT * FROM tower_attacker WHERE tower_battle_id = ? ORDER BY created_at DESC",
            [battle.id])

        first_activity = attackers[-1].created_at if len(
            attackers) > 0 else battle.last_updated

        blob = ""
        blob += self.format_battle_info(battle, t, verbose=True)
        blob += "Duration: <highlight>%s</highlight>\n\n" % self.util.time_to_readable(
            battle.last_updated - first_activity)
        blob += "<header2>Attackers:</header2>\n"

        for row in attackers:
            blob += "<tab>" + self.format_attacker(row)
            blob += " " + self.format_timestamp(row.created_at, t)
            if row.is_victory:
                blob += " - <notice>Winner!</notice>"
            blob += "\n"

        return blob

    def get_battle(self, battle_id):
        return self.db.query_single(
            "SELECT b.*, p.short_name, p.long_name, t.site_name, t.x_coord, t.y_coord, t.min_ql, t.max_ql "
            "FROM tower_battle b "
            "LEFT JOIN playfields p ON p.id = b.playfield_id "
            "LEFT JOIN tower_site t ON b.playfield_id = t.playfield_id AND b.site_number = t.site_number "
            "WHERE b.id = ?", [battle_id])
Exemple #19
0
class BuddyService:
    BUDDY_LOGON_EVENT = "buddy_logon"
    BUDDY_LOGOFF_EVENT = "buddy_logoff"

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

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

    def pre_start(self):
        self.bot.register_packet_handler(server_packets.BuddyAdded.id,
                                         self.handle_add)
        self.bot.register_packet_handler(server_packets.BuddyRemoved.id,
                                         self.handle_remove)
        self.bot.register_packet_handler(server_packets.LoginOK.id,
                                         self.handle_login_ok)
        self.event_service.register_event_type(self.BUDDY_LOGON_EVENT)
        self.event_service.register_event_type(self.BUDDY_LOGOFF_EVENT)

    def handle_add(self, conn: Conn, packet: server_packets.BuddyAdded):
        if packet.char_id == 0:
            self.logger.warning("Buddy added or updated with char_id '0'")
            return

        buddy = conn.buddy_list.get(packet.char_id, {
            "types": [],
            "conn_id": conn.id
        })
        buddy["online"] = packet.online
        conn.buddy_list[packet.char_id] = buddy

        # verify that buddy does not exist on any other conn
        for conn_id, other_conn in self.bot.get_conns():
            if conn.id == conn_id:
                continue

            buddy = other_conn.buddy_list.get(packet.char_id, None)
            if buddy:
                # remove from other conn list
                del other_conn.buddy_list[packet.char_id]

                self.logger.warning(
                    "Removing char '%s' from conn '%s' since it already exists on another conn"
                    % (packet.char_id, conn.id))
                other_conn.send_packet(
                    client_packets.BuddyRemove(packet.char_id))

        if packet.online == 1:
            self.event_service.fire_event(self.BUDDY_LOGON_EVENT, packet)
        else:
            self.event_service.fire_event(self.BUDDY_LOGOFF_EVENT, packet)

    def handle_remove(self, conn: Conn, packet: server_packets.BuddyRemoved):
        if packet.char_id == 0:
            self.logger.warning("Buddy removed with char_id '0'")

        if packet.char_id in conn.buddy_list:
            if len(conn.buddy_list[packet.char_id]["types"]) > 0:
                self.logger.warning(
                    "Removing buddy %d that still has types %s" %
                    (packet.char_id, conn.buddy_list[packet.char_id]["types"]))

            del conn.buddy_list[packet.char_id]

    def handle_login_ok(self, conn: Conn, packet):
        self.buddy_list_size += 1000
        # conn.buddy_list[conn.char_id] = {"online": True, "types": ["conn"], "conn_id": conn.id}

    def add_buddy(self, char_id, _type):
        if not char_id:
            return False

        # check if we are trying to add a conn as a buddy
        if self.is_conn_char_id(char_id):
            return False

        buddy = self.get_buddy(char_id)
        if buddy:
            buddy["types"].append(_type)
        else:
            conn = self.get_conn_for_new_buddy()
            # TODO send ChatCommand packet in order to get back response - use FeatureFlag
            conn.send_packet(client_packets.BuddyAdd(char_id, "\1"))
            conn.buddy_list[char_id] = {
                "online": None,
                "types": [_type],
                "conn_id": conn.id
            }

        return True

    def is_conn_char_id(self, char_id):
        for _id, conn in self.bot.get_conns():
            if conn.char_id == char_id:
                return True

        return False

    def remove_buddy(self, char_id, _type, force_remove=False):
        if not char_id:
            return False

        for _id, conn in self.bot.get_conns():
            if char_id == conn.char_id:
                continue

            buddy = conn.buddy_list.get(char_id, None)
            if buddy:
                if _type in buddy["types"]:
                    buddy["types"].remove(_type)

                if len(buddy["types"]) == 0 or force_remove:
                    conn = self.bot.conns[buddy["conn_id"]]
                    conn.send_packet(client_packets.BuddyRemove(char_id))

        return True

    def get_buddy(self, char_id):
        for _id, conn in self.bot.get_conns():
            if conn.char_id == char_id:
                return {"online": 1, "types": ["conn"], "conn_id": conn.id}
            elif char_id in conn.buddy_list:
                return conn.buddy_list[char_id]
        return None

    def is_online(self, char_id):
        buddy = self.get_buddy(char_id)
        if buddy is None:
            return None
        else:
            return buddy.get("online", None)

    def get_all_buddies(self):
        result = {}
        for _id, conn in self.bot.get_conns():
            for char_id, buddy in conn.buddy_list.items():
                # TODO what if buddies exist on multiple conns?
                result[char_id] = buddy

        return result

    def remove_all_buddies_by_type(self, _type):
        buddies = filter(lambda obj: _type in obj["types"],
                         self.get_all_buddies())
        for char_id, buddy in buddies.items():
            buddy["types"].remove(_type)

            if len(buddy["types"]) == 0:
                conn = self.bot.conns[buddy["conn_id"]]
                conn.send_packet(client_packets.BuddyRemove(char_id))

    def get_buddy_list_size(self):
        count = 0
        for _id, conn in self.bot.get_conns():
            count += len(conn.buddy_list)

        return count

    def get_conn_for_new_buddy(self):
        buddy_list_size = None
        selected_conn = None
        for _id, conn in self.bot.get_conns():
            if buddy_list_size is None or len(
                    conn.buddy_list) < buddy_list_size:
                buddy_list_size = len(conn.buddy_list)
                selected_conn = conn

        return selected_conn
Exemple #20
0
class OrgPorkService:
    CACHE_GROUP = "org_roster"
    CACHE_MAX_AGE = 86400

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

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

    def get_org_info(self, org_id):
        cache_key = "%d.%d.json" % (org_id, self.bot.dimension)

        # check cache for fresh value
        cache_result = self.cache_service.retrieve(self.CACHE_GROUP, cache_key,
                                                   self.CACHE_MAX_AGE)

        is_cache = False
        if cache_result:
            result = json.loads(cache_result)
            is_cache = True
        else:
            url = "http://people.anarchy-online.com/org/stats/d/%d/name/%d/basicstats.xml?data_type=json" % (
                self.bot.dimension, org_id)

            r = requests.get(url)
            try:
                result = r.json()

                # if org has no members, org does not exist
                if result[0]["NUMMEMBERS"] == 0:
                    result = None
            except ValueError as e:
                self.logger.warning(
                    "Error marshalling value as json: %s" % r.text, 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
                cache_result = self.cache_service.retrieve(
                    self.CACHE_GROUP, cache_key)
                if cache_result:
                    result = json.loads(cache_result)
                    is_cache = True

        if not result:
            return None

        org_info = result[0]
        org_members = result[1]
        last_updated = result[2]

        new_org_info = DictObject({
            "counts": {
                "gender": {
                    "Female": org_info["FEMALECOUNT"],
                    "Male": org_info["MALECOUNT"],
                    "Neuter": org_info["NEUTERCOUNT"],
                },
                "breed": {
                    "Atrox": org_info["ATROXCOUNT"],
                    "Nanomage": org_info["NANORACECOUNT"],
                    "Opifex": org_info["OPIFEXCOUNT"],
                    "Solitus": org_info["SOLITUSCOUNT"],
                },
                "profession": {
                    "Monster": org_info["MONSTERCOUNT"],
                    "Adventurer": org_info["ADVENTURERCOUNT"],
                    "Agent": org_info["AGENTCOUNT"],
                    "Bureaucrat": org_info["BTCOUNT"],
                    "Doctor": org_info["DOCTORCOUNT"],
                    "Enforcer": org_info["ENFCOUNT"],
                    "Engineer": org_info["ENGINEEERCOUNT"],
                    "Fixer": org_info["FIXERCOUNT"],
                    "Keeper": org_info["KEEPERCOUNT"],
                    "Martial Artist": org_info["MACOUNT"],
                    "Meta-Physicist": org_info["METACOUNT"],
                    "Nano-Technician": org_info["NANOCOUNT"],
                    "Shade": org_info["SHADECOUNT"],
                    "Soldier": org_info["SOLIDERCOUNT"],
                    "Trader": org_info["TRADERCOUNT"],
                }
            },
            "min_level": org_info["MINLVL"],
            "num_members": org_info["NUMMEMBERS"],
            "dimension": org_info["ORG_DIMENSION"],
            "governing_type": org_info["GOVERNINGNAME"],
            "max_level": org_info["MAXLVL"],
            "org_id": org_info["ORG_INSTANCE"],
            "objective": org_info["OBJECTIVE"],
            "description": org_info["DESCRIPTION"],
            "history": org_info["HISTORY"],
            "avg_level": org_info["AVGLVL"],
            "name": org_info["NAME"],
            "faction": org_info["SIDE_NAME"],
            "faction_id": org_info["SIDE"],
        })

        with self.db.transaction():
            members = {}
            for org_member in org_members:
                char_info = DictObject({
                    "name":
                    org_member["NAME"],
                    "char_id":
                    org_member["CHAR_INSTANCE"],
                    "first_name":
                    org_member["FIRSTNAME"],
                    "last_name":
                    org_member["LASTNAME"],
                    "level":
                    org_member["LEVELX"],
                    "breed":
                    org_member["BREED"],
                    "dimension":
                    org_member["CHAR_DIMENSION"],
                    "gender":
                    org_member["SEX"],
                    "faction":
                    org_info["SIDE_NAME"],
                    "profession":
                    org_member["PROF"],
                    "profession_title":
                    org_member["PROF_TITLE"],
                    "ai_rank":
                    org_member["DEFENDER_RANK_TITLE"],
                    "ai_level":
                    org_member["ALIENLEVEL"],
                    "pvp_rating":
                    org_member["PVPRATING"],
                    "pvp_title":
                    none_to_empty_string(org_member["PVPTITLE"]),
                    "head_id":
                    org_member["HEADID"],
                    "org_id":
                    org_info.get("ORG_INSTANCE", 0),
                    "org_name":
                    org_info.get("NAME", ""),
                    "org_rank_name":
                    org_member.get("RANK_TITLE", ""),
                    "org_rank_id":
                    org_member.get("RANK", 0),
                    "source":
                    "people.anarchy-online.com"
                })

                if not is_cache:
                    self.pork_service.save_character_info(char_info)

                # prefetch char ids from chat server
                self.character_service._send_lookup_if_needed(char_info.name)

                members[char_info.char_id] = char_info

        if len(members) == 0:
            return None
        else:
            return DictObject({
                "org_info":
                new_org_info,
                "org_members":
                members,
                "last_updated":
                int(
                    datetime.datetime.strptime(
                        last_updated, "%Y/%m/%d %H:%M:%S").timestamp())
            })
Exemple #21
0
class MigrateController:
    DATABASE_TYPE_MYSQL = "mysql"
    DATABASE_TYPE_SQLITE = "sqlite"

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

    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.db = registry.get_instance("db")
        self.character_service = registry.get_instance("character_service")
        self.alts_service = registry.get_instance("alts_service")
        self.pork_service = registry.get_instance("pork_service")

    @event(event_type="connect",
           description="Configure migration controller",
           is_system=True)
    def connect_event(self, event_type, event_data):
        self.db2 = DB()

        # Optional: the name of the bot character that the budabot/bebot ran as
        # if the bot name is the same, then you can leave this blank, otherwise you must fill in this value
        bot_name = ""

        # Optional: the org_id of the org
        # the bot will use the org_id of the primary conn if this is not set, which is usually correct
        org_id = 0

        # if your budabot/bebot used mysql, then uncomment the second line below and fill out the appropriate values
        # otherwise, if your budabot used sqlite, then uncomment the first line below and enter the path to the sqlite db file
        # do NOT uncomment both of them
        # REQUIRED: uncomment ONE of these two lines below

        # self.db2.connect_sqlite("./data/budabot.db")
        # self.db2.connect_mysql(host="localhost", port=3306, username="", password="", database_name="")

        self.bot_name = bot_name.lower(
        ) if bot_name else self.bot.get_primary_conn().get_char_name().lower()
        self.org_id = org_id if org_id else self.bot.get_primary_conn().org_id
        self.dimension = self.bot.dimension

        # TODO in each command, check if db has been initialized properly first

    @command(command="bebot",
             params=[Const("migrate"), Const("alts")],
             access_level="superadmin",
             description="Migrate alts from a Bebot database")
    def migrate_bebot_alts_cmd(self, request, _1, _2):
        data = self.db2.query(
            "SELECT a.alt, u1.char_id AS alt_char_id, a.main, u2.char_id AS main_char_id "
            "FROM alts a "
            f"LEFT JOIN {self.bot_name}_users u1 ON a.alt = u1.nickname "
            f"LEFT JOIN {self.bot_name}_users u2 ON a.main = u2.nickname "
            "WHERE a.confirmed = 1 "
            "ORDER BY main, alt")
        current_main = None
        current_main_id = None
        count_inactive = 0
        count_success = 0
        count_failure = 0

        request.reply("Processing %s alt records..." % len(data))

        for row in data:
            if row.main != current_main:
                current_main = row.main
                current_main_id = self.resolve_to_char_id(
                    row.main, row.main_char_id)

            if not current_main_id:
                self.logger.warning(
                    f"Could not resolve main char '{current_main}' to char id")
                count_inactive += 1
                continue

            alt_id = self.resolve_to_char_id(row.alt, row.alt_char_id)
            if not alt_id:
                self.logger.warning(
                    f"Could not resolve alt char '{row.alt}' to char id")
                count_inactive += 1
                continue

            msg, result = self.alts_service.add_alt(current_main_id, alt_id)
            if result:
                count_success += 1
            else:
                count_failure += 1

        return f"<highlight>{count_success}</highlight> alts were migrated successfully, " \
               f"<highlight>{count_failure}</highlight> alts failed to be added, " \
               f"and <highlight>{count_inactive}</highlight> chars were inactive and could not be resolved to char ids."

    @command(command="bebot",
             params=[Const("migrate"), Const("logon")],
             access_level="superadmin",
             description="Migrate logon messages from a Bebot database")
    def migrate_bebot_logon_cmd(self, request, _1, _2):
        data = self.db2.query(f"SELECT message, id FROM {self.bot_name}_logon")

        request.reply("Processing %s logon records..." % len(data))

        for row in data:
            self.db.exec(
                "INSERT INTO log_messages (char_id, logon) VALUES(?, ?)",
                [row.id, row.message])

        return f"Successfully migrated <highlight>%d</highlight> logon messages." % len(
            data)

    @command(command="budabot",
             params=[Const("migrate"), Const("admins")],
             access_level="superadmin",
             description="Migrate admins from a Budabot database")
    def migrate_budabot_admins_cmd(self, request, _1, _2):
        data = self.db2.query(
            "SELECT a.name, p.charid AS char_id, CASE WHEN adminlevel = 4 THEN 'admin' WHEN adminlevel = 3 THEN 'moderator' END AS access_level "
            f"FROM admin_{self.bot_name} a LEFT JOIN players p ON (a.name = p.name AND p.dimension = ?) "
            "WHERE p.charid > 0", [self.dimension])
        with self.db.transaction():
            for row in data:
                char_id = self.resolve_to_char_id(row.name, row.char_id)

                if char_id and row.access_level:
                    self.db.exec("DELETE FROM admin WHERE char_id = ?",
                                 [char_id])
                    self.db.exec(
                        "INSERT INTO admin (char_id, access_level) VALUES (?, ?)",
                        [char_id, row.access_level])

        return f"Successfully migrated <highlight>%d</highlight> admin characters." % len(
            data)

    @command(command="budabot",
             params=[Const("migrate"), Const("banlist")],
             access_level="superadmin",
             description="Migrate ban list from a Budabot database")
    def migrate_budabot_banlist_cmd(self, request, _1, _2):
        data = self.db2.query(
            "SELECT b.charid AS char_id, p.charid AS sender_char_id, time AS created_at, banend AS finished_at, reason "
            f"FROM banlist_{self.bot_name} b JOIN players p ON (b.admin = p.name AND p.dimension = ?)"
            "WHERE p.charid > 0", [self.dimension])
        with self.db.transaction():
            for row in data:
                self.db.exec("DELETE FROM ban_list WHERE char_id = ?",
                             [row.char_id])
                self.db.exec(
                    "INSERT INTO ban_list (char_id, sender_char_id, created_at, finished_at, reason, ended_early) VALUES (?, ?, ?, ?, ?, ?)",
                    [
                        row.char_id, row.sender_char_id, row.created_at,
                        row.finished_at, row.reason, 0
                    ])

        return f"Successfully migrated <highlight>%d</highlight> banned characters." % len(
            data)

    @command(command="budabot",
             params=[Const("migrate"), Const("alts")],
             access_level="superadmin",
             description="Migrate alts from a Budabot database")
    def migrate_budabot_alts_cmd(self, request, _1, _2):
        data = self.db2.query(
            "SELECT p1.charid AS main_char_id, p2.charid AS alt_char_id "
            "FROM alts a JOIN players p1 ON (p1.name = a.main AND p1.dimension = ?) "
            "JOIN players p2 ON (p2.name = a.alt AND p2.dimension = ?)"
            "WHERE validated = 1 AND p1.charid > 0 AND p2.charid > 0 ORDER BY a.main ASC",
            [self.dimension, self.dimension])
        with self.db.transaction():
            current_main = 0
            group_id = 0
            for row in data:
                if row.main_char_id != current_main:
                    current_main = row.main_char_id
                    group_id = self.db.query_single(
                        "SELECT (COALESCE(MAX(group_id), 0) + 1) AS next_group_id FROM alts"
                    ).next_group_id
                    self.db.exec("DELETE FROM alts WHERE char_id = ?",
                                 [row.main_char_id])
                    self.db.exec(
                        "INSERT INTO alts (char_id, group_id, status) VALUES (?, ?, ?)",
                        [row.main_char_id, group_id, AltsService.MAIN])

                self.db.exec("DELETE FROM alts WHERE char_id = ?",
                             [row.alt_char_id])
                self.db.exec(
                    "INSERT INTO alts (char_id, group_id, status) VALUES (?, ?, ?)",
                    [row.alt_char_id, group_id, AltsService.CONFIRMED])

        return f"Successfully migrated <highlight>%d</highlight> alt characters." % len(
            data)

    @command(command="budabot",
             params=[Const("migrate"), Const("members")],
             access_level="superadmin",
             description="Migrate members from a Budabot database")
    def migrate_budabot_members_cmd(self, request, _1, _2):
        data = self.db2.query(
            "SELECT m.name AS sender, p.charid AS char_id, m.autoinv AS auto_invite "
            f"FROM members_{self.bot_name} m JOIN players p ON (m.name = p.name AND p.dimension = ?) "
            "WHERE p.charid > 0", [self.dimension])

        num = 0
        for row in data:
            char_id = self.resolve_to_char_id(row.sender, row.char_id)

            if char_id:
                num += 1
                self.db.exec("DELETE FROM member WHERE char_id = ?",
                             [row.char_id])
                self.db.exec(
                    "INSERT INTO member (char_id, auto_invite) VALUES (?, ?)",
                    [row.char_id, row.auto_invite])

        return f"Successfully migrated <highlight>{num}</highlight> members."

    @command(command="budabot",
             params=[Const("migrate"), Const("quotes")],
             access_level="superadmin",
             description="Migrate quotes from a Budabot database")
    def migrate_budabot_quotes_cmd(self, request, _1, _2):
        data = self.db2.query(
            "SELECT q.poster, p.charid AS char_id, q.id, q.msg, q.dt "
            "FROM quote q LEFT JOIN players p ON (q.poster = p.name AND p.dimension = ?)",
            [self.dimension])
        count_inactive = 0

        request.reply("Processing %s quote records..." % len(data))

        for row in data:
            char_id = self.resolve_to_char_id(row.poster, row.char_id)

            if not char_id:
                char_id = -1
                count_inactive += 1

            self.db.exec("DELETE FROM quote WHERE id = ?", [row.id])
            self.db.exec(
                "INSERT INTO quote (id, char_id, created_at, content) VALUES (?, ?, ?, ?)",
                [row.id, char_id, row.dt, row.msg])

        return f"Quotes successfully migrated. <highlight>{count_inactive}</highlight> posters were inactive and could not be resolved to char ids."

    @command(command="budabot",
             params=[Const("migrate"), Const("log_messages")],
             access_level="superadmin",
             description="Migrate log messages from a Budabot database")
    def migrate_budabot_log_messages_cmd(self, request, _1, _2):
        data = self.db2.query(
            f"SELECT p2.charid AS char_id, p1.sender, p1.name, p1.value "
            f"FROM preferences_{self.bot_name} p1 LEFT JOIN players p2 ON (p1.sender = p2.name AND p2.dimension = ?) "
            "WHERE p1.name = 'logon_msg' OR p1.name = 'logoff_msg'",
            [self.dimension])
        count_inactive = 0
        count_logon = 0
        count_logoff = 0

        request.reply("Processing %s log messages records..." % len(data))

        for row in data:
            char_id = self.resolve_to_char_id(row.sender, row.char_id)

            if not char_id:
                count_inactive += 1
            else:
                existing = self.db.query_single(
                    "SELECT 1 FROM log_messages WHERE char_id = ?", [char_id])

                if not existing:
                    self.db.exec(
                        "INSERT INTO log_messages (char_id, logon, logoff) VALUES (?, NULL, NULL)",
                        [char_id])

                if row.name == 'logon_msg' and row.value:
                    self.db.exec(
                        "UPDATE log_messages SET logon = ? WHERE char_id = ?",
                        [row.value, char_id])
                    count_logon += 1
                elif row.name == 'logoff_msg' and row.value:
                    self.db.exec(
                        "UPDATE log_messages SET logoff = ? WHERE char_id = ?",
                        [row.value, char_id])
                    count_logoff += 1

        return f"<highlight>{count_logon}</highlight> logon and <highlight>{count_logoff}</highlight> logoff messages successfully migrated. " \
               f"<highlight>{count_inactive}</highlight> messages were from inactive characters that could not be resolved to char ids."

    @command(command="budabot",
             params=[Const("migrate"), Const("name_history")],
             access_level="superadmin",
             description="Migrate name history from a Budabot database")
    def migrate_budabot_name_history_cmd(self, request, _1, _2):
        data = self.db2.query(
            "SELECT charid AS char_id, name, dt AS created_at FROM name_history"
        )

        request.reply(
            "Processing %s name history records. This may take some time..." %
            len(data))

        with self.db.transaction():
            for row in data:
                self.db.exec(
                    "DELETE FROM name_history WHERE char_id = ? AND name = ?",
                    [row.char_id, row.name])
                self.db.exec(
                    "INSERT INTO name_history (char_id, name, created_at) VALUES (?, ?, ?)",
                    [row.char_id, row.name, row.created_at])

        return f"Successfully migrated <highlight>%d</highlight> name history records." % len(
            data)

    @command(command="budabot",
             params=[Const("migrate"), Const("news")],
             access_level="superadmin",
             description="Migrate news from a Budabot database")
    def migrate_budabot_news_cmd(self, request, _1, _2):
        data = self.db2.query(
            "SELECT n.name AS poster, p.charid AS char_id, news, sticky, time AS created_at, deleted AS deleted_at "
            "FROM news n JOIN players p ON (n.name = p.name AND p.dimension = ?) WHERE p.charid > 0",
            [self.dimension])
        for row in data:
            char_id = self.resolve_to_char_id(row.poster, row.char_id)

            if not char_id:
                char_id = -1

            self.db.exec("DELETE FROM news WHERE char_id = ? AND news = ?",
                         [char_id, row.news])
            self.db.exec(
                "INSERT INTO news (char_id, news, sticky, created_at, deleted_at) VALUES (?, ?, ?, ?, ?)",
                [
                    char_id, row.news, row.sticky, row.created_at,
                    row.deleted_at
                ])

        return f"Successfully migrated <highlight>%d</highlight> news records." % len(
            data)

    @command(command="budabot",
             params=[Const("migrate"), Const("notes")],
             access_level="superadmin",
             description="Migrate notes from a Budabot database")
    def migrate_budabot_notes_cmd(self, request, _1, _2):
        data = self.db2.query(
            "SELECT n.added_by AS sender, p.charid AS char_id, n.note, n.dt AS created_at "
            "FROM notes n JOIN players p ON (p.name = n.added_by AND p.dimension = ?) WHERE p.charid > 0",
            [self.dimension])

        num = 0
        for row in data:
            char_id = self.resolve_to_char_id(row.sender, row.char_id)

            if char_id:
                num += 1
                self.db.exec(
                    "DELETE FROM notes WHERE char_id = ? AND note = ?",
                    [char_id, row.note])
                self.db.exec(
                    "INSERT INTO notes (char_id, note, created_at) VALUES (?, ?, ?)",
                    [char_id, row.note, row.created_at])

        return f"Successfully migrated <highlight>{num}</highlight> note records."

    @command(command="budabot",
             params=[Const("migrate"), Const("last_seen")],
             access_level="superadmin",
             description="Migrate last_seen data from a Budabot database")
    def migrate_budabot_last_seen_cmd(self, request, _1, _2):
        data = self.db2.query(
            "SELECT o.name AS sender, p.charid AS char_id, logged_off AS last_seen "
            f"FROM org_members_{self.bot_name} o JOIN players p ON (o.name = p.name AND p.dimension = ?) "
            "WHERE p.charid > 0", [self.dimension])

        num = 0
        with self.db.transaction():
            for row in data:
                char_id = self.resolve_to_char_id(row.sender, row.char_id)

                if char_id:
                    num += 1
                    self.db.exec("DELETE FROM last_seen WHERE char_id = ?",
                                 [char_id])
                    self.db.exec(
                        "INSERT INTO last_seen (char_id, dt) VALUES (?, ?)",
                        [char_id, row.last_seen])

        return f"Successfully migrated <highlight>{num}</highlight> last seen records."

    @command(command="budabot",
             params=[Const("migrate"), Const("cloak_status")],
             access_level="superadmin",
             description="Migrate cloak status records from a Budabot database"
             )
    def migrate_budabot_cloak_status_cmd(self, request, _1, _2):
        if not self.org_id:
            return "Could not migrate cloak status record since org id is not set."

        data = self.db2.query(
            "SELECT o.player AS name, p.charid AS char_id, action, time AS created_at "
            f"FROM org_city_{self.bot_name} o JOIN players p ON (o.player = p.name AND p.dimension = ?) "
            "WHERE p.charid > 0", [self.dimension])

        num = 0
        with self.db.transaction():
            self.db.exec("DELETE FROM cloak_status WHERE org_id = ?",
                         [self.org_id])
            for row in data:
                char_id = self.resolve_to_char_id(row.name, row.char_id)

                if char_id:
                    num += 1
                    self.db.exec(
                        "INSERT INTO cloak_status (char_id, action, created_at, org_id) VALUES (?, ?, ?, ?)",
                        [char_id, row.action, row.created_at, self.org_id])

        return f"Successfully migrated <highlight>{num}</highlight> cloak status records."

    @command(command="budabot",
             params=[Const("migrate"), Const("org_activity")],
             access_level="superadmin",
             description="Migrate org activity records from a Budabot database"
             )
    def migrate_budabot_org_activity_cmd(self, request, _1, _2):
        if not self.org_id:
            return "Could not migrate cloak status record since org id is not set."

        request.reply("Processing records. This may take some time...")

        data = self.db2.query(
            "SELECT o.actor AS actor_name, p1.charid AS actor_char_id, o.actee AS actee_name, p2.charid AS actee_char_id, action, time AS created_at "
            "FROM org_history o JOIN players p1 ON (o.actor = p1.name AND p1.dimension = ?) JOIN players p2 ON (o.actee = p2.name AND p2.dimension = ?) "
            "WHERE p1.charid > 0 AND p2.charid > 0",
            [self.dimension, self.dimension])

        num = 0
        with self.db.transaction():
            self.db.exec("DELETE FROM org_activity WHERE org_id = ?",
                         [self.org_id])
            for row in data:
                actor_char_id = self.resolve_to_char_id(
                    row.actor_name, row.actor_char_id)
                actee_char_id = self.resolve_to_char_id(
                    row.actee_name, row.actee_char_id)

                if actor_char_id and actee_char_id:
                    num += 1
                    self.db.exec(
                        "INSERT INTO org_activity (actor_char_id, actee_char_id, action, created_at, org_id) VALUES (?, ?, ?, ?, ?)",
                        [
                            actor_char_id, actee_char_id, row.action,
                            row.created_at, self.org_id
                        ])

        return f"Successfully migrated <highlight>{num}</highlight> org activity records."

    @command(
        command="budabot",
        params=[Const("migrate"), Const("players")],
        access_level="superadmin",
        description="Migrate character info records from a Budabot database")
    def migrate_budabot_player_cmd(self, request, _1, _2):
        data = self.db2.query(
            "SELECT * FROM players WHERE charid > 0 AND dimension = ?",
            [self.dimension])

        request.reply("Processing %s records. This may take some time..." %
                      len(data))

        num = 0
        with self.db.transaction():
            for row in data:
                if row.charid:
                    num += 1
                    self.db.exec("DELETE FROM player WHERE char_id = ?",
                                 [row.charid])
                    self.db.exec(
                        "INSERT INTO player (ai_level, ai_rank, breed, char_id, dimension, faction, first_name, gender, head_id, last_name, "
                        "last_updated, level, name, org_id, org_name, org_rank_id, org_rank_name, profession, profession_title, pvp_rating, pvp_title, source) "
                        "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
                        [
                            row.ai_level, row.ai_rank, row.breed, row.charid,
                            row.dimension, row.faction, row.firstname,
                            row.gender, row.head_id if row.head_id else 0,
                            row.lastname, row.last_update, row.level, row.name,
                            row.guild_id, row.guild, row.guild_rank_id or 0,
                            row.guild_rank, row.profession, row.prof_title,
                            row.pvp_rating if row.pvp_rating else 0,
                            row.pvp_title if row.pvp_title else "", row.source
                        ])

            # maybe this is needed also? self.db.exec("DELETE FROM player WHERE char_id = 4294967295")

        return f"Successfully migrated <highlight>{num}</highlight> character info records."

    def resolve_to_char_id(self, name, char_id):
        if char_id and char_id > 0:
            return char_id

        char_id = self.character_service.resolve_char_to_id(name)
        if char_id:
            return char_id

        char_info = self.pork_service.get_character_info(name)
        if char_info:
            return char_info.char_id

        return None
Exemple #22
0
class MigrateController:
    def __init__(self):
        self.logger = Logger(__name__)

    def inject(self, registry):
        self.db = registry.get_instance("db")
        self.character_service = registry.get_instance("character_service")
        self.alts_service = registry.get_instance("alts_service")
        self.pork_service = registry.get_instance("pork_service")

    def pre_start(self):
        self.bot_name = "bot_name"
        database = DictObject({
            "type": "mysql",
            "name": "database_name",
            "username": "******",
            "password": "******",
            "host": "localhost",
            "port": 3306
        })

        self.db2 = DB()
        self.db2.connect_mysql(database.host, database.port, database.username,
                               database.password, database.name)

    @command(command="bebot",
             params=[Const("migrate"), Const("alts")],
             access_level="superadmin",
             description="Migrate alts from a Bebot database")
    def migrate_alts_cmd(self, request, _1, _2):
        data = self.db2.query(
            "SELECT a.alt, u1.char_id AS alt_char_id, a.main, u2.char_id AS main_char_id "
            "FROM alts a "
            f"LEFT JOIN {self.bot_name}_users u1 ON a.alt = u1.nickname "
            f"LEFT JOIN {self.bot_name}_users u2 ON a.main = u2.nickname "
            "WHERE a.confirmed = 1 "
            "ORDER BY main, alt")
        current_main = None
        current_main_id = None
        count_inactive = 0
        count_success = 0
        count_failure = 0

        request.reply("Processing %s alt records..." % len(data))

        for row in data:
            if row.main != current_main:
                current_main = row.main
                current_main_id = self.resolve_to_char_id(
                    row.main, row.main_char_id)

            if not current_main_id:
                self.logger.warning(
                    f"Could not resolve main char '{current_main}' to char id")
                count_inactive += 1
                continue

            alt_id = self.resolve_to_char_id(row.alt, row.alt_char_id)
            if not alt_id:
                self.logger.warning(
                    f"Could not resolve alt char '{row.alt}' to char id")
                count_inactive += 1
                continue

            msg, result = self.alts_service.add_alt(current_main_id, alt_id)
            if result:
                count_success += 1
            else:
                count_failure += 1

        return f"<highlight>{count_success}</highlight> alts were migrated successfully, " \
               f"<highlight>{count_failure}</highlight> alts failed to be added, " \
               f"and <highlight>{count_inactive}</highlight> chars were inactive and could not be resolved to char ids."

    @command(command="bebot",
             params=[Const("migrate"), Const("logon")],
             access_level="superadmin",
             description="Migrate logon messages from a Bebot database")
    def migrate_logon_cmd(self, request, _1, _2):
        data = self.db2.query(f"SELECT message, id FROM {self.bot_name}_logon")

        request.reply("Processing %s logon records..." % len(data))

        for row in data:
            self.db.exec(
                "INSERT INTO log_messages (char_id, logon) VALUES(?, ?)",
                [row.id, row.message])

        return f"Logon messages migrated successfully."

    @command(command="budabot",
             params=[Const("migrate"), Const("quotes")],
             access_level="superadmin",
             description="Migrate quotes from a Bebot database")
    def migrate_quotes_cmd(self, request, _1, _2):
        data = self.db2.query(
            "SELECT p.charid AS char_id, q.id, q.msg, q.dt FROM quote q LEFT JOIN players p ON q.poster = p.name"
        )
        count_inactive = 0

        request.reply("Processing %s quote records..." % len(data))

        for row in data:
            char_id = self.resolve_to_char_id(row.poster, row.char_id)

            if not char_id:
                char_id = -1
                count_inactive += 1

            self.db.exec(
                "INSERT INTO quote (id, char_id, created_at, content) VALUES (?, ?, ?, ?)",
                [row.id, char_id, row.dt, row.msg])

        return f"Quotes successfully migrated. <highlight>{count_inactive}</highlight> posters were inactive and could not be resolved to char ids."

    def resolve_to_char_id(self, name, char_id):
        if char_id and char_id > 0:
            return char_id

        char_id = self.character_service.resolve_char_to_id(name)
        if char_id:
            return char_id

        char_info = self.pork_service.get_character_info(name)
        if char_info:
            return char_info.char_id

        return None
Exemple #23
0
class Bot:
    def __init__(self):
        self.socket = None
        self.char_id = None
        self.char_name = None
        self.logger = Logger(__name__)

    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(10)
        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: LoginCharacterList = self.read_packet()
        if isinstance(character_list_packet, LoginError):
            self.logger.error("Error logging in: %s" %
                              character_list_packet.message)
            return False
        if character not in character_list_packet.names:
            self.logger.error("Character '%s' does not exist on this account" %
                              character)
            return False
        index = character_list_packet.names.index(character)

        # select character
        self.char_id = character_list_packet.char_ids[index]
        self.char_name = character_list_packet.names[index]
        if character_list_packet.online_statuses[index]:
            sleep_duration = 20
            self.logger.warning(
                "Character '%s' is already logged on, waiting %ds before proceeding"
                % (self.char_name, sleep_duration))
            time.sleep(sleep_duration)
        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 packet
        else:
            self.logger.error("Error logging in: %s" % packet.message)
            return False

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

        read, write, error = select.select([self.socket], [], [],
                                           max_delay_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)

            try:
                return ServerPacket.get_instance(packet_type, data)
            except Exception as e:
                self.logger.error(
                    "Error parsing packet parameters for packet_type '%d' and payload: %s"
                    % (packet_type, data), e)
                return None

    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 BuddyManager:
    BUDDY_LOGON_EVENT = "buddy_logon"
    BUDDY_LOGOFF_EVENT = "buddy_logoff"

    def __init__(self):
        self.buddy_list = {}
        self.buddy_list_size = 1000
        self.logger = Logger("buddy_manager")

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

    def pre_start(self):
        self.bot.add_packet_handler(server_packets.BuddyAdded.id,
                                    self.handle_add)
        self.bot.add_packet_handler(server_packets.BuddyRemoved.id,
                                    self.handle_remove)
        self.bot.add_packet_handler(server_packets.LoginOK.id,
                                    self.handle_login_ok)
        self.event_manager.register_event_type(self.BUDDY_LOGON_EVENT)
        self.event_manager.register_event_type(self.BUDDY_LOGOFF_EVENT)

    def handle_add(self, packet):
        buddy = self.buddy_list.get(packet.char_id, {"types": []})
        buddy["online"] = packet.online
        self.buddy_list[packet.char_id] = buddy
        if packet.online == 1:
            self.event_manager.fire_event(self.BUDDY_LOGON_EVENT, packet)
        else:
            self.event_manager.fire_event(self.BUDDY_LOGOFF_EVENT, packet)

    def handle_remove(self, packet):
        if packet.char_id in self.buddy_list:
            if len(self.buddy_list[packet.char_id]["types"]) > 0:
                self.logger.warning(
                    "Removing buddy %d that still has types %s" %
                    (packet.char_id, self.buddy_list[packet.char_id]["types"]))
            del self.buddy_list[packet.char_id]

    def handle_login_ok(self, packet):
        self.buddy_list_size += 1000

    def add_buddy(self, char_id, _type):
        if char_id and char_id != self.bot.char_id:
            if char_id not in self.buddy_list:
                self.bot.send_packet(client_packets.BuddyAdd(char_id, "\1"))
                self.buddy_list[char_id] = {"online": None, "types": [_type]}
            elif _type not in self.buddy_list[char_id]["types"]:
                self.buddy_list[char_id]["types"].append(_type)

            return True
        else:
            return False

    def remove_buddy(self, char_id, _type, force_remove=False):
        if char_id:
            if char_id not in self.buddy_list:
                return False
            else:
                if _type in self.buddy_list[char_id]["types"]:
                    self.buddy_list[char_id]["types"].remove(_type)
                if len(self.buddy_list[char_id]["types"]) == 0 or force_remove:
                    self.bot.send_packet(client_packets.BuddyRemove(char_id))
                return True
        else:
            return False

    def get_buddy(self, char):
        char_id = self.character_manager.resolve_char_to_id(char)
        return self.buddy_list.get(char_id, None)

    def is_online(self, char):
        char_id = self.character_manager.resolve_char_to_id(char)
        buddy = self.get_buddy(char_id)
        if buddy is None:
            return None
        else:
            return buddy.get("online", None)

    def get_all_buddies(self):
        return dict(self.buddy_list)
Exemple #25
0
class RaidInstanceController:
    UNASSIGNED_RAID_INSTANCE_ID = 0

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

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

    def start(self):
        self.db.exec(
            "CREATE TABLE IF NOT EXISTS raid_instance (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, conn_id VARCHAR(50) NOT NULL)"
        )

        self.db.exec("DROP TABLE IF EXISTS raid_instance_char")
        self.db.exec(
            "CREATE TABLE raid_instance_char (raid_instance_id INT NOT NULL, char_id INT PRIMARY KEY, is_leader TINYINT NOT NULL)"
        )

        self.setting_service.register(
            self.module_name, "raid_instance_relay_symbol", "@",
            TextSettingType(["!", "#", "*", "@", "$", "+", "-"]),
            "Symbol for external relay")

    @command(command="raidinstance",
             params=[],
             access_level="guest",
             description="Show the current raid instances")
    def raid_instance_cmd(self, request):
        blob = self.text.make_tellcmd("Refresh", "raidinstance")
        blob += "     "
        blob += self.text.make_tellcmd("Apply", "raidinstance apply")
        blob += "\n\n"
        num_assigned = 0
        num_unassigned = 0

        raid_instances = self.get_raid_instances()

        data = self.get_raid_instance_chars()
        current_raid_instance_id = None
        for row in data:
            if row.raid_instance_id != current_raid_instance_id:
                conn = self.bot.conns.get(row.conn_id)
                bot_name = "(" + conn.char_name + ")" if conn else ""
                blob += f"\n<header2>{row.raid_instance_name} {bot_name}</header2>\n"
                current_raid_instance_id = row.raid_instance_id

            if not row.char_id:
                continue

            if row.raid_instance_id == self.UNASSIGNED_RAID_INSTANCE_ID:
                num_unassigned += 1
            else:
                num_assigned += 1
            blob += self.compact_char_display(row)
            add_leader_link = (
                row.raid_instance_id != self.UNASSIGNED_RAID_INSTANCE_ID
                and not row.is_leader)
            blob += " " + self.get_assignment_links(raid_instances, row.name,
                                                    add_leader_link)
            blob += "\n"
            blob += "\n"

        blob += "\n" + self.text.make_tellcmd("Clear All",
                                              "raidinstance clear")
        symbol = self.setting_service.get(
            "raid_instance_relay_symbol").get_value()
        blob += f"\n\nTip: You can use <highlight>{symbol}</highlight> at the start of your message to send it to all bot channels."
        blob += "\n\nInspired by the <highlight>RIS</highlight> module written for Bebot by <highlight>Bitnykk</highlight>"

        return ChatBlob(
            "Raid Instance (%d, %d)" % (num_assigned, num_unassigned), blob)

    @command(command="raidinstance",
             params=[Const("assign"),
                     Any("raid_instance"),
                     Character("char")],
             access_level="guest",
             description="Add a character to a raid instance",
             sub_command="leader")
    def raid_instance_assign_cmd(self, request, _, raid_instance_name, char):
        if not char.char_id:
            return StandardMessage.char_not_found(char.name)

        raid_instance = self.get_raid_instance(raid_instance_name)
        if not raid_instance:
            return f"Raid instance <highlight>{raid_instance_name}</highlight> does not exist."

        self.refresh_raid_instance_chars()

        row = self.db.query_single(
            "SELECT raid_instance_id FROM raid_instance_char WHERE char_id = ?",
            [char.char_id])
        if row:
            if raid_instance.id == row.raid_instance_id:
                return f"Character <highlight>{char.name}</highlight> is already assigned to raid instance <highlight>{raid_instance.name}</highlight>."
            else:
                self.update_char_raid_instance(char.char_id, raid_instance.id)
                return f"Character <highlight>{char.name}</highlight> has been assigned to raid instance <highlight>{raid_instance.name}</highlight>."
        else:
            return f"Character <highlight>{char.name}</highlight> is not in the private channel."

    @command(command="raidinstance",
             params=[Const("clear")],
             access_level="guest",
             description="Remove all characters from all raid instances",
             sub_command="leader")
    def raid_instance_clear_cmd(self, request, _):
        self.db.exec("DELETE FROM raid_instance_char")

        return f"All characters have been removed from raid instances."

    @command(command="raidinstance",
             params=[Const("unassign"), Character("char")],
             access_level="guest",
             description="Remove a character from all raid instances",
             sub_command="leader")
    def raid_instance_unassign_cmd(self, request, _, char):
        if not char.char_id:
            return StandardMessage.char_not_found(char.name)

        self.refresh_raid_instance_chars()

        row = self.db.query_single(
            "SELECT r2.name FROM raid_instance_char r1 JOIN raid_instance r2 ON r1.raid_instance_id = r2.id WHERE r1.char_id = ?",
            [char.char_id])
        if row:
            self.update_char_raid_instance(char.char_id,
                                           self.UNASSIGNED_RAID_INSTANCE_ID)
            return f"Character <highlight>{char.name}</highlight> has been removed from raid instance <highlight>{row.name}</highlight>."
        else:
            return f"Character <highlight>{char.name}</highlight> is not assigned to any raid instances."

    @command(command="raidinstance",
             params=[Const("leader"), Character("char")],
             access_level="guest",
             description="Set the leader for a raid instance",
             sub_command="leader")
    def raid_instance_leader_cmd(self, request, _, char):
        if not char.char_id:
            return StandardMessage.char_not_found(char.name)

        raid_instance = self.get_raid_instance_by_char(char.char_id)
        if not raid_instance:
            return f"Character <highlight>{char.name}</highlight> does not belong to a raid instance."

        self.set_leader(char.char_id, raid_instance.id)

        return f"Character <highlight>{char.name}</highlight> has been set as the leader for raid instance <highlight>{raid_instance.name}</highlight>."

    @command(command="raidinstance",
             params=[Const("apply")],
             access_level="guest",
             description="Apply the current raid instance configuration",
             sub_command="leader")
    def raid_instance_apply_cmd(self, request, _):
        for raid_instance in self.get_raid_instances():
            conn = self.bot.conns.get(raid_instance.conn_id)
            if not conn:
                self.logger.warning(
                    f"Could not find conn with id '{raid_instance.conn_id}'")
                continue

            private_channel = set()
            private_channel.update(conn.private_channel.keys())

            data = self.db.query(
                "SELECT char_id FROM raid_instance_char WHERE raid_instance_id = ?",
                [raid_instance.id])
            for row in data:
                if row.char_id not in private_channel:
                    self.private_channel_service.invite(row.char_id, conn)

                private_channel.discard(row.char_id)

            for char_id in private_channel:
                self.private_channel_service.kick(char_id, conn)

        return "Raid instance configuration has been applied."

    @command(
        command="raidinstance",
        params=[Const("create"),
                Any("raid_instance_name"),
                Any("conn_id")],
        access_level="admin",
        description="Create or update a raid instance",
        sub_command="manage")
    def raid_instance_create_cmd(self, request, _, raid_instance_name,
                                 conn_id):
        conn = self.get_conn_by_id(conn_id)
        if not conn:
            return f"Could not find bot connection with ID <highlight>{conn_id}</highlight>."

        conn_display = f"{conn.char_name} ({conn.id})"

        if not conn.is_main:
            return f"Bot connection <highlight>{conn_display}</highlight> cannot be assigned to a raid instance because it is not a main bot."

        raid_instance = self.get_raid_instance(raid_instance_name)
        if raid_instance:
            if raid_instance.name == raid_instance_name and raid_instance.conn_id == conn.id:
                return f"Raid instance <highlight>{raid_instance_name}</highlight> already exists."
            else:
                self.db.exec(
                    "UPDATE raid_instance SET name = ?, conn_id = ? WHERE id = ?",
                    [raid_instance_name, conn.id, raid_instance.id])
                return f"Raid instance <highlight>{raid_instance_name}</highlight> has been updated."
        else:
            self.db.exec(
                "INSERT INTO raid_instance (name, conn_id) VALUES (?, ?)",
                [raid_instance_name, conn.id])
            return f"Raid instance <highlight>{raid_instance_name}</highlight> has been created and assigned to bot connection <highlight>{conn_display}</highlight>."

    @command(command="raidinstance",
             params=[Const("delete"),
                     Any("raid_instance_name")],
             access_level="admin",
             description="Remove a raid instance",
             sub_command="manage")
    def raid_instance_delete_cmd(self, request, _, raid_instance_name):
        raid_instance = self.get_raid_instance(raid_instance_name)
        if not raid_instance:
            return f"Raid instance <highlight>{raid_instance_name}</highlight> does not exist."

        self.db.exec(
            "DELETE FROM raid_instance_char WHERE raid_instance_id = ?",
            [raid_instance.id])
        self.db.exec("DELETE FROM raid_instance WHERE id = ?",
                     [raid_instance.id])

        return f"Raid instance <highlight>{raid_instance_name}</highlight> has been deleted."

    def get_raid_instance_chars(self):
        self.refresh_raid_instance_chars()

        data = self.db.query(
            "SELECT * FROM ("
            "SELECT p.*, COALESCE(p.name, r1.char_id) AS name, r2.id AS raid_instance_id, "
            "r1.is_leader, r2.name AS raid_instance_name, r2.conn_id "
            "FROM raid_instance r2 "
            "LEFT JOIN raid_instance_char r1 ON r1.raid_instance_id = r2.id "
            "LEFT JOIN player p ON r1.char_id = p.char_id "
            "UNION "
            "SELECT p.*, COALESCE(p.name, r3.char_id) AS name, r3.raid_instance_id, "
            "r3.is_leader, 'Unassigned' AS raid_instance_name, '' AS conn_id "
            "FROM raid_instance_char r3 "
            "LEFT JOIN player p ON r3.char_id = p.char_id "
            "WHERE r3.raid_instance_id = ?) "
            "ORDER BY raid_instance_id != ? DESC, raid_instance_name, is_leader, profession",
            [
                self.UNASSIGNED_RAID_INSTANCE_ID,
                self.UNASSIGNED_RAID_INSTANCE_ID
            ])

        return data

    def refresh_raid_instance_chars(self):
        current_raid_instances = set(
            map(lambda x: x.char_id,
                self.db.query("SELECT char_id FROM raid_instance_char")))

        current_private_channel = set()
        for _id, conn in self.bot.get_conns(lambda x: x.is_main):
            current_private_channel.update(conn.private_channel.keys())

        for char_id in current_private_channel.difference(
                current_raid_instances):
            self.db.exec(
                "INSERT INTO raid_instance_char (char_id, raid_instance_id, is_leader) VALUES (?, ?, 0)",
                [char_id, self.UNASSIGNED_RAID_INSTANCE_ID])

        for char_id in current_raid_instances.difference(
                current_private_channel):
            self.db.exec(
                "DELETE FROM raid_instance_char WHERE char_id = ? AND raid_instance_id = ?",
                [char_id, self.UNASSIGNED_RAID_INSTANCE_ID])

    def update_char_raid_instance(self, char_id, raid_instance_id):
        return self.db.exec(
            "UPDATE raid_instance_char SET raid_instance_id = ?, is_leader = 0 WHERE char_id = ?",
            [raid_instance_id, char_id])

    def set_leader(self, char_id, raid_instance_id):
        self.db.exec(
            "UPDATE raid_instance_char SET is_leader = 0 WHERE raid_instance_id = ?",
            [raid_instance_id])
        self.db.exec(
            "UPDATE raid_instance_char SET is_leader = 1 WHERE raid_instance_id = ? AND char_id = ?",
            [raid_instance_id, char_id])

    def compact_char_display(self, char_info):
        if char_info.level:
            msg = "<highlight>%s</highlight> (%d/<green>%d</green>) %s" % (
                char_info.name, char_info.level, char_info.ai_level,
                char_info.profession)
        elif char_info.name:
            msg = "<highlight>%s</highlight>" % char_info.name
        else:
            msg = "<highlight>Unknown(%d)</highlight>" % char_info.char_id

        if char_info.is_leader:
            msg += " [Leader]"

        return msg

    def get_assignment_links(self, raid_instances, char_name, add_leader_link):
        def map_row(x):
            if x.id == self.UNASSIGNED_RAID_INSTANCE_ID:
                return self.text.make_tellcmd(
                    x.name, f"raidinstance unassign {char_name}")
            else:
                return self.text.make_tellcmd(
                    x.name, f"raidinstance assign {x.name} {char_name}")

        links = list(map(map_row, raid_instances))
        if add_leader_link:
            links.insert(
                0,
                self.text.make_tellcmd("MakeLeader",
                                       f"raidinstance leader {char_name}"))
        return " ".join(links)

    def get_raid_instances(self):
        data = self.db.query(
            "SELECT id, name, conn_id FROM raid_instance ORDER BY name")
        data.append(
            DictObject({
                "id": self.UNASSIGNED_RAID_INSTANCE_ID,
                "name": "Unassigned",
                "conn_id": None
            }))
        return data

    def get_raid_instance(self, raid_instance_name):
        return self.db.query_single(
            "SELECT id, name, conn_id FROM raid_instance WHERE name LIKE ?",
            [raid_instance_name])

    def get_raid_instance_by_char(self, char_id):
        return self.db.query_single(
            "SELECT id, name, conn_id "
            "FROM raid_instance r1 JOIN raid_instance_char r2 ON r1.id = r2.raid_instance_id "
            "WHERE r2.char_id = ?", [char_id])

    def get_conn_by_id(self, conn_id):
        conn = self.bot.conns.get(conn_id)
        if conn:
            return conn

        conns = self.bot.get_conns(
            lambda x: x.char_name.lower() == conn_id.lower())
        if conns:
            return conns[0][1]

        return None
Exemple #26
0
class SettingService:
    def __init__(self):
        self.logger = Logger(__name__)
        self.settings = {}
        self.db_cache = {}
        self.change_listeners = {}

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

    def start(self):
        # process decorators
        for _, inst in Registry.get_all_instances().items():
            for name, method in get_attrs(inst).items():
                if hasattr(method, "setting"):
                    setting_name, value, description, extended_description, obj = getattr(
                        method, "setting")
                    self.register(inst.module_name, setting_name, value, obj,
                                  description, extended_description)

    def register(self,
                 module,
                 name,
                 value,
                 setting: SettingType,
                 description,
                 extended_description=None):
        """Call during start"""
        name = name.lower()
        module = module.lower()
        setting.set_name(name)
        setting.set_description(description)
        setting.set_extended_description(extended_description)

        if not description:
            self.logger.warning("No description specified for setting '%s'" %
                                name)

        if " " in name:
            raise Exception(
                "One or more spaces found in setting name '%s' for module '%s'"
                % (name, module))

        row = self.db.query_single(
            "SELECT name, value, description FROM setting WHERE name = ?",
            [name])

        if row is None:
            self.logger.debug("Adding setting '%s'" % name)

            self.db.exec(
                "INSERT INTO setting (name, value, description, module, verified) VALUES (?, ?, ?, ?, ?)",
                [name, "", description, module, 1])

            # verify default value is a valid value, and is formatted appropriately
            setting.set_value(value)
        else:
            self.logger.debug("Updating setting '%s'" % name)
            self.db.exec(
                "UPDATE setting SET description = ?, verified = ?, module = ? WHERE name = ?",
                [description, 1, module, name])

        self.settings[name] = setting

    def register_change_listener(self, setting_name, handler):
        """
        Call during start

        Args:
            setting_name: str
            handler: (name: string, old_value, new_value) -> void
        """

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

        if setting_name in self.settings:
            if setting_name not in self.change_listeners:
                self.change_listeners[setting_name] = []
            self.change_listeners[setting_name].append(handler)
        else:
            raise Exception(
                "Could not register change_listener for setting '%s' since it does not exist"
                % setting_name)

    def get_value(self, name):
        # check cache first
        result = self.db_cache.get(name, None)
        if result:
            return result.value
        else:
            row = self.db.query_single(
                "SELECT value FROM setting WHERE name = ?", [name])

            # store result in cache
            self.db_cache[name] = row

            return row.value if row else None

    def set_value(self, name, value):
        old_value = self.get_value(name)

        # clear cache
        self.db_cache[name] = None

        self.db.exec("UPDATE setting SET value = ? WHERE name = ?",
                     [value, name])

        if name in self.change_listeners:
            for change_listener in self.change_listeners[name]:
                change_listener(name, old_value, value)

    def get(self, name):
        name = name.lower()
        setting = self.settings.get(name, None)
        if setting:
            return setting
        else:
            return None
Exemple #27
0
class BuddyService:
    BUDDY_LOGON_EVENT = "buddy_logon"
    BUDDY_LOGOFF_EVENT = "buddy_logoff"

    def __init__(self):
        self.buddy_list = {}
        self.buddy_list_size = 0
        self.logger = Logger(__name__)

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

    def pre_start(self):
        self.bot.register_packet_handler(server_packets.BuddyAdded.id,
                                         self.handle_add)
        self.bot.register_packet_handler(server_packets.BuddyRemoved.id,
                                         self.handle_remove)
        self.bot.register_packet_handler(server_packets.LoginOK.id,
                                         self.handle_login_ok)
        self.event_service.register_event_type(self.BUDDY_LOGON_EVENT)
        self.event_service.register_event_type(self.BUDDY_LOGOFF_EVENT)

    def handle_add(self, conn: Conn, packet):
        buddy = self.buddy_list[conn.id].get(packet.char_id, {
            "types": [],
            "conn_id": conn.id
        })
        buddy["online"] = packet.online
        self.buddy_list[conn.id][packet.char_id] = buddy

        # verify that buddy does not exist on any other conn
        for conn_id, conn_buddy_list in self.buddy_list.items():
            if conn.id != conn_id:
                buddy = conn_buddy_list.get(packet.char_id, None)
                if buddy:
                    if buddy["online"] is None:
                        # remove from other conn list
                        del conn_buddy_list[packet.char_id]
                    else:
                        # remove from this conn
                        self.logger.warning(
                            "Removing char '%s' from conn '%s' since it already exists on another conn"
                            % (packet.char_id, conn.id))
                        conn.send_packet(
                            client_packets.BuddyRemove(packet.char_id))

        if packet.online == 1:
            self.event_service.fire_event(self.BUDDY_LOGON_EVENT, packet)
        else:
            self.event_service.fire_event(self.BUDDY_LOGOFF_EVENT, packet)

    def handle_remove(self, conn: Conn, packet):
        conn_buddy_list = self.buddy_list[conn.id]
        if packet.char_id in conn_buddy_list:
            if len(conn_buddy_list[packet.char_id]["types"]) > 0:
                self.logger.warning(
                    "Removing buddy %d that still has types %s" %
                    (packet.char_id, conn_buddy_list[packet.char_id]["types"]))

            del conn_buddy_list[packet.char_id]

    def handle_login_ok(self, conn: Conn, packet):
        self.buddy_list_size += 1000
        self.buddy_list[conn.id] = {}
        self.buddy_list[conn.id][conn.char_id] = {
            "online": True,
            "types": [],
            "conn_id": conn.id
        }

    def add_buddy(self, char_id, _type):
        if not char_id:
            return False

        # check if we are trying to add a conn as a buddy
        if self.is_conn_char_id(char_id):
            return False

        buddy = self.get_buddy(char_id)
        if buddy:
            buddy["types"].append(_type)
        else:
            conn = self.get_conn_for_new_buddy()
            if not conn:
                self.logger.warning(
                    f"Could not add buddy '{char_id}' with type '{_type}' since buddy list is full"
                )
            else:
                if conn.char_id != char_id:
                    conn.send_packet(client_packets.BuddyAdd(char_id, "\1"))
                    self.buddy_list[conn.id][char_id] = {
                        "online": None,
                        "types": [_type],
                        "conn_id": conn.id
                    }

        return True

    def is_conn_char_id(self, char_id):
        for _id, conn in self.bot.conns.items():
            if conn.char_id == char_id:
                return True

        return False

    def remove_buddy(self, char_id, _type, force_remove=False):
        if char_id:
            buddy = self.get_buddy(char_id)
            if not buddy:
                return False

            if _type in buddy["types"]:
                buddy["types"].remove(_type)

            if len(buddy["types"]) == 0 or force_remove:
                if not self.is_conn_char_id(char_id):
                    conn = self.bot.conns[buddy["conn_id"]]
                    conn.send_packet(client_packets.BuddyRemove(char_id))

            return True
        else:
            return False

    def get_buddy(self, char_id):
        for conn_id, conn_buddy_list in self.buddy_list.items():
            if char_id in conn_buddy_list:
                return conn_buddy_list[char_id]
        return None

    def is_online(self, char_id):
        buddy = self.get_buddy(char_id)
        if buddy is None:
            return None
        else:
            return buddy.get("online", None)

    def get_all_buddies(self):
        result = {}
        for conn_id, conn_buddy_list in self.buddy_list.items():
            for char_id, buddy in conn_buddy_list.items():
                # TODO what if buddies exist on multiple conns?
                result[char_id] = buddy

        return result

    def get_buddy_list_size(self):
        count = 0
        for conn_id, conn_buddy_list in self.buddy_list.items():
            count += len(conn_buddy_list)

        return count

    def get_conn_for_new_buddy(self):
        buddy_list_size = 1001
        _id = None
        for conn_id, conn_buddy_list in self.buddy_list.items():
            if len(conn_buddy_list) < buddy_list_size:
                buddy_list_size = len(conn_buddy_list)
                _id = conn_id

        return self.bot.conns.get(_id, None)
Exemple #28
0
class CommandService:
    PRIVATE_MESSAGE_CHANNEL = "msg"

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

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

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

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

    def start(self):
        access_levels = {}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.pre_processors.append(pre_processor)

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

        Args:
            label: str
            value: str
        """

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

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

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

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

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

            # message = html.unescape(message)

            command_str, command_args = self.get_command_parts(message)

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

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

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

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

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

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

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

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

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

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

        sql += " ORDER BY sub_command, channel"

        return self.db.query(sql, params)

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

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

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

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

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

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

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

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

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

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

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

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

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

        return help_text

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

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

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

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

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

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

    def trim_command_symbol(self, s):
        symbol = self.setting_service.get("symbol").get_value()
        if s.startswith(symbol):
            s = s[len(symbol):]
        return s
Exemple #29
0
class DiscordController:
    MESSAGE_SOURCE = "discord"
    COMMAND_CHANNEL = "discord"

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

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

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

        self.message_hub_service.register_message_source(self.MESSAGE_SOURCE)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            self.event_service.fire_event(dtype, message)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return name

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

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

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

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

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

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

            if not new_value:
                self.disconnect_discord_client()
Exemple #30
0
class CommandService:
    PRIVATE_CHANNEL = "priv"
    ORG_CHANNEL = "org"
    PRIVATE_MESSAGE = "msg"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            message = html.unescape(message)

            command_str, command_args = self.get_command_parts(message)

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

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

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

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

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

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

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

        sql += " ORDER BY sub_command, channel"

        return self.db.query(sql, params)

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

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

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

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

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

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

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

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

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

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

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

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

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

        return help_text

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

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

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

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

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

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

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

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

        symbol = packet.message[:1]
        command_str = packet.message[1:]
        if symbol == self.setting_service.get("symbol").get_value(
        ) and self.public_channel_service.is_org_channel_id(packet.channel_id):
            self.process_command(command_str, self.ORG_CHANNEL, packet.char_id,
                                 lambda msg: self.bot.send_org_message(msg))
Exemple #31
0
class MessageHubService:
    def __init__(self):
        self.logger = Logger(__name__)
        self.hub = {}
        self.sources = []

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

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

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

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

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

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

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

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

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

        self.reload_mapping(destination)

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

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

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

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

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

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

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

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

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

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

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

    def get_formatted_message(self, channel_prefix, sender, message):
        formatted_message = ""
        if channel_prefix:
            formatted_message += f"{channel_prefix} "
        if sender:
            char_name = self.text.make_charlink(sender.name)
            formatted_message += f"{char_name}: "
        # TODO pagination should not happen until destination channel is known
        if isinstance(message, ChatBlob):
            message = self.text.paginate_single(message, self.bot.get_primary_conn())
        formatted_message += message
        return formatted_message