Exemplo n.º 1
0
 def test_named_flag_parameters(self):
     param = NamedFlagParameters(["test1", "test2", "test3"])
     self.assertEqual({'test1': True, 'test2': True, 'test3': True}, self.param_test_helper(param, "--test1 --test2 --test3"))
     self.assertEqual({'test1': True, 'test2': True, 'test3': True}, self.param_test_helper(param, "--test3 --test2 --test1"))
     self.assertEqual({'test1': False, 'test2': True, 'test3': False}, self.param_test_helper(param, "--test2"))
     self.assertEqual({'test1': False, 'test2': True, 'test3': True}, self.param_test_helper(param, "--test2 --test3"))
     self.assertEqual({'test1': True, 'test2': True, 'test3': False}, self.param_test_helper(param, "--test2 --test1"))
     self.assertIsNone(self.param_test_helper(param, ""))
Exemplo n.º 2
0
class TimeController:
    def __init__(self):
        self.time_format = "%Y-%m-%d %H:%M:%S %Z%z"

    @command(command="time",
             params=[NamedFlagParameters(["all_timezones"])],
             access_level="all",
             description="Show the current time")
    def time_cmd(self, request, flag_params):
        dt = datetime.now()
        t = int(time.time())

        if not flag_params.all_timezones:
            return "The current time is <highlight>%s</highlight> [%d]." % (
                dt.astimezone(pytz.utc).strftime("%Y-%m-%d %H:%M:%S %Z"), t)
        else:
            blob = "Unixtime => %d\n\n" % t
            current_region = ""
            for tz in pytz.common_timezones:
                result = tz.split("/", 2)
                if len(result) == 2:
                    region, city = result
                else:
                    region = result[0]
                    city = result[0]

                if current_region != region:
                    blob += "\n<pagebreak><header2>%s</header2>\n" % region
                    current_region = region

                blob += "%s => %s\n" % (city, dt.astimezone(
                    pytz.timezone(tz)).strftime(self.time_format))

            return ChatBlob("Timezones", blob)

    @command(command="time",
             params=[Any("timezone")],
             access_level="all",
             description="Show time for the specified timezone")
    def time_zone_cmd(self, request, timezone_str):
        timezone_str = timezone_str.lower()
        for tz in pytz.common_timezones:
            if tz.lower() == timezone_str:
                return "%s => %s" % (tz, datetime.now(
                    tz=pytz.timezone(tz)).strftime(self.time_format))

        return f"Unknown timezone <highlight>{timezone_str}</highlight>. Use <highlight><symbol>time --all_timezones</highlight> to see a list of timezones."
Exemplo n.º 3
0
class CloakController:
    MESSAGE_SOURCE = "cloak_reminder"
    CLOAK_EVENT = "cloak"

    CLOAK_STATUS_OFF = "off"
    CLOAK_STATUS_ON = "on"
    CLOAK_STATUS_MANUAL = "on*"

    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.db = registry.get_instance("db")
        self.util = registry.get_instance("util")
        self.text = registry.get_instance("text")
        self.character_service = registry.get_instance("character_service")
        self.command_alias_service = registry.get_instance("command_alias_service")
        self.timer_controller = registry.get_instance("timer_controller", is_optional=True)
        self.event_service = registry.get_instance("event_service")
        self.access_service = registry.get_instance("access_service")
        self.message_hub_service = registry.get_instance("message_hub_service")

    def pre_start(self):
        self.bot.register_packet_handler(PublicChannelMessage.id, self.handle_public_message)
        self.event_service.register_event_type(self.CLOAK_EVENT)
        self.message_hub_service.register_message_source(self.MESSAGE_SOURCE)

    def start(self):
        self.db.exec("CREATE TABLE IF NOT EXISTS cloak_status (char_id INT NOT NULL, action VARCHAR(10) NOT NULL, created_at INT NOT NULL, org_id INT NOT NULL)")
        self.command_alias_service.add_alias("city", "cloak")

    @command(command="cloak", params=[Const("history"), Int("org_id")], access_level="org_member",
             description="Shows the cloak history")
    def cloak_history_command(self, request, _, org_id):
        data = self.db.query("SELECT c.*, p.name FROM cloak_status c LEFT JOIN player p ON c.char_id = p.char_id "
                             "WHERE c.org_id = ? ORDER BY created_at DESC LIMIT 20", [org_id])

        blob = ""
        for row in data:
            action = self.get_cloak_status_display(row.action)
            blob += "%s turned the device %s at %s.\n" % (row.name, action, self.util.format_datetime(row.created_at))

        conn = self.bot.get_conn_by_org_id(org_id)
        org_name = conn.get_org_name()
        return ChatBlob(f"Cloak History for {org_name}", blob)

    @command(command="cloak", params=[NamedFlagParameters(["all"])], access_level="org_member",
             description="Shows the cloak status")
    def cloak_command(self, request, flag_params):
        t = int(time.time())

        if flag_params.all:
            blob = ""
            for _id, conn in self.bot.get_conns(lambda x: x.is_main and x.org_id):
                row = self.db.query_single("SELECT c.char_id, c.action, c.created_at, p.name FROM cloak_status c LEFT JOIN player p ON c.char_id = p.char_id "
                                           "WHERE c.org_id = ? ORDER BY created_at DESC LIMIT 1", [conn.org_id])

                org_name = conn.get_org_name()
                if row:
                    action = self.get_cloak_status_display(row.action)
                    time_str = self.util.time_to_readable(t - row.created_at)
                    history_link = self.text.make_tellcmd("History", f"cloak history {conn.org_id}")
                    blob += f"{org_name} - {action} [{time_str} ago] {history_link}\n"
                else:
                    blob += f"{org_name} - Unknown\n"

            title = "Cloak Status"

            return ChatBlob(title, blob)
        else:
            conn = request.conn
            if not conn.org_id:
                return "This bot is not a member of an org."

            row = self.db.query_single("SELECT c.char_id, c.action, c.created_at, p.name FROM cloak_status c LEFT JOIN player p ON c.char_id = p.char_id "
                                       "WHERE c.org_id = ? ORDER BY created_at DESC LIMIT 1", [conn.org_id])

            org_name = conn.get_org_name()
            if row:
                action = self.get_cloak_status_display(row.action)
                time_str = self.util.time_to_readable(t - row.created_at)
                return f"{org_name} - {action} [{time_str} ago]"
            else:
                return f"{org_name} - Unknown cloak status"

    @command(command="cloak", params=[Options(["raise", "on"]), Int("org_id", is_optional=True)], access_level="org_member",
             description="Manually raises the cloak status on the bot")
    def cloak_raise_command(self, request, _, org_id):
        org_id = org_id or request.conn.org_id

        if not org_id:
            return "This bot is not a member of an org."

        row = self.db.query_single("SELECT action FROM cloak_status WHERE org_id = ?", [org_id])
        if row and (row.action == self.CLOAK_STATUS_ON or row.action == self.CLOAK_STATUS_MANUAL):
            return "The cloaking device is already <green>enabled</green>."

        self.db.exec("INSERT INTO cloak_status (char_id, action, created_at, org_id) VALUES (?, ?, ?, ?)",
                     [request.sender.char_id, self.CLOAK_STATUS_MANUAL, int(time.time()), org_id])
        return "The cloaking device has been set to enabled in the bot (you must still enable the cloak if it is disabled)."

    @event(event_type=CLOAK_EVENT, description="Record when the city cloak is turned off and on", is_system=True)
    def city_cloak_event(self, event_type, event_data):
        self.db.exec("INSERT INTO cloak_status (char_id, action, created_at, org_id) VALUES (?, ?, ?, ?)",
                     [event_data.char_id, event_data.action, int(time.time()), event_data.conn.org_id])

    @timerevent(budatime="15m", description="Reminds the players when cloak can be raised")
    def cloak_reminder_event(self, event_type, event_data):
        messages = []
        for _id, conn in self.bot.get_conns(lambda x: x.is_main and x.org_id):
            row = self.db.query_single("SELECT c.*, p.name FROM cloak_status c LEFT JOIN player p ON c.char_id = p.char_id "
                                       "WHERE c.org_id = ? ORDER BY created_at DESC LIMIT 1", [conn.org_id])
            if row:
                one_hour = 3600
                t = int(time.time())
                time_until_change = row.created_at + one_hour - t
                if row.action == self.CLOAK_STATUS_OFF and 0 >= time_until_change > (one_hour * 6 * -1):
                    time_str = self.util.time_to_readable(t - row.created_at)
                    org_name = conn.get_org_name()
                    messages.append(f"The cloaking device for org <highlight>{org_name}</highlight> is <orange>disabled</orange> but can be enabled. "
                                    f"<highlight>{row.name}</highlight> disabled it {time_str} ago.")

        if messages:
            self.message_hub_service.send_message(self.MESSAGE_SOURCE, None, None, "\n".join(messages))

    @event(event_type=CLOAK_EVENT, description="Set a timer for when cloak can be raised and lowered")
    def city_cloak_timer_event(self, event_type, event_data):
        if event_data.action == self.CLOAK_STATUS_OFF:
            timer_name = f"Raise City Cloak ({event_data.conn.get_org_name()})"
        elif event_data.action == self.CLOAK_STATUS_ON:
            timer_name = f"Lower City Cloak ({event_data.conn.get_org_name()})"
        else:
            raise Exception(f"Unknown cloak action '{event_data.action}'")

        timer_name = self.timer_controller.get_timer_name(timer_name)

        self.timer_controller.add_timer(timer_name, event_data.char_id, PublicChannelService.ORG_CHANNEL_COMMAND, int(time.time()), 3600)

    def handle_public_message(self, conn: Conn, packet: PublicChannelMessage):
        if not conn.is_main:
            return

        ext_msg = packet.extended_message
        if ext_msg and ext_msg.category_id == 1001 and ext_msg.instance_id == 1:
            char_name = ext_msg.params[0]
            char_id = self.character_service.resolve_char_to_id(char_name)
            action = ext_msg.params[1]
            self.event_service.fire_event(self.CLOAK_EVENT, DictObject({"char_id": char_id, "name": char_name, "action": action, "conn": conn}))

    def get_cloak_status_display(self, status):
        if status == self.CLOAK_STATUS_ON:
            return "<green>on</green>"
        elif status == self.CLOAK_STATUS_MANUAL:
            return "<green>on*</green>"
        elif status == self.CLOAK_STATUS_OFF:
            return "<orange>off</orange>"
        else:
            return "<highlight>Unknown</highlight>"
Exemplo n.º 4
0
class LastSeenController:
    def __init__(self):
        self.logger = Logger(__name__)

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

    def start(self):
        self.db.exec("CREATE TABLE IF NOT EXISTS last_seen (char_id INT NOT NULL PRIMARY KEY, "
                     "dt INT NOT NULL DEFAULT 0)")

    @command(command="lastseen", params=[Character("character"), NamedFlagParameters(["show_all"])], access_level="org_member",
             description="Show the last time an org member was online (on any alt)")
    def lastseen_cmd(self, request, char, flag_params):
        sql = "SELECT p.*, a.group_id, a.status, l.dt FROM player p " \
              "LEFT JOIN alts a ON p.char_id = a.char_id " \
              "LEFT JOIN last_seen l ON p.char_id = l.char_id " \
              "WHERE p.char_id = ? OR a.group_id = (SELECT group_id FROM alts WHERE char_id = ?) " \
              "ORDER BY l.dt DESC, p.name ASC"

        data = self.db.query(sql, [char.char_id, char.char_id])
        blob = ""
        if len(data) == 0:
            return f"No lastseen information for <highlight>{char.name}</highlight> has been recorded."
        else:
            if flag_params.show_all:
                for row in data:
                    blob += f"<highlight>{row.name}</highlight>"
                    if row.dt:
                        blob += " last seen at " + self.util.format_datetime(row.dt)
                    else:
                        blob += " unknown"
                    blob += "\n\n"

                return ChatBlob("Last Seen Info for %s (%d)" % (char.name, len(data)), blob)
            else:
                online_alts = list(filter(lambda x: self.buddy_service.is_online(x.char_id), data))
                if online_alts:
                    online_alts_str = ", ".join(map(lambda x: f"<highlight>{x.name}</highlight>", online_alts))

                    return f"<highlight>{char.name}</highlight> is <green>online</green> with: {online_alts_str}."
                else:
                    alt_name = data[0].name
                    if data[0].dt:
                        last_seen = self.util.format_datetime(data[0].dt)
                        return f"<highlight>{char.name}</highlight> was last seen online with <highlight>{alt_name}</highlight> at <highlight>{last_seen}</highlight>."
                    else:
                        return f"No lastseen information for <highlight>{char.name}</highlight> has been recorded."

    @event(event_type=BuddyService.BUDDY_LOGON_EVENT, description="Record last seen info")
    def handle_buddy_logon_event(self, event_type, event_data):
        self.update_last_seen(event_data.char_id)

    def update_last_seen(self, char_id):
        t = int(time.time())
        if self.db.exec("UPDATE last_seen SET dt = ? WHERE char_id = ?", [t, char_id]) == 0:
            self.db.exec("INSERT IGNORE INTO last_seen (char_id, dt) VALUES (?, ?)", [char_id, t])

    def get_last_seen(self, char_id):
        return self.db.query_single("SELECT dt FROM last_seen WHERE char_id = ?", [char_id])
Exemplo n.º 5
0
class OnlineController:
    ORG_CHANNEL = "Org"
    PRIVATE_CHANNEL = "Private"

    def __init__(self):
        self.afk_regex = re.compile("^(afk|brb) ?(.*)$", re.IGNORECASE)
        self.channels = [(self.ORG_CHANNEL, 0), (self.PRIVATE_CHANNEL, 1)]

    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.db: DB = registry.get_instance("db")
        self.text = registry.get_instance("text")
        self.util = registry.get_instance("util")
        self.pork_service = registry.get_instance("pork_service")
        self.character_service = registry.get_instance("character_service")
        self.command_alias_service = registry.get_instance(
            "command_alias_service")
        self.alts_service = registry.get_instance("alts_service")
        self.alts_controller = registry.get_instance("alts_controller")

    def start(self):
        self.db.exec("DROP TABLE IF EXISTS online")
        self.db.exec(
            "CREATE TABLE online (char_id INT NOT NULL, afk_dt INT NOT NULL, afk_reason VARCHAR(255) DEFAULT '', channel CHAR(50) NOT NULL, dt INT NOT NULL, UNIQUE(char_id, channel))"
        )

        self.command_alias_service.add_alias("o", "online")

    @command(command="online",
             params=[],
             access_level="member",
             description="Show the list of online characters")
    def online_cmd(self, request):
        return self.get_online_output()

    @command(
        command="online",
        params=[
            Any("profession"),
            NamedFlagParameters(["include_offline_alts"])
        ],
        access_level="member",
        description=
        "Show the list of online characters with alts of a certain profession")
    def online_profession_cmd(self, request, prof, flag_params):
        profession = self.util.get_profession(prof)
        if not profession:
            return "Error! Unknown profession <highlight>%s</highlight>." % prof

        num_org_bots = len(
            self.bot.get_conns(lambda x: x.is_main and x.org_id))

        blob = ""
        blob += self.text.make_tellcmd(
            "Include offline alts",
            f"online {profession} --include_offline_alts") + "\n\n"
        count = 0
        for channel, _ in self.channels:
            online_list = self.get_online_alts(
                channel, profession, flag_params.include_offline_alts)
            if len(online_list) > 0:
                blob += "<header2>%s Channel</header2>\n" % channel

                current_main = ""
                for row in online_list:
                    if current_main != row.main:
                        count += 1
                        blob += "\n%s\n" % row.main
                        current_main = row.main

                    org_info = ""
                    if channel == self.PRIVATE_CHANNEL or num_org_bots > 1:
                        if row.org_name:
                            org_info = ", %s (%s)" % (row.org_name,
                                                      row.org_rank_name)

                    blob += "  <highlight>%s</highlight> (%d/<green>%d</green>) %s%s" % (
                        row.name, row.level or 0, row.ai_level
                        or 0, row.profession, org_info)
                    if row.online:
                        blob += " [<green>Online</green>]"
                    blob += "\n"
                blob += "\n\n"

        return ChatBlob("Online %s (%d)" % (profession, count), blob)

    @command(
        command="count",
        params=[],
        access_level="member",
        description=
        "Show counts of players by title level, profession, and organization")
    def count_cmd(self, request):
        data = self.db.query(
            "SELECT p.*, o.channel, COALESCE(p.name, o.char_id) AS name FROM online o LEFT JOIN player p ON o.char_id = p.char_id ORDER BY channel ASC"
        )

        title_levels = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0}
        profs = {
            "Adventurer": 0,
            "Agent": 0,
            "Bureaucrat": 0,
            "Doctor": 0,
            "Enforcer": 0,
            "Engineer": 0,
            "Fixer": 0,
            "Keeper": 0,
            "Martial Artist": 0,
            "Meta-Physicist": 0,
            "Nano-Technician": 0,
            "Soldier": 0,
            "Trader": 0,
            "Shade": 0,
            "Unknown": 0
        }
        orgs = {}

        # populate counts
        for row in data:
            prof_name = row.profession if row.profession else "Unknown"
            profs[prof_name] += 1

            title_levels[self.util.get_title_level(row.level)] += 1

            org_name = row.org_name if row.org_name else "&lt;None&gt;"
            org_count = orgs.get(org_name, 0)
            orgs[org_name] = org_count + 1

        blob = ""
        blob += "<header>Title Levels</header>\n"
        for title_level, count in title_levels.items():
            if count > 0:
                blob += "%d: %d\n" % (title_level, count)

        blob += "\n\n<header>Professions</header>\n"
        for prof, count in profs.items():
            if count > 0:
                blob += "%s: %d\n" % (prof, count)

        blob += "\n\n<header>Organizations</header>\n"
        for org, count in orgs.items():
            if count > 0:
                blob += "%s: %d\n" % (org, count)

        return ChatBlob("Count (%d)" % len(data), blob)

    def get_online_characters(self, channel):
        sql = "SELECT " \
                "p1.*, " \
                "o.afk_dt, " \
                "o.afk_reason, " \
                "COALESCE(p2.name, p1.name, o.char_id) AS main, " \
                "COALESCE(p1.name, o.char_id) AS name " \
              "FROM online o " \
              "LEFT JOIN alts a1 ON o.char_id = a1.char_id " \
              "LEFT JOIN player p1 ON o.char_id = p1.char_id " \
              "LEFT JOIN alts a2 ON a1.group_id = a2.group_id AND a2.status = ? " \
              "LEFT JOIN player p2 ON a2.char_id = p2.char_id " \
              "WHERE channel = ? " \
              "ORDER BY COALESCE(p2.name, p1.name, o.char_id) ASC, COALESCE(p1.name, o.char_id) ASC"

        return self.db.query(sql, [AltsService.MAIN, channel])

    @event(PrivateChannelService.JOINED_PRIVATE_CHANNEL_EVENT,
           "Record in database when someone joins private channel",
           is_system=True)
    def private_channel_joined_event(self, event_type, event_data):
        self.pork_service.load_character_info(event_data.char_id)
        self.db.exec(
            "INSERT INTO online (char_id, afk_dt, afk_reason, channel, dt) VALUES (?, ?, ?, ?, ?)",
            [
                event_data.char_id, 0, "", self.PRIVATE_CHANNEL,
                int(time.time())
            ])

    @event(PrivateChannelService.LEFT_PRIVATE_CHANNEL_EVENT,
           "Record in database when someone leaves private channel",
           is_system=True)
    def private_channel_left_event(self, event_type, event_data):
        self.db.exec("DELETE FROM online WHERE char_id = ? AND channel = ?",
                     [event_data.char_id, self.PRIVATE_CHANNEL])

    @event(OrgMemberController.ORG_MEMBER_LOGON_EVENT,
           "Record in database when org member logs on",
           is_system=True)
    def org_member_logon_record_event(self, event_type, event_data):
        self.pork_service.load_character_info(event_data.char_id)
        self.db.exec(
            "INSERT INTO online (char_id, afk_dt, afk_reason, channel, dt) VALUES (?, ?, ?, ?, ?)",
            [event_data.char_id, 0, "", self.ORG_CHANNEL,
             int(time.time())])

    @event(OrgMemberController.ORG_MEMBER_LOGOFF_EVENT,
           "Record in database when org member logs off",
           is_system=True)
    def org_member_logoff_record_event(self, event_type, event_data):
        self.db.exec("DELETE FROM online WHERE char_id = ? AND channel = ?",
                     [event_data.char_id, self.ORG_CHANNEL])

    @event(PrivateChannelService.PRIVATE_CHANNEL_MESSAGE_EVENT,
           "Check for afk messages in private channel")
    def afk_check_private_channel_event(self, event_type, event_data):
        if not self.bot.get_conn_by_char_id(event_data.char_id):
            self.afk_check(
                event_data.char_id, event_data.message, lambda msg: self.bot.
                send_private_channel_message(msg, conn=event_data.conn))

    @event(PublicChannelService.ORG_CHANNEL_MESSAGE_EVENT,
           "Check for afk messages in org channel")
    def afk_check_org_channel_event(self, event_type, event_data):
        if not self.bot.get_conn_by_char_id(event_data.char_id):
            self.afk_check(
                event_data.char_id, event_data.message,
                lambda msg: self.bot.send_org_message(msg,
                                                      conn=event_data.conn))

    @event(OrgMemberController.ORG_MEMBER_LOGON_EVENT,
           "Send online list to org members logging in")
    def org_member_send_logon_event(self, event_type, event_data):
        if self.bot.is_ready():
            self.bot.send_private_message(event_data.char_id,
                                          self.get_online_output(),
                                          conn=event_data.conn)

    @event(PrivateChannelService.JOINED_PRIVATE_CHANNEL_EVENT,
           "Send online list to characters joining the private channel")
    def private_channel_send_logon_event(self, event_type, event_data):
        self.bot.send_private_message(event_data.char_id,
                                      self.get_online_output(),
                                      conn=event_data.conn)

    def afk_check(self, char_id, message, channel_reply):
        matches = self.afk_regex.search(message)
        if matches:
            char_name = self.character_service.resolve_char_to_name(char_id)
            self.set_afk(char_id, int(time.time()), message)
            # channel_reply("<highlight>%s</highlight> is now afk." % char_name)
        else:
            # will handle multiple rows since set_afk() will update multiple
            row = self.db.query_single(
                "SELECT * FROM online WHERE char_id = ? AND afk_dt > 0",
                [char_id])
            if row:
                self.set_afk(char_id, 0, "")
                char_name = self.character_service.resolve_char_to_name(
                    char_id)
                time_string = self.util.time_to_readable(
                    int(time.time()) - row.afk_dt)
                channel_reply("<highlight>%s</highlight> is back after %s." %
                              (char_name, time_string))

    def set_afk(self, char_id, dt, reason):
        self.db.exec(
            "UPDATE online SET afk_dt = ?, afk_reason = ? WHERE char_id = ?",
            [dt, reason, char_id])

    def get_online_output(self):
        num_org_bots = len(
            self.bot.get_conns(lambda x: x.is_main and x.org_id))

        blob = ""
        count = 0
        for channel, _ in self.channels:
            online_list = self.get_online_characters(channel)
            if len(online_list) == 0:
                continue

            blob += "<header2>%s Channel</header2>\n" % channel

            current_main = ""
            for row in online_list:
                if current_main != row.main:
                    count += 1
                    blob += "\n<pagebreak>%s\n" % self.text.make_tellcmd(
                        row.main, "alts %s" % row.main)
                    current_main = row.main

                afk = ""
                if row.afk_dt > 0:
                    afk = " - <highlight>%s (%s ago)</highlight>" % (
                        row.afk_reason,
                        self.util.time_to_readable(
                            int(time.time()) - row.afk_dt))

                org_info = ""
                if channel == self.PRIVATE_CHANNEL or num_org_bots > 1:
                    if row.org_name:
                        org_info = ", %s (%s)" % (row.org_name,
                                                  row.org_rank_name)

                blob += "  %s (%d/<green>%d</green>) %s%s%s\n" % (
                    row.name, row.level or 0, row.ai_level
                    or 0, row.profession, org_info, afk)
            blob += "\n\n"

        return ChatBlob("Online (%d)" % count, blob)

    def get_char_info_display(self, char_id, conn: Conn):
        char_info = self.pork_service.get_character_info(char_id)
        if char_info:
            name = self.text.format_char_info(char_info)
        else:
            char_name = self.character_service.resolve_char_to_name(char_id)
            name = "<highlight>%s</highlight>" % char_name

        alts = self.alts_service.get_alts(char_id)
        cnt = len(alts)
        if cnt > 1:
            if alts[0].char_id == char_id:
                main = "Alts (%d)" % cnt
            else:
                main = "Alts of %s (%d)" % (alts[0].name, cnt)

            name += " - " + self.text.paginate_single(
                ChatBlob(main, self.alts_controller.format_alt_list(alts)),
                conn)

        return name

    def get_online_alts(self, channel, profession, include_offline=False):
        sql = "SELECT DISTINCT " \
                "p2.*, " \
                "(CASE WHEN o2.char_id IS NULL THEN 0 ELSE 1 END) AS online, " \
                "COALESCE(p1.name, o.char_id) AS main, " \
                "COALESCE(p2.name, o.char_id) AS name, " \
                "o.channel " \
              "FROM online o " \
              "LEFT JOIN alts a1 ON o.char_id = a1.char_id " \
              "LEFT JOIN alts a2 ON a1.group_id = a2.group_id AND a2.status = ? " \
              "LEFT JOIN player p1 ON p1.char_id = COALESCE(a2.char_id, o.char_id) " \
              "LEFT JOIN alts a3 ON a2.group_id = a3.group_id " \
              "LEFT JOIN player p2 ON p2.char_id = COALESCE(a3.char_id, o.char_id) " \
              "LEFT JOIN online o2 ON p2.char_id = o2.char_id " \
              "WHERE o.channel = ? AND p2.profession = ?"

        if not include_offline:
            sql += " AND o2.char_id IS NOT NULL"

        sql += " ORDER BY p1.name ASC, p2.name ASC"

        return self.db.query(sql, [AltsService.MAIN, channel, profession])

    def sort_channels(self, item):
        channel, priority = item
        return str(priority) + channel

    def register_online_channel(self, channel_name, sort_priority=2):
        for name, _ in self.channels:
            if name == channel_name:
                return

        self.channels.append((channel_name, sort_priority))
        self.channels.sort(key=self.sort_channels)

    def deregister_online_channel(self, channel):
        self.db.query("DELETE FROM online WHERE channel = ?", [channel])
        for i, obj in enumerate(self.channels):
            if obj[0] == channel:
                del self.channels[i]
Exemplo n.º 6
0
class RaidController:
    MESSAGE_SOURCE = "raid"
    NO_RAID_RUNNING_RESPONSE = "No raid is running."

    def __init__(self):
        self.raid: Raid = None

    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.setting_service: SettingService = registry.get_instance("setting_service")
        self.alts_service: AltsService = registry.get_instance("alts_service")
        self.buddy_service = registry.get_instance("buddy_service")
        self.character_service: CharacterService = registry.get_instance("character_service")
        self.private_channel_service = registry.get_instance("private_channel_service")
        self.points_controller: PointsController = registry.get_instance("points_controller")
        self.util: Util = registry.get_instance("util")
        self.message_hub_service = registry.get_instance("message_hub_service")
        self.leader_controller = registry.get_instance("leader_controller")
        self.topic_controller = registry.get_instance("topic_controller")
        self.member_controller = registry.get_instance("member_controller")

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

        self.db.exec("CREATE TABLE IF NOT EXISTS raid_log (raid_id INT PRIMARY KEY AUTO_INCREMENT, raid_name VARCHAR(255) NOT NULL, "
                     "started_by BIGINT NOT NULL, raid_start INT NOT NULL, raid_end INT NOT NULL)")
        self.db.exec("CREATE TABLE IF NOT EXISTS raid_log_participants (raid_id INT NOT NULL, raider_id BIGINT NOT NULL, "
                     "accumulated_points INT DEFAULT 0, left_raid INT, was_kicked INT, was_kicked_reason VARCHAR(500))")

        self.db.load_sql_file(self.module_dir + "/" + "raid_loot.sql")

    @command(command="raid", params=[], access_level="member",
             description="Show the current raid status")
    def raid_cmd(self, request):
        if not self.raid:
            return self.NO_RAID_RUNNING_RESPONSE

        t = int(time.time())

        blob = ""
        blob += "Name: <highlight>%s</highlight>\n" % self.raid.raid_name
        blob += "Started By: <highlight>%s</highlight>\n" % self.raid.started_by.name
        blob += "Started At: <highlight>%s</highlight> (%s ago)\n" % (self.util.format_datetime(self.raid.started_at), self.util.time_to_readable(t - self.raid.started_at))
        blob += "Status: %s" % ("<green>Open</green>" if self.raid.is_open else "<red>Closed</red>")
        if self.raid.is_open:
            blob += " (%s)" % self.text.make_tellcmd("Join", "raid join")
        blob += "\n\n"

        topic = self.topic_controller.get_topic()
        if topic:
            time_str = self.util.time_to_readable(int(time.time()) - topic["created_at"])
            blob += "<header2>Orders</header2>\n"
            blob += "%s\n- <highlight>%s</highlight> %s ago\n\n" % (topic["topic_message"], topic["created_by"]["name"], time_str)

        blob += "<header2>Raiders</header2>\n"
        for raider in self.raid.raiders:
            if raider.is_active:
                blob += self.text.format_char_info(raider.get_active_char()) + "\n"

        return ChatBlob("Raid Status", blob)

    @command(command="raid", params=[Const("start"), Any("raid_name")],
             description="Start new raid", access_level="moderator", sub_command="manage")
    def raid_start_cmd(self, request, _, raid_name: str):
        if self.raid:
            return f"The raid <highlight>{self.raid.raid_name}</highlight> is already running."

        # if a leader is already set, only start raid if sender can take leader from current leader
        msg = self.leader_controller.set_raid_leader(request.sender, request.sender, request.conn)
        request.reply(msg)
        leader = self.leader_controller.get_leader(request.conn)
        if leader and leader.char_id != request.sender.char_id:
            return None

        self.raid = Raid(raid_name, request.sender)

        sql = "INSERT INTO raid_log (raid_name, started_by, raid_start, raid_end) VALUES (?,?,?,?)"
        self.db.exec(sql, [self.raid.raid_name, self.raid.started_by.char_id, self.raid.started_at, 0])
        self.raid.raid_id = self.db.last_insert_id()

        leader_alts = self.alts_service.get_alts(request.sender.char_id)
        self.raid.raiders.append(Raider(leader_alts, request.sender.char_id))

        join_link = self.text.paginate_single(ChatBlob("Click here", self.get_raid_join_blob()), request.conn)

        msg = "\n<highlight>----------------------------------------</highlight>\n"
        msg += "<highlight>%s</highlight> has started the raid <highlight>%s</highlight>.\n" % (request.sender.name, raid_name)
        msg += "%s to join\n" % join_link
        msg += "<highlight>----------------------------------------</highlight>"

        self.send_message(msg, request.conn)

    @command(command="raid", params=[Const("cancel")], description="Cancel the raid without saving/logging",
             access_level="moderator", sub_command="manage")
    def raid_cancel_cmd(self, request, _):
        if self.raid is None:
            return self.NO_RAID_RUNNING_RESPONSE

        self.send_message("<highlight>%s</highlight> canceled the raid <highlight>%s</highlight>." % (request.sender.name, self.raid.raid_name), request.conn)
        self.raid = None
        self.topic_controller.clear_topic()

    @command(command="raid", params=[Const("join")], description="Join the ongoing raid", access_level="member")
    def raid_join_cmd(self, request, _):
        if not self.raid:
            return self.NO_RAID_RUNNING_RESPONSE

        main_id = self.alts_service.get_main(request.sender.char_id).char_id
        in_raid = self.is_in_raid(main_id)

        if in_raid is not None:
            if in_raid.active_id == request.sender.char_id:
                if in_raid.is_active:
                    return "You are already participating in the raid."
                else:
                    if not self.raid.is_open:
                        return "Raid is closed."
                    in_raid.is_active = True
                    in_raid.was_kicked = None
                    in_raid.was_kicked_reason = None
                    in_raid.left_raid = None

                    self.points_controller.add_log_entry(main_id, request.sender.char_id, f"Joined raid {self.raid.raid_name}")
                    self.send_message("%s returned to actively participating in the raid." % request.sender.name, request.conn)

            elif in_raid.is_active:
                former_active_name = self.character_service.resolve_char_to_name(in_raid.active_id)
                in_raid.active_id = request.sender.char_id
                self.points_controller.add_log_entry(main_id, request.sender.char_id,
                                                     f"Switched to alt {request.sender.name} ({request.sender.char_id} in raid {self.raid.raid_name}")
                self.send_message("<highlight>%s</highlight> joined the raid with a different alt, <highlight>%s</highlight>." % (former_active_name, request.sender.name),
                                  request.conn)

            elif not in_raid.is_active:
                if not self.raid.is_open:
                    return "Raid is closed."

                self.points_controller.add_log_entry(main_id, request.sender.char_id,
                                                     f"Switched to alt {request.sender.name} in raid {self.raid.raid_name}")
                self.points_controller.add_log_entry(main_id, request.sender.char_id, f"Joined raid {self.raid.raid_name}")

                former_active_name = self.character_service.resolve_char_to_name(in_raid.active_id)
                in_raid.active_id = request.sender.char_id
                in_raid.was_kicked = None
                in_raid.was_kicked_reason = None
                in_raid.left_raid = None
                self.send_message("%s returned to actively participate with a different alt, <highlight>%s</highlight>." % (former_active_name, request.sender.name),
                                  request.conn)

        elif self.raid.is_open:
            alts = self.alts_service.get_alts(request.sender.char_id)
            self.raid.raiders.append(Raider(alts, request.sender.char_id))
            self.points_controller.add_log_entry(main_id, request.sender.char_id, f"Joined raid {self.raid.raid_name}")
            self.send_message("<highlight>%s</highlight> joined the raid." % request.sender.name, request.conn)
            if request.sender.char_id not in self.bot.get_primary_conn().private_channel:
                self.private_channel_service.invite(request.sender.char_id, self.bot.get_primary_conn())
        else:
            return "Raid is closed."

    @command(command="raid", params=[Const("leave")], description="Leave the ongoing raid", access_level="member")
    def raid_leave_cmd(self, request, _):
        main_id = self.alts_service.get_main(request.sender.char_id).char_id
        in_raid = self.is_in_raid(main_id)
        if in_raid:
            if not in_raid.is_active:
                return "You are not active in the raid."

            in_raid.is_active = False
            in_raid.left_raid = int(time.time())
            self.points_controller.add_log_entry(main_id, request.sender.char_id, f"Left raid {self.raid.raid_name}")
            self.send_message("<highlight>%s</highlight> left the raid." % request.sender.name, request.conn)
        else:
            return "You are not in the raid."

    @command(command="raid", params=[Const("addpts"), Any("name")], description="Add points to all active participants",
             access_level="moderator", sub_command="manage")
    def points_add_cmd(self, request, _, name: str):
        if not self.raid:
            return self.NO_RAID_RUNNING_RESPONSE

        preset = self.db.query_single("SELECT name, points FROM points_presets WHERE name = ?", [name])
        if not preset:
            return ChatBlob("No such preset - see list of presets", self.points_controller.build_preset_list())

        self.raid.added_points = True

        for raider in self.raid.raiders:
            account = self.points_controller.get_account(raider.main_id, request.conn)

            if raider.is_active:
                if account.disabled == 0:
                    self.points_controller.alter_points(raider.main_id, request.sender.char_id, preset.name, preset.points)
                    raider.accumulated_points += preset.points
                else:
                    self.points_controller.add_log_entry(raider.main_id, request.sender.char_id,
                                                         "Participated in raid with a disabled account, missed points from %s." % preset.name)
            else:
                self.points_controller.add_log_entry(raider.main_id, request.sender.char_id,
                                                     "Was inactive during raid, %s, when points for %s were dished out." % (self.raid.raid_name, preset.name))

        self.send_message("<highlight>%d</highlight> points added to all active raiders for <highlight>%s</highlight>." % (preset.points, preset.name), request.conn)

    @command(command="raid", params=[Const("active")], description="Get a list of raiders to do active check",
             access_level="moderator", sub_command="manage")
    def raid_active_cmd(self, request, _):
        if not self.raid:
            return self.NO_RAID_RUNNING_RESPONSE

        blob = ""

        count = 0
        raider_names = []
        for raider in self.raid.raiders:
            if count == 10:
                active_check_names = "/assist "
                active_check_names += "\\n /assist ".join(raider_names)
                blob += "\n[<a href='chatcmd://%s'>Active check</a>]\n\n" % active_check_names
                count = 0
                raider_names.clear()

            raider_name = self.character_service.resolve_char_to_name(raider.active_id)
            akick_link = self.text.make_tellcmd("Active kick", "raid kick %s inactive" % raider_name)
            warn_link = self.text.make_chatcmd("Warn", "/tell %s You missed active check, please give notice." % raider_name)
            blob += "<highlight>%s</highlight> [%s] [%s]\n" % (raider_name, akick_link, warn_link)
            raider_names.append(raider_name)
            count += 1

        if len(raider_names) > 0:
            active_check_names = "/assist "
            active_check_names += "\\n /assist ".join(raider_names)

            blob += "\n[<a href='chatcmd://%s'>Active check</a>]\n\n" % active_check_names
            raider_names.clear()

        return ChatBlob("Active check", blob)

    @command(command="raid", params=[Const("add"), Character("char")],
             description="Add a character to the raid", access_level="moderator", sub_command="manage")
    def raid_add_cmd(self, request, _, char):
        if self.raid is None:
            return self.NO_RAID_RUNNING_RESPONSE

        alts = self.alts_service.get_alts(char.char_id)
        main_id = alts[0].char_id
        in_raid = self.is_in_raid(main_id)

        if in_raid is None:
            self.raid.raiders.append(Raider(alts, char.char_id))
            self.bot.send_private_message(char.char_id,
                                          f"You have been added to the raid <highlight>{self.raid.raid_name}</highlight>.",
                                          conn=request.conn)
            self.points_controller.add_log_entry(main_id, request.sender.char_id, f"Added to raid {self.raid.raid_name}")
            if char.char_id not in self.bot.get_primary_conn().private_channel:
                self.private_channel_service.invite(char.char_id)
            return "<highlight>%s</highlight> has been added to the raid." % char.name
        else:
            if not in_raid.is_active:
                in_raid.is_active = True
                in_raid.was_kicked = None
                in_raid.was_kicked_reason = None
                in_raid.left_raid = None
                self.points_controller.add_log_entry(main_id, request.sender.char_id, f"Added to raid {self.raid.raid_name}")
                self.bot.send_private_message(char.char_id,
                                              f"You have been set as active in the raid <highlight>{self.raid.raid_name}</highlight>.",
                                              conn=request.conn)
                return f"<highlight>{char.name}</highlight> has been set as active."
            else:
                return f"<highlight>{char.name}</highlight> is already in the raid."

    @command(command="raid", params=[Const("kick"), Character("char"), Any("reason")],
             description="Set raider as kicked with a reason", access_level="moderator", sub_command="manage")
    def raid_kick_cmd(self, request, _, char: SenderObj, reason: str):
        if self.raid is None:
            return self.NO_RAID_RUNNING_RESPONSE

        main_id = self.alts_service.get_main(char.char_id).char_id
        in_raid = self.is_in_raid(main_id)

        if in_raid is not None:
            if not in_raid.is_active:
                return "<highlight>%s</highlight> is already set as inactive." % char.name

            in_raid.is_active = False
            in_raid.was_kicked = int(time.time())
            in_raid.was_kicked_reason = reason
            self.points_controller.add_log_entry(main_id, request.sender.char_id, f"Kicked from raid {self.raid.raid_name} with reason: {reason}")
            self.bot.send_private_message(char.char_id,
                                          f"You have been kicked from raid <highlight>{self.raid.raid_name}</highlight> with reason <highlight>{reason}</highlight>.",
                                          conn=request.conn)
            return "<highlight>%s</highlight> has been kicked from the raid with reason <highlight>%s</highlight>." % (char.name, reason)
        else:
            return "<highlight>%s</highlight> is not participating." % char.name

    @command(command="raid", params=[Options(["open", "unlock"])], description="Open raid for new participants",
             access_level="moderator", sub_command="manage")
    def raid_open_cmd(self, request, action):
        if not self.raid:
            return self.NO_RAID_RUNNING_RESPONSE

        if self.raid.is_open:
            return "Raid is already open."
        else:
            self.raid.is_open = True
            self.send_message("Raid has been opened by %s." % request.sender.name, request.conn)

    @command(command="raid", params=[Options(["close", "lock"])], description="Close raid for new participants",
             access_level="moderator", sub_command="manage")
    def raid_close_cmd(self, request, action):
        if not self.raid:
            return self.NO_RAID_RUNNING_RESPONSE

        if self.raid.is_open:
            self.raid.is_open = False
            self.send_message("Raid has been closed by %s." % request.sender.name, request.conn)
        else:
            return "Raid is already closed."

    @command(command="raid", params=[Options(["end", "save"]), NamedFlagParameters(["force"])], description="End raid, and log results",
             access_level="moderator", sub_command="manage")
    def raid_save_cmd(self, request, _, flag_params):
        if not self.raid:
            return self.NO_RAID_RUNNING_RESPONSE

        if not self.raid.added_points and not flag_params.force:
            blob = "You have not added any points for this raid. Are you sure you want to end this raid now? "
            blob += self.text.make_tellcmd("Yes", "raid end --force")
            return ChatBlob("End Raid Confirmation", blob)

        sql = "UPDATE raid_log SET raid_end = ? WHERE raid_id = ?"
        self.db.exec(sql, [int(time.time()), self.raid.raid_id])

        for raider in self.raid.raiders:
            sql = "INSERT INTO raid_log_participants (raid_id, raider_id, accumulated_points, left_raid, was_kicked, was_kicked_reason) VALUES (?,?,?,?,?,?)"
            self.db.exec(sql, [self.raid.raid_id, raider.active_id, raider.accumulated_points, raider.left_raid, raider.was_kicked, raider.was_kicked_reason])

        self.raid = None
        self.topic_controller.clear_topic()

        self.send_message("Raid saved and ended.", request.conn)

    @command(command="raid", params=[Const("history"), Int("raid_id")],
             description="Show log entry for raid",
             access_level="moderator", sub_command="manage")
    def raid_history_detail_cmd(self, request, _, raid_id: int):
        sql = "SELECT r.*, p.*, p2.name AS raider_name FROM raid_log r " \
              "LEFT JOIN raid_log_participants p ON r.raid_id = p.raid_id " \
              "LEFT JOIN player p2 ON p.raider_id = p2.char_id " \
              "WHERE r.raid_id = ? ORDER BY p.accumulated_points DESC"
        log_entry = self.db.query(sql, [raid_id])

        if not log_entry:
            return "No such log entry."

        blob = "Raid name: <highlight>%s</highlight>\n" % log_entry[0].raid_name
        blob += "Started by: <highlight>%s</highlight>\n" % self.character_service.resolve_char_to_name(log_entry[0].started_by)
        blob += "Start time: <highlight>%s</highlight>\n" % self.util.format_datetime(log_entry[0].raid_start)
        blob += "End time: <highlight>%s</highlight>\n" % self.util.format_datetime(log_entry[0].raid_end)
        blob += "Run time: <highlight>%s</highlight>\n" % self.util.time_to_readable(log_entry[0].raid_end - log_entry[0].raid_start)

        pts_sum = self.db.query_single("SELECT COALESCE(SUM(p.accumulated_points), 0) AS sum FROM raid_log_participants p WHERE p.raid_id = ?", [raid_id]).sum
        blob += "Total points: <highlight>%d</highlight>\n\n" % pts_sum

        blob += "<header2>Participants</header2>\n"
        for raider in log_entry:
            main_info = self.alts_service.get_main(raider.raider_id)
            if main_info.char_id != raider.raider_id:
                alt_link_text = "Alt of %s" % main_info.name
            else:
                alt_link_text = "Alts"
            alt_link = self.text.make_tellcmd(alt_link_text, "alts %s" % raider.raider_name)
            account_link = self.text.make_tellcmd("Account", "account %s" % raider.raider_name)
            blob += "%s - %d points earned [%s] [%s]\n" % (raider.raider_name, raider.accumulated_points, account_link, alt_link)

            if raider.left_raid:
                blob += "Left raid: %s\n" % self.util.format_datetime(raider.left_raid)

            if raider.was_kicked:
                blob += "Was kicked: %s\n" % self.util.format_datetime(raider.was_kicked)

            if raider.was_kicked_reason:
                blob += "Kick reason: %s\n" % raider.was_kicked_reason

            blob += "\n"

        return ChatBlob("Raid: %s" % log_entry[0].raid_name, blob)

    @command(command="raid", params=[Const("history")], description="Show a list of recent raids",
             access_level="member")
    def raid_history_cmd(self, request, _):
        sql = "SELECT * FROM raid_log ORDER BY raid_end DESC LIMIT 30"
        raids = self.db.query(sql)

        blob = ""
        for raid in raids:
            participant_link = self.text.make_tellcmd("Detail", "raid history %d" % raid.raid_id)
            timestamp = self.util.format_datetime(raid.raid_start)
            leader_name = self.character_service.resolve_char_to_name(raid.started_by)
            blob += "[%d] [%s] <highlight>%s</highlight> started by <highlight>%s</highlight> [%s]\n" % (raid.raid_id, timestamp, raid.raid_name, leader_name, participant_link)

        return ChatBlob("Raid History (%d)" % len(raids), blob)

    @command(command="raid", params=[Const("announce"), Any("message", is_optional=True)], access_level="moderator", sub_command="manage",
             description="Announce the current raid to members")
    def raid_announce_cmd(self, request, _, message):
        if not self.raid:
            return self.NO_RAID_RUNNING_RESPONSE

        if not self.bot.mass_message_queue:
            return "Could not announce raid since bot does not have mass messaging capabilities."

        join_link = self.text.paginate_single(ChatBlob("Click here", self.get_raid_join_blob()), request.conn)

        msg = "<highlight>%s</highlight> has started the raid <highlight>%s</highlight>. " % (self.raid.started_by.name, self.raid.raid_name)
        msg += "%s to join." % join_link
        if message:
            msg += " " + message

        count = 0
        for member in self.member_controller.get_all_members():
            main = self.alts_service.get_main(member.char_id)
            if self.buddy_service.is_online(member.char_id) and not self.is_in_raid(main.char_id):
                count += 1
                self.bot.send_mass_message(member.char_id, msg, conn=request.conn)

        return f"Raid announcement is sending to <highlight>{count}</highlight> online members."

    def is_in_raid(self, main_id: int):
        if self.raid is None:
            return None

        for raider in self.raid.raiders:
            if raider.main_id == main_id:
                return raider

    def get_raid_join_blob(self):
        return "<header2>1. Join the raid</header2>\n" \
               "To join the current raid <highlight>%s</highlight>, send the following tell to <myname>\n" \
               "<tab><tab><a href='chatcmd:///tell <myname> <symbol>raid join'>/tell <myname> raid " \
               "join</a>\n\n<header2>2. Enable LFT</header2>\nWhen you have joined the raid, go lft " \
               "with \"<myname>\" as description\n<tab><tab><a href='chatcmd:///lft <myname>'>/lft <myname></a>\n\n" \
               "<header2>3. Announce</header2>\nYou could announce to the raid leader, that you have enabled " \
               "LFT\n<tab><tab><a href='chatcmd:///group <myname> I am on lft'>Announce</a> that you have enabled " \
               "lft\n\n<header2>4. Rally with yer mateys</header2>\nFinally, move towards the starting location of " \
               "the raid.\n<highlight>Ask for help</highlight> if you're in doubt of where to go." % self.raid.raid_name

    def send_message(self, msg, conn):
        # TODO remove once messagehub can handle ChatBlobs
        pages = self.bot.get_text_pages(msg, conn, self.setting_service.get("private_message_max_page_length").get_value())
        for page in pages:
            self.message_hub_service.send_message(self.MESSAGE_SOURCE, None, None, page)
Exemplo n.º 7
0
class ConfigController:
    def inject(self, registry):
        self.db: DB = registry.get_instance("db")
        self.text: Text = registry.get_instance("text")
        self.command_service = registry.get_instance("command_service")
        self.event_service = registry.get_instance("event_service")
        self.setting_service = registry.get_instance("setting_service")
        self.config_events_controller = registry.get_instance(
            "config_events_controller")
        self.ts: TranslationService = registry.get_instance(
            "translation_service")
        self.getresp = self.ts.get_response

    def start(self):
        self.ts.register_translation("module/config", self.load_config_msg)

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

    @command(command="config",
             params=[],
             access_level="admin",
             description="Show configuration options for the bot")
    def config_list_cmd(self, request):
        sql = """SELECT
                module,
                SUM(CASE WHEN enabled = 1 THEN 1 ELSE 0 END) count_enabled,
                SUM(CASE WHEN enabled = 0 THEN 1 ELSE 0 END) count_disabled
            FROM
                (SELECT module, enabled FROM command_config
                UNION
                SELECT module, enabled FROM event_config WHERE is_hidden = 0
                UNION
                SELECT module, 2 FROM setting) t
            GROUP BY
                module
            ORDER BY
                module ASC"""

        data = self.db.query(sql)
        count = len(data)
        blob = ""
        current_group = ""
        for row in data:
            parts = row.module.split(".")
            group = parts[0]
            module = parts[1]
            if group != current_group:
                current_group = group
                blob += "\n<header2>" + current_group + "</header2>\n"

            blob += self.text.make_tellcmd(module,
                                           "config mod " + row.module) + " "
            if row.count_enabled > 0 and row.count_disabled > 0:
                blob += self.getresp("module/config", "partial")
            else:
                blob += self.getresp(
                    "module/config", "enabled_high"
                    if row.count_disabled == 0 else "disabled_high")
            blob += "\n"

        return ChatBlob(
            self.getresp("module/config", "config", {"count": count}), blob)

    @command(command="config",
             params=[
                 Options(["mod", "module"]),
                 Any("module_name"),
                 NamedFlagParameters(["include_hidden_events"])
             ],
             access_level="admin",
             description="Show configuration options for a specific module")
    def config_module_list_cmd(self, request, _, module, named_params):
        module = module.lower()

        blob = ""

        data = self.db.query(
            "SELECT name FROM setting WHERE module = ? ORDER BY name ASC",
            [module])
        if data:
            blob += self.getresp("module/config", "settings")
            for row in data:
                setting = self.setting_service.get(row.name)
                blob += "%s: %s (%s)\n" % (
                    setting.get_description(), setting.get_display_value(),
                    self.text.make_tellcmd("change",
                                           "config setting " + row.name))

        data = self.db.query(
            "SELECT DISTINCT command, sub_command FROM command_config WHERE module = ? ORDER BY command ASC",
            [module])
        if data:
            blob += self.getresp("module/config", "commands")
            for row in data:
                command_key = self.command_service.get_command_key(
                    row.command, row.sub_command)
                blob += self.text.make_tellcmd(
                    command_key, "config cmd " + command_key) + "\n"

        blob += self.format_events(self.get_events(module, False),
                                   self.getresp("module/config", "events"))

        if named_params.include_hidden_events:
            blob += self.format_events(
                self.get_events(module, True),
                self.getresp("module/config", "hidden_events"))

        if blob:
            if not named_params.include_hidden_events:
                blob += "\n" + self.text.make_tellcmd(
                    self.getresp("module/config", "include_hidden_events"),
                    f"config mod {module} --include_hidden_events")

            return ChatBlob(
                self.getresp("module/config", "mod_title", {"mod": module}),
                blob)
        else:
            return self.getresp("module/config", "mod_not_found",
                                {"mod": module})

    @command(command="config",
             params=[Const("settinglist")],
             access_level="admin",
             description="List all settings")
    def config_settinglist_cmd(self, request, _):
        blob = ""

        data = self.db.query("SELECT * FROM setting ORDER BY module, name ASC")
        count = len(data)
        if data:
            blob += self.getresp("module/config", "settings")
            current_module = ""
            for row in data:
                if row.module != current_module:
                    current_module = row.module
                    blob += "\n<pagebreak><header2>%s</header2>\n" % row.module

                setting = self.setting_service.get(row.name)
                blob += "%s: %s (%s)\n" % (
                    setting.get_description(), setting.get_display_value(),
                    self.text.make_tellcmd("change",
                                           "config setting " + row.name))

        return ChatBlob(
            self.getresp("module/config", "settinglist_title",
                         {"count": count}), blob)

    @command(command="config",
             params=[
                 Const("setting"),
                 Any("setting_name"),
                 Options(["set", "clear"]),
                 Any("new_value", is_optional=True)
             ],
             access_level="admin",
             description="Change a setting value")
    def config_setting_update_cmd(self, request, _, setting_name, op,
                                  new_value):
        setting_name = setting_name.lower()

        if op == "clear":
            new_value = ""
        elif not new_value:
            return self.getresp("module/config", "no_new_value")
        setting = self.setting_service.get(setting_name)

        if setting:
            setting.set_value(new_value)
            if op == "clear":
                return self.getresp("module/config", "set_clr",
                                    {"setting": setting_name})
            else:
                return self.getresp("module/config", "set_new", {
                    "setting": setting_name,
                    "value": setting.get_display_value()
                })
        else:
            return self.getresp("module/config", "setting_not_found",
                                {"setting": setting_name})

    @command(command="config",
             params=[Const("setting"), Any("setting_name")],
             access_level="admin",
             description="Show configuration options for a setting")
    def config_setting_show_cmd(self, request, _, setting_name):
        setting_name = setting_name.lower()

        blob = ""

        setting = self.setting_service.get(setting_name)

        if setting:
            blob += self.getresp("module/config", "current_value",
                                 {"value": str(setting.get_display_value())})
            blob += self.getresp("module/config", "description",
                                 {"desc": setting.get_description()})
            if setting.get_extended_description():
                blob += setting.get_extended_description() + "\n\n"
            blob += setting.get_display()
            return ChatBlob(
                self.getresp("module/config", "setting",
                             {"setting": setting_name}), blob)
        else:
            return self.getresp("module/config", "setting_not_found",
                                {"setting": setting_name})

    def get_events(self, module, is_hidden):
        return self.db.query(
            "SELECT event_type, event_sub_type, handler, description, enabled, is_hidden "
            f"FROM event_config WHERE module = ? AND is_hidden = ? "
            "ORDER BY is_hidden, event_type, handler ASC",
            [module, 1 if is_hidden else 0])

    def format_events(self, data, title):
        blob = ""
        if data:
            blob += f"\n<header2>{title}</header2>\n"
            for row in data:
                event_type_key = self.event_service.get_event_type_key(
                    row.event_type, row.event_sub_type)
                enabled = self.getresp(
                    "module/config",
                    "enabled_high" if row.enabled == 1 else "disabled_high")
                blob += "%s - %s [%s]" % (
                    self.config_events_controller.format_event_type(row),
                    row.description, enabled)
                blob += " " + self.text.make_tellcmd(
                    "On", "config event %s %s enable" %
                    (event_type_key, row.handler))
                blob += " " + self.text.make_tellcmd(
                    "Off", "config event %s %s disable" %
                    (event_type_key, row.handler))
                if row.event_type == "timer":
                    blob += " " + self.text.make_tellcmd(
                        "Run Now", "config event %s %s run" %
                        (event_type_key, row.handler))
                blob += "\n"
        return blob
Exemplo n.º 8
0
class HelpController:
    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.text = registry.get_instance("text")
        self.db = registry.get_instance("db")
        self.access_service = registry.get_instance("access_service")
        self.command_service = registry.get_instance("command_service")
        self.command_alias_service = registry.get_instance(
            "command_alias_service")
        self.ts: TranslationService = registry.get_instance(
            "translation_service")
        self.getresp = self.ts.get_response

    def start(self):
        self.ts.register_translation("module/help", self.load_help_msg)
        self.command_alias_service.add_alias("version", "about")

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

    @command(command="about",
             params=[],
             access_level="all",
             description="Show information about the development of this bot")
    def about_cmd(self, request):
        with open(os.path.dirname(os.path.realpath(__file__)) + os.sep +
                  "about.txt",
                  mode="r",
                  encoding="UTF-8") as f:
            return ChatBlob(
                self.getresp("module/help", "about_title",
                             {"version": self.bot.version}), f.read())

    @command(command="help",
             params=[],
             access_level="all",
             description="Show a list of commands to get help with")
    def help_list_cmd(self, request):
        data = self.db.query(
            "SELECT command, module, access_level FROM command_config "
            "WHERE enabled = 1 "
            "ORDER BY module ASC, command ASC")
        blob = ""
        current_group = ""
        current_module = ""
        current_command = ""
        access_level = self.access_service.get_access_level(
            request.sender.char_id)
        for row in data:
            if access_level[
                    "level"] > self.access_service.get_access_level_by_label(
                        row.access_level)["level"]:
                continue

            parts = row.module.split(".")
            group = parts[0]
            module = parts[1]
            if group != current_group:
                current_group = group
                blob += "\n\n<header2>" + current_group + "</header2>"

            if module != current_module:
                current_module = module
                blob += "\n" + module + ":"

            if row.command != current_command:
                current_command = row.command
                blob += " " + self.text.make_tellcmd(row.command,
                                                     "help " + row.command)

        return ChatBlob("Help (main)", blob)

    @command(command="help",
             params=[Any("command"),
                     NamedFlagParameters(["show_regex"])],
             access_level="all",
             description="Show help for a specific command")
    def help_detail_cmd(self, request, help_topic, named_params):
        help_topic = help_topic.lower()

        # check for alias
        alias = self.command_alias_service.check_for_alias(help_topic)
        if alias:
            help_topic = alias

        help_text = self.command_service.get_help_text(request.sender.char_id,
                                                       help_topic,
                                                       request.channel,
                                                       named_params.show_regex)
        if help_text:
            return self.command_service.format_help_text(help_topic, help_text)
        else:
            return self.getresp("module/help", "no_help",
                                {"topic": help_topic})
Exemplo n.º 9
0
class ConfigCommandController:
    def inject(self, registry):
        self.db: DB = registry.get_instance("db")
        self.text: Text = registry.get_instance("text")
        self.access_service = registry.get_instance("access_service")
        self.command_service = registry.get_instance("command_service")
        self.ts: TranslationService = registry.get_instance(
            "translation_service")
        self.getresp = self.ts.get_response

    @command(command="config",
             params=[
                 Const("cmd"),
                 Any("cmd_name"),
                 Options(["enable", "disable"]),
                 Any("channel")
             ],
             access_level="admin",
             description="Enable or disable a command")
    def config_cmd_status_cmd(self, request, _, cmd_name, action, cmd_channel):
        cmd_name = cmd_name.lower()
        action = action.lower()
        cmd_channel = cmd_channel.lower()
        command_str, sub_command_str = self.command_service.get_command_key_parts(
            cmd_name)
        enabled = 1 if action == "enable" else 0

        if cmd_channel != "all" and not self.command_service.is_command_channel(
                cmd_channel):
            return self.getresp("module/config", "cmd_unknown_channel",
                                {"channel": cmd_channel})

        if not self.has_sufficient_access_level(request.sender.char_id,
                                                command_str, sub_command_str,
                                                cmd_channel):
            return "You do not have the required access level to change this command."

        sql = "UPDATE command_config SET enabled = ? WHERE command = ? AND sub_command = ?"
        params = [enabled, command_str, sub_command_str]
        if cmd_channel != "all":
            sql += " AND channel = ?"
            params.append(cmd_channel)

        count = self.db.exec(sql, params)
        if count == 0:
            return self.getresp("module/config", "cmd_unknown_for_channel", {
                "channel": cmd_channel,
                "cmd": cmd_name
            })
        else:
            action = self.getresp(
                "module/config",
                "enabled_low" if action == "enable" else "disabled_low")
            if cmd_channel == "all":
                return self.getresp("module/config", "cmd_toggle_success", {
                    "cmd": cmd_name,
                    "changedto": action
                })
            else:
                return self.getresp("module/config",
                                    "cmd_toggle_channel_success", {
                                        "channel": cmd_channel,
                                        "cmd": cmd_name,
                                        "changedto": action
                                    })

    @command(command="config",
             params=[
                 Const("cmd"),
                 Any("cmd_name"),
                 Const("access_level"),
                 Any("channel"),
                 Any("access_level")
             ],
             access_level="admin",
             description="Change access_level for a command")
    def config_cmd_access_level_cmd(self, request, _1, cmd_name, _2,
                                    cmd_channel, access_level):
        cmd_name = cmd_name.lower()
        cmd_channel = cmd_channel.lower()
        access_level = access_level.lower()
        command_str, sub_command_str = self.command_service.get_command_key_parts(
            cmd_name)

        if cmd_channel != "all" and not self.command_service.is_command_channel(
                cmd_channel):
            return self.getresp("module/config", "cmd_unknown_channel",
                                {"channel": cmd_channel})

        if self.access_service.get_access_level_by_label(access_level) is None:
            return self.getresp("module/config", "unknown_accesslevel",
                                {"al": access_level})

        if not self.has_sufficient_access_level(request.sender.char_id,
                                                command_str, sub_command_str,
                                                cmd_channel):
            return "You do not have the required access level to change this command."

        sql = "UPDATE command_config SET access_level = ? WHERE command = ? AND sub_command = ?"
        params = [access_level, command_str, sub_command_str]
        if cmd_channel != "all":
            sql += " AND channel = ?"
            params.append(cmd_channel)

        count = self.db.exec(sql, params)
        if count == 0:
            return self.getresp("module/config", "cmd_unknown_for_channel", {
                "channel": cmd_channel,
                "cmd": cmd_name
            })
        else:
            if cmd_channel == "all":
                return self.getresp("module/config", "set_accesslevel_success",
                                    {
                                        "cmd": cmd_name,
                                        "al": access_level
                                    })
            else:
                return self.getresp("module/config", "set_accesslevel_fail", {
                    "channel": cmd_channel,
                    "cmd": cmd_name,
                    "al": access_level
                })

    @command(command="config",
             params=[
                 Const("cmd"),
                 Any("cmd_name"),
                 NamedFlagParameters(["show_channels"])
             ],
             access_level="admin",
             description="Show command configuration")
    def config_cmd_show_cmd(self, request, _, cmd_name, flag_params):
        cmd_name = cmd_name.lower()
        command_str, sub_command_str = self.command_service.get_command_key_parts(
            cmd_name)

        cmd_channel_configs = self.get_command_channel_config(
            command_str, sub_command_str)

        if not cmd_channel_configs:
            return self.getresp("module/config", "no_cmd", {"cmd": cmd_name})

        blob = ""
        if flag_params.show_channels or not self.are_command_channel_configs_same(
                cmd_channel_configs):
            blob += self.format_cmd_channel_configs_separate_channels(
                cmd_name, cmd_channel_configs)
        else:
            blob += self.format_cmd_channel_configs_consolidated(
                cmd_name, cmd_channel_configs)

        sub_commands = self.get_sub_commands(command_str, sub_command_str)
        if sub_commands:
            blob += "<header2>Subcommands</header2>\n"
            for row in sub_commands:
                command_name = self.command_service.get_command_key(
                    row.command, row.sub_command)
                blob += self.text.make_tellcmd(
                    command_name, f"config cmd {command_name}") + "\n\n"

        # include help text
        blob += "\n\n".join(
            map(lambda handler: handler["help"],
                self.command_service.get_handlers(cmd_name)))
        return ChatBlob("Command (%s)" % cmd_name, blob)

    def get_sub_commands(self, command_str, sub_command_str):
        return self.db.query(
            "SELECT DISTINCT command, sub_command FROM command_config WHERE command = ? AND sub_command != ?",
            [command_str, sub_command_str])

    def get_command_channel_config(self, command_str, sub_command_str):
        result = []
        for command_channel, channel_label in self.command_service.channels.items(
        ):
            cmd_configs = self.command_service.get_command_configs(
                command=command_str,
                sub_command=sub_command_str,
                channel=command_channel,
                enabled=None)

            if len(cmd_configs) > 0:
                result.append(
                    DictObject({
                        "channel": command_channel,
                        "channel_label": channel_label,
                        "cmd_config": cmd_configs[0]
                    }))

        return result

    def are_command_channel_configs_same(self, cmd_channel_configs):
        if len(cmd_channel_configs) < 2:
            return True

        access_level = cmd_channel_configs[0].cmd_config.access_level
        enabled = cmd_channel_configs[0].cmd_config.enabled
        for cmd_channel_config in cmd_channel_configs[1:]:
            if cmd_channel_config.cmd_config.access_level != access_level or cmd_channel_config.cmd_config.enabled != enabled:
                return False

        return True

    def format_cmd_channel_configs_separate_channels(self, cmd_name,
                                                     cmd_channel_configs):
        blob = ""
        for obj in cmd_channel_configs:
            cmd_config = obj.cmd_config

            blob += "<header2>%s</header2> " % obj.channel_label
            blob += self.format_cmd_config(cmd_name, cmd_config.enabled,
                                           cmd_config.access_level,
                                           obj.channel)

        return blob

    def format_cmd_channel_configs_consolidated(self, cmd_name,
                                                cmd_channel_configs):
        cmd_config = cmd_channel_configs[0].cmd_config
        channel = "all"

        blob = ""
        blob += self.format_cmd_config(cmd_name, cmd_config.enabled,
                                       cmd_config.access_level, channel)
        blob += self.text.make_tellcmd(
            "Configure command channels individually",
            f"config cmd {cmd_name} --show_channels")
        blob += "\n\n"

        return blob

    def format_cmd_config(self, cmd_name, enabled, access_level, channel):
        blob = ""
        status = self.getresp(
            "module/config",
            "enabled_high" if enabled == 1 else "disabled_high")

        blob += "%s (%s: %s)\n" % (
            status, self.getresp("module/config",
                                 "access_level"), access_level.capitalize())

        # show status config
        blob += "Status:"
        enable_link = self.text.make_tellcmd(
            self.getresp("module/config", "enable"),
            "config cmd %s enable %s" % (cmd_name, channel))
        disable_link = self.text.make_tellcmd(
            self.getresp("module/config", "disable"),
            "config cmd %s disable %s" % (cmd_name, channel))

        blob += "  " + enable_link + "  " + disable_link

        # show access level config
        blob += "\n" + self.getresp("module/config", "access_level")
        for access_level in self.access_service.access_levels:
            # skip "None" access level
            if access_level["level"] == 0:
                continue

            label = access_level["label"]
            link = self.text.make_tellcmd(
                label.capitalize(), "config cmd %s access_level %s %s" %
                (cmd_name, channel, label))
            blob += "  " + link
        blob += "\n\n"

        return blob

    def has_sufficient_access_level(self, char_id, command_str,
                                    sub_command_str, channel):
        access_level = self.access_service.get_access_level(char_id)

        params = [command_str, sub_command_str]
        sql = "SELECT access_level FROM command_config WHERE command = ? AND sub_command = ?"
        if channel != "all":
            sql += " AND channel = ?"
            params.append(channel)

        data = self.db.query(sql, params)
        for row in data:
            if self.access_service.compare_access_levels(
                    row.access_level, access_level["label"]) > 0:
                return False

        return True
Exemplo n.º 10
0
class CharacterInfoController:
    BUDDY_IS_ONLINE_TYPE = "is_online"

    def __init__(self):
        self.name_history = []
        self.waiting_for_update = {}

    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.db: DB = registry.get_instance("db")
        self.text: Text = registry.get_instance("text")
        self.pork_service = registry.get_instance("pork_service")
        self.command_alias_service = registry.get_instance(
            "command_alias_service")
        self.util = registry.get_instance("util")
        self.alts_service = registry.get_instance("alts_service")
        self.alts_controller = registry.get_instance("alts_controller")
        self.buddy_service = registry.get_instance("buddy_service")

    def pre_start(self):
        self.bot.register_packet_handler(CharacterName.id,
                                         self.character_name_update)

    def start(self):
        self.db.exec(
            "CREATE TABLE IF NOT EXISTS name_history (char_id INT NOT NULL, name VARCHAR(20) NOT NULL, created_at INT NOT NULL, PRIMARY KEY (char_id, name))"
        )
        self.command_alias_service.add_alias("w", "whois")
        self.command_alias_service.add_alias("lookup", "whois")
        self.command_alias_service.add_alias("is", "whois")

    @command(command="whois",
             params=[
                 Character("character"),
                 Int("server_num", is_optional=True),
                 NamedFlagParameters(["force_update"])
             ],
             access_level="member",
             description="Get whois information for a character",
             extended_description=
             "Use server_num 6 for RK2019 and server_num 5 for live")
    def whois_cmd(self, request, char, dimension, flag_params):
        dimension = dimension or self.bot.dimension
        force_update = flag_params.force_update

        if dimension == self.bot.dimension and char.char_id:
            online_status = self.buddy_service.is_online(char.char_id)
            if online_status is None:
                self.bot.register_packet_handler(BuddyAdded.id,
                                                 self.handle_buddy_status)
                self.waiting_for_update[char.char_id] = DictObject({
                    "char_id":
                    char.char_id,
                    "name":
                    char.name,
                    "callback":
                    partial(self.show_output,
                            char,
                            dimension,
                            force_update,
                            reply=request.reply,
                            conn=request.conn)
                })
                self.buddy_service.add_buddy(char.char_id,
                                             self.BUDDY_IS_ONLINE_TYPE)
            else:
                self.show_output(char, dimension, force_update, online_status,
                                 request.reply, request.conn)
        else:
            self.show_output(char, dimension, force_update, None,
                             request.reply, request.conn)

    def show_output(self, char, dimension, force_update, online_status, reply,
                    conn):
        max_cache_age = 0 if force_update else 86400

        if dimension != self.bot.dimension:
            char_info = self.pork_service.request_char_info(
                char.name, dimension)
        else:
            char_info = self.pork_service.get_character_info(
                char.name, max_cache_age)

        if char_info and char_info.source != "chat_server":
            alts = self.alts_controller.alts_service.get_alts(char.char_id)
            blob = "Name: %s [%s] [%s]\n" % (
                self.get_full_name(char_info),
                self.text.make_tellcmd(
                    "History", "history %s %s" %
                    (char.name, char_info.dimension)),
                self.text.make_tellcmd("Alts (%s)" % len(alts),
                                       f"alts {char.name}"))
            blob += "Char ID: %d\n" % char_info.char_id
            blob += "Profession: %s\n" % char_info.profession
            blob += "Faction: %s\n" % self.text.get_formatted_faction(
                char_info.faction)
            blob += "Breed: %s\n" % char_info.breed
            blob += "Gender: %s\n" % char_info.gender
            blob += "Level: %d\n" % char_info.level
            blob += "AI Level: <green>%d</green>\n" % char_info.ai_level
            if char_info.org_id:
                orglist_link = self.text.make_tellcmd(
                    "Orglist", f"orglist {char_info.org_id}")
                blob += "Org: <highlight>%s</highlight> (%d) [%s]\n" % (
                    char_info.org_name, char_info.org_id, orglist_link)
                blob += "Org Rank: %s (%d)\n" % (char_info.org_rank_name,
                                                 char_info.org_rank_id)
            else:
                blob += "Org: &lt;None&gt;\n"
                blob += "Org Rank: &lt;None&gt;\n"
            # blob += "Head Id: %d\n" % char_info.head_id
            # blob += "PVP Rating: %d\n" % char_info.pvp_rating
            # blob += "PVP Title: %s\n" % char_info.pvp_title
            blob += "Source: %s [%s]\n" % (
                self.format_source(char_info, max_cache_age),
                self.text.make_tellcmd(
                    "Force Update",
                    f"whois {char.name} {dimension} --force_update"))
            blob += "Dimension: %s\n" % char_info.dimension

            if dimension == self.bot.dimension:
                blob += "Status: %s\n" % ("<green>Active</green>" if char.
                                          char_id else "<red>Inactive</red>")

                blob += self.get_name_history(char.char_id, char.name)

            more_info = self.text.paginate_single(ChatBlob("More Info", blob),
                                                  conn)

            msg = self.text.format_char_info(char_info,
                                             online_status) + " " + more_info
        elif char.char_id:
            blob = "<notice>Note: Could not retrieve detailed info for character.</notice>\n\n"
            blob += "Name: <highlight>%s</highlight>\n" % char.name
            blob += "Character ID: <highlight>%d</highlight>\n" % char.char_id
            if online_status is not None:
                blob += "Online status: %s\n" % ("<green>Online</green>"
                                                 if online_status else
                                                 "<red>Offline</red>")
            blob += self.get_name_history(char.char_id, char.name)
            msg = ChatBlob("Basic Info for %s" % char.name, blob)
        else:
            msg = "Could not find character <highlight>%s</highlight> on RK%d." % (
                char.name, dimension)

        reply(msg)

    def get_name_history(self, char_id, name):
        blob = "\n<header2>Name History</header2>\n"
        data = self.db.query(
            "SELECT name, char_id, created_at FROM name_history WHERE char_id = ? OR name = ? ORDER BY created_at DESC",
            [char_id, name])
        for row in data:
            blob += "[%s] %s (%s)\n" % (self.util.format_date(
                row.created_at), row.name, row.char_id)
        return blob

    @timerevent(budatime="1min",
                description="Save name history",
                is_system=True)
    def save_name_history_event(self, event_type, event_data):
        if not self.name_history:
            return

        with self.db.transaction():
            t = int(time.time())
            for entry in self.name_history:
                sql = "INSERT IGNORE INTO name_history (char_id, name, created_at) VALUES (?, ?, ?)"
                self.db.exec(sql, [entry.char_id, entry.name, t])

            self.name_history = []

    def get_full_name(self, char_info):
        name = ""
        if char_info.first_name:
            name += char_info.first_name + " "

        name += "\"<highlight>" + char_info.name + "</highlight>\""

        if char_info.last_name:
            name += " " + char_info.last_name

        return name

    def format_source(self, char_info, max_cache_age):
        if char_info.cache_age == 0:
            return char_info.source
        elif char_info.cache_age < max_cache_age:
            return "%s (cache; %s old)" % (char_info.source,
                                           self.util.time_to_readable(
                                               char_info.cache_age))
        elif char_info.cache_age > max_cache_age:
            return "%s (old cache; %s old)" % (char_info.source,
                                               self.util.time_to_readable(
                                                   char_info.cache_age))

    def handle_buddy_status(self, conn, packet):
        obj = self.waiting_for_update.get(packet.char_id)
        if obj:
            self.buddy_service.remove_buddy(packet.char_id,
                                            self.BUDDY_IS_ONLINE_TYPE)
            del self.waiting_for_update[packet.char_id]
            if not self.waiting_for_update:
                self.bot.remove_packet_handler(BuddyAdded.id,
                                               self.handle_buddy_status)

            obj.callback(packet.online == 1)

    def character_name_update(self, conn, packet):
        self.name_history.append(packet)
Exemplo n.º 11
0
class UtilController:
    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.db = registry.get_instance("db")
        self.util = registry.get_instance("util")
        self.text = registry.get_instance("text")
        self.command_service = registry.get_instance("command_service")
        self.buddy_service = registry.get_instance("buddy_service")
        self.access_service = registry.get_instance("access_service")
        self.event_service = registry.get_instance("event_service")
        self.public_channel_service = registry.get_instance(
            "public_channel_service")

    @command(command="checkaccess",
             params=[Character("character")],
             access_level="moderator",
             description="Check access level for a character",
             sub_command="other")
    def checkaccess_other_cmd(self, request, char):
        if not char.char_id:
            return StandardMessage.char_not_found(char.name)

        return "Access level for <highlight>%s</highlight> is <highlight>%s</highlight> (%s)." % \
               (char.name, char.access_level["label"], self.access_service.get_single_access_level(char.char_id)["label"])

    @command(command="checkaccess",
             params=[],
             access_level="all",
             description="Check your access level")
    def checkaccess_cmd(self, request):
        char = request.sender

        return "Access level for <highlight>%s</highlight> is <highlight>%s</highlight> (%s)." % \
               (char.name, char.access_level["label"], self.access_service.get_single_access_level(char.char_id)["label"])

    @command(command="macro",
             params=[Any("command1|command2|command3...")],
             access_level="all",
             description="Execute multiple commands at once")
    def macro_cmd(self, request, commands):
        commands = commands.split("|")
        for command_str in commands:
            self.command_service.process_command(
                self.command_service.trim_command_symbol(command_str),
                request.channel, request.sender.char_id, request.reply,
                request.conn)

    @command(command="echo",
             params=[Any("message")],
             access_level="all",
             description="Echo back a message")
    def echo_cmd(self, request, message):
        return html.escape(message)

    @command(command="showcommand",
             params=[Character("character"),
                     Any("message")],
             access_level="admin",
             description="Show command output to another character")
    def showcommand_cmd(self, request, char, command_str):
        if not char.char_id:
            return StandardMessage.char_not_found(char.name)

        self.bot.send_private_message(
            char.char_id,
            f"<highlight>{request.sender.name}</highlight> is showing you output for command <highlight>{command_str}</highlight>:",
            conn=request.conn)

        self.command_service.process_command(
            self.command_service.trim_command_symbol(command_str),
            request.channel, request.sender.char_id,
            lambda msg: self.bot.send_private_message(
                char.char_id, msg, conn=request.conn), request.conn)

        return f"Command <highlight>{command_str}</highlight> output has been sent to <highlight>{char.name}</highlight>."

    @command(command="system",
             params=[NamedFlagParameters(["show_all"])],
             access_level="admin",
             description="Show system information")
    def system_cmd(self, request, flag_params):
        mass_message_queue = "None"
        if self.bot.mass_message_queue:
            mass_message_queue = str(self.bot.mass_message_queue.qsize())

        python_version = str(sys.version_info.major) + "." + str(
            sys.version_info.minor) + "." + str(
                sys.version_info.micro) + "." + sys.version_info.releaselevel
        memory_usage = self.util.format_number(
            psutil.Process(os.getpid()).memory_info().rss / 1024)
        uptime = self.util.time_to_readable(int(time.time()) -
                                            self.bot.start_time,
                                            max_levels=None)

        blob = f"Version: <highlight>Tyrbot {self.bot.version}</highlight>\n"
        blob += f"Name: <highlight><myname></highlight>\n\n"

        blob += f"OS: <highlight>{platform.system()} {platform.release()}</highlight>\n"
        blob += f"Python: <highlight>{python_version}</highlight>\n"
        blob += f"Database: <highlight>{self.db.type}</highlight>\n"
        blob += f"Memory Usage: <highlight>{memory_usage} KB</highlight>\n\n"

        blob += f"Superadmin: <highlight>{self.bot.superadmin}</highlight>\n"
        blob += f"Buddy List: <highlight>{self.buddy_service.get_buddy_list_size()}/{self.buddy_service.buddy_list_size}</highlight>\n"
        blob += f"Uptime: <highlight>{uptime}</highlight>\n"
        blob += f"Dimension: <highlight>{self.bot.dimension}</highlight>\n"
        blob += f"Mass Message Queue: <highlight>{mass_message_queue}</highlight>\n\n"

        blob += "<pagebreak><header2>Bots Connected</header2>\n"
        for _id, conn in self.bot.get_conns():
            blob += f"<highlight>{_id}</highlight> - {conn.char_name}({conn.char_id}) "
            if conn.org_id:
                blob += f"Org: {conn.get_org_name()}({conn.org_id})"
            if conn.is_main:
                blob += " <highlight>[main]</highlight>"
            blob += "\n"

            if flag_params.show_all:
                for channel_id, packet in conn.channels.items():
                    blob += f"{packet.args}\n"
                blob += "\n"

        if not flag_params.show_all:
            blob += "\n" + self.text.make_tellcmd("Show More Info",
                                                  "system --show_all") + "\n"
        else:
            blob += "\n"

            blob += "<pagebreak><header2>Event Types</header2>\n"
            for event_type in self.event_service.get_event_types():
                blob += "%s\n" % event_type
            blob += "\n"

            blob += "<pagebreak><header2>Access Levels</header2>\n"
            for access_level in self.access_service.get_access_levels():
                blob += "%s (%d)\n" % (access_level["label"],
                                       access_level["level"])

        return ChatBlob("System Info", blob)

    @command(
        command="htmldecode",
        params=[Any("command")],
        access_level="all",
        description=
        "Decode html entities from a command before passing to the bot for execution"
    )
    def htmldecode_cmd(self, request, command_str):
        self.command_service.process_command(html.unescape(command_str),
                                             request.channel,
                                             request.sender.char_id,
                                             request.reply, request.conn)
Exemplo n.º 12
0
class OrgListController:
    ORGLIST_BUDDY_TYPE = "orglist"

    DEFAULT_OFFLINE_MEMBER_DISPLAY_THRESHOLD = 200
    SHOW_ALL_OFFLINE_MEMBERS = 10000

    def __init__(self):
        self.orglist = None
        self.governing_types = DictObject({
            "Anarchism": ["Anarchist"],
            "Monarchy": ["Monarch", "Counsil", "Follower"],
            "Feudalism": ["Lord", "Knight", "Vassal", "Peasant"],
            "Republic":
            ["President", "Advisor", "Veteran", "Member", "Applicant"],
            "Faction":
            ["Director", "Board Member", "Executive", "Member", "Applicant"],
            "Department": [
                "President", "General", "Squad Commander", "Unit Commander",
                "Unit Leader", "Unit Member", "Applicant"
            ]
        })

    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.db = registry.get_instance("db")
        self.util = registry.get_instance("util")
        self.text = registry.get_instance("text")
        self.pork_service = registry.get_instance("pork_service")
        self.org_pork_service = registry.get_instance("org_pork_service")
        self.pork_service = registry.get_instance("pork_service")
        self.buddy_service: BuddyService = registry.get_instance(
            "buddy_service")
        self.character_service = registry.get_instance("character_service")

    @command(command="orglist",
             params=[Int("org_id"),
                     NamedFlagParameters(["show_all_offline"])],
             access_level="all",
             description="Show online status of characters in an org")
    def orglist_cmd(self, request, org_id, flag_params):
        self.start_orglist_lookup(
            request.reply, org_id,
            self.SHOW_ALL_OFFLINE_MEMBERS if flag_params.show_all_offline else
            self.DEFAULT_OFFLINE_MEMBER_DISPLAY_THRESHOLD)

    @command(command="orglist",
             params=[
                 Any("character|org_name|org_id"),
                 NamedFlagParameters(["show_all_offline"])
             ],
             access_level="all",
             description="Show online status of characters in an org")
    def orglist_character_cmd(self, request, search, flag_params):
        if search.isdigit():
            org_id = int(search)
        else:
            orgs = self.pork_service.find_orgs(search)
            num_orgs = len(orgs)
            if num_orgs == 0:
                char_info = self.pork_service.get_character_info(search)
                if char_info:
                    if not char_info.org_id:
                        return "<highlight>%s</highlight> does not appear to belong to an org." % search.capitalize(
                        )
                    else:
                        org_id = char_info.org_id
                else:
                    return "Could not find character or org <highlight>%s</highlight>." % search
            elif num_orgs == 1:
                org_id = orgs[0].org_id
            else:
                blob = ""
                for org in orgs:
                    blob += self.text.make_tellcmd(
                        "%s (%d)" % (org.org_name, org.org_id),
                        "orglist %d" % org.org_id) + "\n"
                return ChatBlob("Org List (%d)" % num_orgs, blob)

        self.start_orglist_lookup(
            request.reply, org_id,
            self.SHOW_ALL_OFFLINE_MEMBERS if flag_params.show_all_offline else
            self.DEFAULT_OFFLINE_MEMBER_DISPLAY_THRESHOLD)

    def start_orglist_lookup(self, reply, org_id,
                             offline_member_display_threshold):
        if self.orglist:
            elapsed = int(time.time()) - self.orglist.get("started_at")
            if elapsed > 60 * 10:
                reply(
                    "Automatically ending orglist which has been running for %s (%d remaining, %d waiting, %d finished)."
                    % (self.util.time_to_readable(elapsed),
                       len(self.orglist.org_members),
                       len(self.orglist.waiting_org_members),
                       len(self.orglist.finished_org_members)))
                self.orglist = None
                self.buddy_service.remove_all_buddies_by_type(
                    self.ORGLIST_BUDDY_TYPE)
            else:
                reply(
                    "There is an orglist already in progress. Elapsed time: " +
                    self.util.time_to_readable(elapsed))
                return

        reply("Downloading org roster for org id %d..." % org_id)

        self.orglist = self.org_pork_service.get_org_info(org_id)

        if not self.orglist:
            reply("Could not find org with ID <highlight>%d</highlight>." %
                  org_id)
            return

        self.orglist.started_at = int(time.time())
        self.orglist.org_members = list(self.orglist.org_members.values())
        self.orglist.reply = reply
        self.orglist.waiting_org_members = {}
        self.orglist.finished_org_members = {}
        self.orglist.offline_member_display_threshold = offline_member_display_threshold

        reply(
            "Checking online status for %d members of <highlight>%s</highlight>..."
            % (len(self.orglist.org_members), self.orglist.org_info.name))

        # process all name lookups
        while self.bot.iterate(1):
            pass

        self.check_for_orglist_end()

    @event(event_type=BuddyService.BUDDY_LOGON_EVENT,
           description="Detect online buddies for orglist command",
           is_system=True)
    def buddy_logon_event(self, event_type, event_data):
        if self.orglist and event_data.char_id in self.orglist.waiting_org_members:
            self.update_online_status(event_data.char_id, True)
            self.check_for_orglist_end()

    @event(event_type=BuddyService.BUDDY_LOGOFF_EVENT,
           description="Detect offline buddies for orglist command",
           is_system=True)
    def buddy_logoff_event(self, event_type, event_data):
        if self.orglist and event_data.char_id in self.orglist.waiting_org_members:
            self.update_online_status(event_data.char_id, False)
            self.check_for_orglist_end()

    def update_online_status(self, char_id, status):
        self.orglist.finished_org_members[
            char_id] = self.orglist.waiting_org_members[char_id]
        self.orglist.finished_org_members[char_id].online = status
        del self.orglist.waiting_org_members[char_id]

    def check_for_orglist_end(self):
        if self.orglist.org_members:
            self.iterate_org_members()

        if not self.orglist.waiting_org_members:
            self.orglist.reply(self.format_result())
            self.orglist = None

    def format_result(self):
        org_ranks = {}
        for rank_name in self.governing_types[
                self.orglist.org_info.governing_type]:
            org_ranks[rank_name] = DictObject({
                "online_members": [],
                "offline_members": []
            })

        org_ranks["Inactive"] = DictObject({
            "online_members": [],
            "offline_members": []
        })

        for char_id, org_member in self.orglist.finished_org_members.items():
            if org_member.online == 2:
                org_ranks["Inactive"].offline_members.append(org_member)
            elif org_member.online == 1:
                org_ranks[org_member.org_rank_name].online_members.append(
                    org_member)
            else:
                org_ranks[org_member.org_rank_name].offline_members.append(
                    org_member)

        blob = "[%s] [%s] [%s]" % (
            self.text.make_chatcmd(
                "HTML",
                f"/start http://people.anarchy-online.com/org/stats/d/5/name/{self.orglist.org_info.org_id}/"
            ),
            self.text.make_chatcmd(
                "XML",
                f"/start http://people.anarchy-online.com/org/stats/d/5/name/{self.orglist.org_info.org_id}/basicstats.xml"
            ),
            self.text.make_chatcmd(
                "JSON",
                f"/start http://people.anarchy-online.com/org/stats/d/5/name/{self.orglist.org_info.org_id}/basicstats.xml?data_type=json"
            ))

        if self.orglist.offline_member_display_threshold == self.DEFAULT_OFFLINE_MEMBER_DISPLAY_THRESHOLD:
            blob += "  " + self.text.make_tellcmd(
                "Show all offline members",
                f"orglist {self.orglist.org_info.org_id} --show_all_offline")

        blob += "\n\n"
        num_online = 0
        num_total = 0
        for rank_name, rank_info in org_ranks.items():
            rank_num_online = len(rank_info.online_members)
            rank_num_total = len(rank_info.offline_members) + rank_num_online
            blob += "<pagebreak><header2>%s (%d / %d)</header2>\n" % (
                rank_name, rank_num_online, rank_num_total)
            num_online += rank_num_online
            num_total += rank_num_total
            for org_member in sorted(rank_info.online_members,
                                     key=lambda x: x.name):
                level = org_member.level if org_member.ai_level == 0 else "%d/<green>%d</green>" % (
                    org_member.level, org_member.ai_level)
                blob += "%s (Level <highlight>%s</highlight>, %s %s <highlight>%s</highlight>)\n" % (
                    org_member.name, level, org_member.gender,
                    org_member.breed, org_member.profession)

            if rank_num_total == rank_num_online:
                pass
            elif rank_num_total < self.orglist.offline_member_display_threshold:
                blob += "<font color='#555555'>" + ", ".join(
                    map(
                        lambda x: x.name,
                        sorted(rank_info.offline_members,
                               key=lambda x: x.name))) + "</font>"
                blob += "\n"
            else:
                blob += "<font color='#555555'>Offline members omitted for brevity</font>\n"
            blob += "\n"

        return ChatBlob(
            "Orglist for '%s' (%d / %d)" %
            (self.orglist.org_info.name, num_online, num_total), blob)

    def iterate_org_members(self):
        # add org_members that we don't have online status for as buddies
        while self.orglist.org_members and self.buddy_list_has_available_slots(
        ):
            org_member = self.orglist.org_members.pop()
            char_id = org_member.char_id
            self.orglist.waiting_org_members[char_id] = org_member
            is_online = self.buddy_service.is_online(char_id)
            if is_online is None:
                if self.character_service.resolve_char_to_id(org_member.name):
                    self.buddy_service.add_buddy(char_id,
                                                 self.ORGLIST_BUDDY_TYPE)
                    self.buddy_service.remove_buddy(char_id,
                                                    self.ORGLIST_BUDDY_TYPE)
                else:
                    # character is inactive, set as offline
                    self.update_online_status(char_id, 2)
            else:
                self.update_online_status(char_id, is_online)

    def buddy_list_has_available_slots(self):
        return self.buddy_service.buddy_list_size - self.buddy_service.get_buddy_list_size(
        ) > 0
Exemplo n.º 13
0
class BanController:
    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.text = registry.get_instance("text")
        self.util = registry.get_instance("util")
        self.ban_service = registry.get_instance("ban_service")
        self.command_alias_service = registry.get_instance(
            "command_alias_service")

    def start(self):
        self.command_alias_service.add_alias("unban", "ban rem")

    @command(command="ban",
             params=[
                 Const("list", is_optional=True),
                 NamedFlagParameters(["include_expired"])
             ],
             access_level="moderator",
             description="Show the ban list")
    def ban_list_cmd(self, request, _, flag_params):
        t = int(time.time())
        data = self.ban_service.get_ban_list(flag_params.include_expired)
        blob = ""
        for row in data:
            end_time = "never" if row.finished_at == -1 else self.util.format_datetime(
                row.finished_at)
            time_left = "" if row.finished_at == -1 else " (%s left)" % self.util.time_to_readable(
                row.finished_at - t)
            added_time = self.util.format_datetime(row.created_at)
            name = row.name if row.name else ("Unknown(%d)" % row.char_id)

            blob += f"<pagebreak>Name: <highlight>{name}</highlight>\n"
            blob += f"Added: <highlight>{added_time}</highlight>\n"
            blob += f"By: <highlight>{row.sender_name}</highlight>\n"
            blob += f"Ends: <highlight>{end_time}</highlight>{time_left}\n"
            blob += f"Reason: <highlight>{row.reason}</highlight>\n\n"

        return ChatBlob(f"Ban List ({len(data)})", blob)

    @command(command="ban",
             params=[Options(["rem", "remove"]),
                     Character("character")],
             access_level="moderator",
             description="Remove a character from the ban list")
    def ban_remove_cmd(self, request, _, char):
        if not char.char_id:
            return StandardMessage.char_not_found(char.name)

        if not self.ban_service.get_ban(char.char_id):
            return f"<highlight>{char.name}</highlight> is not banned."

        self.ban_service.remove_ban(char.char_id, request.sender.char_id)
        return f"<highlight>{char.name}</highlight> has been removed from the ban list."

    @command(command="ban",
             params=[
                 Const("add", is_optional=True),
                 Character("character"),
                 Time("duration", is_optional=True),
                 Any("reason", is_optional=True)
             ],
             access_level="moderator",
             description="Add a character to the ban list")
    def ban_add_cmd(self, request, _, char, duration, reason):
        if not char.char_id:
            return StandardMessage.char_not_found(char.name)

        if self.ban_service.get_ban(char.char_id):
            return f"<highlight>{char.name}</highlight> is already banned."

        if reason and len(reason) > 255:
            return "Ban reason cannot be more than 255 characters."

        self.ban_service.add_ban(char.char_id, request.sender.char_id,
                                 duration, reason)
        msg = f"<highlight>{char.name}</highlight> has been added to the ban list"
        if duration:
            msg += " for "
            msg += self.util.time_to_readable(duration)
        if reason:
            msg += " with reason: "
            msg += reason
        msg += "."
        return msg
Exemplo n.º 14
0
class HelpController:
    def inject(self, registry):
        self.bot = registry.get_instance("bot")
        self.text = registry.get_instance("text")
        self.db = registry.get_instance("db")
        self.access_service = registry.get_instance("access_service")
        self.command_service = registry.get_instance("command_service")
        self.command_alias_service = registry.get_instance(
            "command_alias_service")

    def start(self):
        self.command_alias_service.add_alias("version", "about")

    @command(command="about",
             params=[],
             access_level="all",
             description="Show information about the development of this bot")
    def about_cmd(self, request):
        with open(os.path.dirname(os.path.realpath(__file__)) + os.sep +
                  "about.txt",
                  mode="r",
                  encoding="UTF-8") as f:
            return ChatBlob(f"About Tyrbot {self.bot.version}", f.read())

    @command(command="help",
             params=[],
             access_level="all",
             description="Show a list of commands to get help with")
    def help_list_cmd(self, request):
        data = self.db.query(
            "SELECT command, module, access_level FROM command_config "
            "WHERE enabled = 1 "
            "ORDER BY module ASC, command ASC")
        blob = ""
        current_group = ""
        current_module = ""
        current_command = ""
        access_level = self.access_service.get_access_level(
            request.sender.char_id)
        for row in data:
            if access_level[
                    "level"] > self.access_service.get_access_level_by_label(
                        row.access_level)["level"]:
                continue

            parts = row.module.split(".")
            group = parts[0]
            module = parts[1]
            if group != current_group:
                current_group = group
                blob += "\n\n<header2>" + current_group + "</header2>"

            if module != current_module:
                current_module = module
                blob += "\n" + module + ":"

            if row.command != current_command:
                current_command = row.command
                blob += " " + self.text.make_tellcmd(row.command,
                                                     "help " + row.command)

        return ChatBlob("Help (main)", blob)

    @command(
        command="help",
        params=[Any("search"),
                NamedFlagParameters(["show_regex"])],
        access_level="all",
        description="Show help for a specific command",
        extended_description=
        "Search param can be either a command name or a module name (eg. 'standard.online')"
    )
    def help_detail_cmd(self, request, help_topic, named_params):
        help_topic = help_topic.lower()

        # check for alias
        alias = self.command_alias_service.check_for_alias(help_topic)
        if alias:
            help_topic = alias

        # check if help topic matches a command
        data = self.db.query(
            "SELECT command, sub_command, access_level FROM command_config "
            "WHERE command = ? AND channel = ? AND enabled = 1",
            [help_topic, request.channel])

        help_text = self.command_service.format_help_text(
            data, request.sender.char_id, named_params.show_regex)
        if help_text:
            return self.command_service.format_help_text_blob(
                help_topic, help_text)

        # check if help topic matches a module
        data = self.db.query(
            "SELECT command, sub_command, access_level FROM command_config "
            "WHERE module = ? AND channel = ? AND enabled = 1",
            [help_topic, request.channel])

        help_text = self.command_service.format_help_text(
            data, request.sender.char_id, named_params.show_regex)
        if help_text:
            return self.command_service.format_help_text_blob(
                help_topic, help_text)

        return f"Could not find help on <highlight>{help_topic}</highlight>."
Exemplo n.º 15
0
class ConfigController:
    def inject(self, registry):
        self.db: DB = registry.get_instance("db")
        self.text: Text = registry.get_instance("text")
        self.util = registry.get_instance("util")
        self.command_service = registry.get_instance("command_service")
        self.event_service = registry.get_instance("event_service")
        self.setting_service = registry.get_instance("setting_service")
        self.config_events_controller = registry.get_instance(
            "config_events_controller")

    @command(command="config",
             params=[],
             access_level="admin",
             description="Show configuration options for the bot")
    def config_list_cmd(self, request):
        sql = """SELECT
                module,
                SUM(CASE WHEN enabled = 1 THEN 1 ELSE 0 END) count_enabled,
                SUM(CASE WHEN enabled = 0 THEN 1 ELSE 0 END) count_disabled
            FROM
                (SELECT module, enabled FROM command_config
                UNION
                SELECT module, enabled FROM event_config WHERE is_hidden = 0
                UNION
                SELECT module, 2 FROM setting) t
            GROUP BY
                module
            ORDER BY
                module ASC"""

        data = self.db.query(sql)
        count = len(data)
        blob = ""
        current_group = ""
        for row in data:
            parts = row.module.split(".")
            group = parts[0]
            module = parts[1]
            if group != current_group:
                current_group = group
                blob += "\n<header2>" + current_group + "</header2>\n"

            blob += self.text.make_tellcmd(module,
                                           "config mod " + row.module) + " "
            if row.count_enabled > 0 and row.count_disabled > 0:
                blob += "Partial"
            else:
                blob += "<green>Enabled</green>" if row.count_disabled == 0 else "<red>Disabled</red>"
            blob += "\n"

        return ChatBlob(f"Config ({count})", blob)

    @command(command="config",
             params=[
                 Options(["mod", "module"]),
                 Any("module_name"),
                 NamedFlagParameters(["include_system_events"])
             ],
             access_level="admin",
             description="Show configuration options for a specific module")
    def config_module_list_cmd(self, request, _, module, named_params):
        module = module.lower()

        blob = ""

        data = self.db.query(
            "SELECT name FROM setting WHERE module = ? ORDER BY name ASC",
            [module])
        if data:
            blob += "<header2>Settings</header2>\n"
            groups = self.util.group_by(data, lambda x: x.name.split("_")[0])
            for group, settings in groups.items():
                for row in settings:
                    setting = self.setting_service.get(row.name)
                    blob += "%s - %s: %s (%s)\n" % (
                        setting.name, setting.get_description(),
                        setting.get_display_value(),
                        self.text.make_tellcmd("change",
                                               "config setting " + row.name))
                blob += "\n"

        data = self.db.query(
            "SELECT DISTINCT command, sub_command FROM command_config WHERE module = ? ORDER BY command ASC",
            [module])
        if data:
            blob += "<header2>Commands</header2>\n"
            for row in data:
                command_key = self.command_service.get_command_key(
                    row.command, row.sub_command)
                blob += self.text.make_tellcmd(
                    command_key, "config cmd " + command_key) + "\n"

        blob += self.format_events(self.get_events(module, False), "Events")

        system_events = self.get_events(module, True)
        if named_params.include_system_events:
            blob += self.format_events(system_events, "System Events")
        elif system_events:
            blob += "\n" + self.text.make_tellcmd(
                "Show system events",
                f"config mod {module} --include_system_events")

        if blob:
            return ChatBlob(f"Module ({module})", blob)
        else:
            return "Could not find module <highlight>{module}</highlight>."

    @command(command="config",
             params=[Const("settinglist")],
             access_level="admin",
             description="List all settings")
    def config_settinglist_cmd(self, request, _):
        blob = ""

        data = self.db.query("SELECT * FROM setting ORDER BY module, name ASC")
        count = len(data)
        if data:
            blob += "<header2>Settings</header2>\n"
            current_module = ""
            for row in data:
                if row.module != current_module:
                    current_module = row.module
                    blob += "\n<pagebreak><header2>%s</header2>\n" % row.module

                setting = self.setting_service.get(row.name)
                blob += "%s - %s: %s (%s)\n" % (
                    setting.name, setting.get_description(),
                    setting.get_display_value(),
                    self.text.make_tellcmd("change",
                                           "config setting " + row.name))

        return ChatBlob(f"Settings ({count})", blob)

    @command(command="config",
             params=[
                 Const("setting"),
                 Any("setting_name"),
                 Options(["set", "clear"]),
                 Any("new_value", is_optional=True)
             ],
             access_level="admin",
             description="Change a setting value")
    def config_setting_update_cmd(self, request, _, setting_name, op,
                                  new_value):
        setting_name = setting_name.lower()

        if op == "clear":
            new_value = ""
        elif not new_value:
            return "Error! New value required to update setting."

        setting = self.setting_service.get(setting_name)

        if setting:
            setting.set_value(new_value)
            if op == "clear":
                return f"Setting <highlight>{setting_name}</highlight> has been cleared."
            else:
                return f"Setting <highlight>{setting_name}</highlight> has been set to {setting.get_display_value()}."
        else:
            return f"Could not find setting <highlight>{setting_name}</highlight>."

    @command(command="config",
             params=[Const("setting"), Any("setting_name")],
             access_level="admin",
             description="Show configuration options for a setting")
    def config_setting_show_cmd(self, request, _, setting_name):
        setting_name = setting_name.lower()

        blob = ""

        setting = self.setting_service.get(setting_name)

        if setting:
            blob += f"Current Value: <highlight>{str(setting.get_display_value())}</highlight>\n"
            blob += f"Description: <highlight>{setting.get_description()}</highlight>\n\n"
            if setting.get_extended_description():
                blob += setting.get_extended_description() + "\n\n"
            blob += setting.get_display()
            return ChatBlob(f"Setting ({setting_name})", blob)
        else:
            return f"Could not find setting <highlight>{setting_name}</highlight>."

    def get_events(self, module, is_system):
        return self.db.query(
            "SELECT event_type, event_sub_type, handler, description, enabled, is_hidden "
            f"FROM event_config WHERE module = ? AND is_hidden = ? "
            "ORDER BY is_hidden, event_type, handler ASC",
            [module, 1 if is_system else 0])

    def format_events(self, data, title):
        blob = ""
        if data:
            blob += f"\n<header2>{title}</header2>\n"
            for row in data:
                event_type_key = self.event_service.get_event_type_key(
                    row.event_type, row.event_sub_type)
                enabled = "<green>Enabled</green>" if row.enabled == 1 else "<red>Disabled</red>"
                blob += "%s - %s [%s]" % (
                    self.config_events_controller.format_event_type(row),
                    row.description, enabled)
                blob += " " + self.text.make_tellcmd(
                    "On", "config event %s %s enable" %
                    (event_type_key, row.handler))
                blob += " " + self.text.make_tellcmd(
                    "Off", "config event %s %s disable" %
                    (event_type_key, row.handler))
                if row.event_type == "timer":
                    blob += " " + self.text.make_tellcmd(
                        "Run Now", "config event %s %s run" %
                        (event_type_key, row.handler))
                blob += "\n"
        return blob