def test_named_parameters(self): param = NamedParameters(["test1", "test2", "test3"]) self.assertEqual({ 'test1': '1', 'test2': '2', 'test3': '3' }, self.param_test(param, "--test1=1 --test2=2 --test3=3")) self.assertEqual({ 'test1': '1', 'test2': '2', 'test3': '3' }, self.param_test(param, "--test3=3 --test2=2 --test1=1")) self.assertEqual({ 'test1': '', 'test2': '2', 'test3': '' }, self.param_test(param, "--test2=2")) self.assertEqual( { 'test1': 'one and two and three', 'test2': '', 'test3': '' }, self.param_test(param, "--test1=one and two and three")) self.assertEqual( { 'test1': 'one and two', 'test2': '', 'test3': 'three and four' }, self.param_test(param, "--test1=one and two --test3=three and four")) self.assertIsNone(self.param_test(param, ""))
class CommandListController: 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.ts: TranslationService = registry.get_instance("translation_service") self.getresp = self.ts.get_response @command(command="config", params=[Const("cmdlist"), NamedParameters(["access_level"])], access_level="admin", description="List all commands") def config_cmdlist_cmd(self, request, _, named_params): sql = "SELECT access_level, channel, enabled, command, module, sub_command FROM command_config" params = [] if named_params.access_level: sql += " WHERE access_level = ?" params.append(named_params.access_level) sql += " ORDER BY module, command, sub_command, channel" data = self.db.query(sql, params) blob = "" current_module = "" current_command_key = "" count = 0 temp_rows = [] for row in data: if current_module != row.module: if temp_rows: blob += self.display_row_data(temp_rows) temp_rows = [] blob += "\n<pagebreak><header2>%s</header2>\n" % row.module current_module = row.module current_command_key = "" command_key = self.command_service.get_command_key(row.command, row.sub_command) if current_command_key != command_key: if temp_rows: blob += self.display_row_data(temp_rows) temp_rows = [] count += 1 blob += "%s - " % (self.text.make_tellcmd(command_key, "config cmd " + command_key)) current_command_key = command_key temp_rows.append(row) if temp_rows: blob += self.display_row_data(temp_rows) return ChatBlob(self.getresp("module/config", "cmdlist_commands", {"amount": count}), blob) def display_row_data(self, rows): return "[%s %s]\n" % (self.get_enabled_str(rows), self.get_access_levels_str(rows)) def get_access_levels_str(self, rows): access_levels = list(map(lambda x: x.access_level, rows)) if all(x == access_levels[0] for x in access_levels): return access_levels[0] else: return ",".join(access_levels) def get_enabled_str(self, rows): enabled = list(map(lambda x: x.enabled, rows)) blob = "" if all(x == enabled[0] for x in enabled): blob += self.format_enabled(enabled[0]) else: for x in enabled: blob += self.format_enabled(x) return blob def format_enabled(self, enabled): return "<green>E</green>" if enabled else "<red>D</red>"
class TowerMessagesController: MESSAGE_SOURCE = "tower_attacks" TOWER_ATTACK_EVENT = "tower_attack" TOWER_VICTORY_EVENT = "tower_victory" TOWER_BATTLE_OUTCOME_ID = 42949672962 ALL_TOWERS_ID = 42949672960 ATTACK_1 = [ 506, 12753364 ] # The %s organization %s just entered a state of war! %s attacked the %s organization %s's tower in %s at location (%d,%d). ATTACK_2 = re.compile( r"^(.+) just attacked the (clan|neutral|omni) organization (.+)'s tower in (.+) at location \((\d+), (\d+)\).\n$" ) VICTORY_1 = re.compile( r"^Notum Wars Update: Victory to the (Clan|Neutral|Omni)s!!!$") VICTORY_2 = re.compile( r"^The (Clan|Neutral|Omni) organization (.+) attacked the (Clan|Neutral|Omni) (.+) at their base in (.+). The attackers won!!$" ) VICTORY_3 = [ 506, 147506468 ] # 'Notum Wars Update: The %s organization %s lost their base in %s.' def __init__(self): self.logger = Logger(__name__) def inject(self, registry): self.bot = registry.get_instance("bot") self.db = registry.get_instance("db") self.text = registry.get_instance("text") self.util = registry.get_instance("util") self.event_service = registry.get_instance("event_service") self.command_alias_service = registry.get_instance( "command_alias_service") self.pork_service = registry.get_instance("pork_service") self.message_hub_service = registry.get_instance("message_hub_service") self.playfield_controller: PlayfieldController = registry.get_instance( "playfield_controller") def pre_start(self): self.message_hub_service.register_message_source(self.MESSAGE_SOURCE) self.event_service.register_event_type(self.TOWER_ATTACK_EVENT) self.event_service.register_event_type(self.TOWER_VICTORY_EVENT) self.bot.register_packet_handler( server_packets.PublicChannelMessage.id, self.handle_public_channel_message) def start(self): self.db.exec( "CREATE TABLE IF NOT EXISTS tower_attacker (id INT PRIMARY KEY AUTO_INCREMENT, att_org_name VARCHAR(50) NOT NULL, att_faction VARCHAR(10) NOT NULL, " "att_char_id INT, att_char_name VARCHAR(20) NOT NULL, att_level INT NOT NULL, att_ai_level INT NOT NULL, att_profession VARCHAR(15) NOT NULL, " "x_coord INT NOT NULL, y_coord INT NOT NULL, is_victory SMALLINT NOT NULL, " "tower_battle_id INT NOT NULL, created_at INT NOT NULL)") self.db.exec( "CREATE TABLE IF NOT EXISTS tower_battle (id INT PRIMARY KEY AUTO_INCREMENT, playfield_id INT NOT NULL, site_number INT NOT NULL, " "def_org_name VARCHAR(50) NOT NULL, def_faction VARCHAR(10) NOT NULL, is_finished INT NOT NULL, battle_type VARCHAR(20) NOT NULL, last_updated INT NOT NULL)" ) self.command_alias_service.add_alias("victory", "attacks") self.command_alias_service.add_alias("battles", "attacks") @command(command="attacks", params=[Const("battle"), Int("battle_id")], access_level="all", description="Show battle info for a specific battle") def attacks_battle_cmd(self, request, _, battle_id): battle = self.get_battle(battle_id) if not battle: return "Could not find battle with ID <highlight>%d</highlight>." % battle_id blob = self.check_for_all_towers_channel() + self.get_battle_blob( battle) return ChatBlob("Battle Info %d" % battle_id, blob) @command(command="attacks", params=[ Any("playfield", is_optional=True, allowed_chars="[a-z0-9 ]"), Int("site_number", is_optional=True), NamedParameters([ "defender_org", "attacker_org", "attacker", "victory", "page" ]) ], access_level="all", description="Show recent tower attacks and victories", extended_description= "Victory param can be 'true', 'false', or 'all' (default)") def attacks_cmd(self, request, playfield_name, site_number, named_params): playfield = None if playfield_name: playfield = self.playfield_controller.get_playfield_by_name_or_id( playfield_name) if not playfield: return f"Could not find playfield <highlight>{playfield_name}</highlight>." page_number = int(named_params.page or "1") victory = "all" if named_params.victory == "" else named_params.victory command_str = "attacks" if playfield_name: command_str += " " + playfield_name if site_number: command_str += " " + str(site_number) page_size = 10 offset = (page_number - 1) * page_size sql = "SELECT DISTINCT b.*, p.long_name, p.short_name FROM tower_battle b " \ "LEFT JOIN playfields p ON b.playfield_id = p.id " \ "LEFT JOIN tower_attacker a ON b.id = a.tower_battle_id " \ "WHERE 1=1" params = [] if playfield: sql += " AND b.playfield_id = ?" params.append(playfield.id) if site_number: sql += " AND b.site_number = ?" params.append(site_number) if victory != "all": command_str += f" --victory={victory}" sql += " AND b.is_finished = ?" if victory == "true": params.append(1) elif victory == "false": params.append(0) else: return "Named param <highlight>--victory</highlight> must be one of: 'true', 'false', or 'all' (default)." if named_params.defender_org: defender_org = named_params.defender_org command_str += f" --defender_org={defender_org}" sql += f" AND b.def_org_name <EXTENDED_LIKE={len(params)}> ?" params.append(defender_org) if named_params.attacker_org: attacker_org = named_params.attacker_org command_str += f" --attacker_org={attacker_org}" sql += f" AND a.att_org_name <EXTENDED_LIKE={len(params)}> ?" params.append(attacker_org) if named_params.attacker: attacker = named_params.attacker command_str += f" --attacker={attacker}" sql += f" AND a.att_char_name LIKE ? OR a.att_char_id = ?" params.append(attacker) params.append(attacker) sql += " ORDER BY b.last_updated DESC LIMIT ?, ?" params.append(offset) params.append(page_size) data = self.db.query(sql, params, extended_like=True) t = int(time.time()) blob = self.check_for_all_towers_channel() blob += self.text.get_paging_links(command_str, page_number, page_size == len(data)) blob += "\n\n" for row in data: blob += "\n<pagebreak>" blob += self.format_battle_info(row, t) blob += "<header2>Attackers:</header2>\n" sql2 = """SELECT a.*, COALESCE(a.att_level, 0) AS att_level, COALESCE(a.att_ai_level, 0) AS att_ai_level FROM tower_attacker a WHERE a.tower_battle_id = ? ORDER BY created_at DESC""" data2 = self.db.query(sql2, [row.id]) for row2 in data2: blob += "<tab>" + self.format_attacker(row2) if row2.is_victory: blob += " - <notice>Winner!</notice>" blob += "\n" if not data2: blob += "<tab>Unknown attacker\n" title = "Tower Attacks" if playfield: title += f" ({playfield.long_name}" if site_number: title += " " + str(site_number) title += ")" return ChatBlob(title, blob) @event(event_type="connect", description="Check if All Towers channel is available", is_system=True) def handle_connect_event(self, event_type, event_data): conn = self.bot.get_primary_conn() if conn.org_id and self.ALL_TOWERS_ID not in conn.channels: self.logger.warning( "The primary bot is a member of an org but does not have access to 'All Towers' channel and therefore will not be able to record tower attacks" ) def handle_public_channel_message( self, conn: Conn, packet: server_packets.PublicChannelMessage): # only listen to tower packets from first bot, to avoid triggering multiple times if conn != self.bot.get_primary_conn(): return if packet.channel_id == self.TOWER_BATTLE_OUTCOME_ID: victory = self.get_victory_event(packet) if victory: self.fire_victory_event(victory) elif packet.channel_id == self.ALL_TOWERS_ID: attack = self.get_attack_event(packet) if attack: self.fire_attack_event(attack) def fire_victory_event(self, obj): # self.logger.debug("tower victory packet: %s" % str(packet)) # lookup playfield playfield_name = obj.location.playfield.long_name obj.location.playfield = self.playfield_controller.get_playfield_by_name( playfield_name) or DictObject() obj.location.playfield.long_name = playfield_name t = int(time.time()) is_victory = 1 is_finished = 1 if obj.type == "attack": # get battle id and site_number last_updated = t - (6 * 3600) row = self.get_last_attack(obj.winner.faction, obj.winner.org_name, obj.loser.faction, obj.loser.org_name, obj.location.playfield.id, last_updated) if row: obj.battle_id = row.battle_id obj.location.site_number = row.site_number self.db.exec( "UPDATE tower_attacker SET is_victory = ? WHERE id = ?", [is_victory, row.attack_id]) self.db.exec( "UPDATE tower_battle SET is_finished = ?, last_updated = ? WHERE id = ?", [is_finished, t, row.battle_id]) else: obj.location.site_number = 0 self.db.exec( "INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)", [ obj.location.playfield.id, obj.location.site_number, obj.loser.org_name, obj.loser.faction, is_finished, obj.type, t ]) obj.battle_id = self.db.last_insert_id() attacker = obj.winner or {} self.db.exec( "INSERT INTO tower_attacker (att_org_name, att_faction, att_char_id, att_char_name, att_level, att_ai_level, att_profession, " "x_coord, y_coord, is_victory, tower_battle_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ attacker.get("org_name", ""), attacker.get("faction", ""), attacker.get("char_id", 0), attacker.get("name", ""), attacker.get("level", 0), attacker.get("ai_level", 0), attacker.get("profession", ""), 0, 0, is_victory, obj.battle_id, t ]) elif obj.type == "terminated": obj.location.site_number = 0 self.db.exec( "INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)", [ obj.location.playfield.id, obj.location.site_number, obj.loser.org_name, obj.loser.faction, is_finished, obj.type, t ]) obj.battle_id = self.db.last_insert_id() else: raise Exception("Unknown victory event type: '%s'" % obj.type) self.event_service.fire_event(self.TOWER_VICTORY_EVENT, obj) def fire_attack_event(self, obj): # self.logger.debug("tower attack packet: %s" % str(packet)) # lookup playfield playfield_name = obj.location.playfield.long_name obj.location.playfield = self.playfield_controller.get_playfield_by_name( playfield_name) or DictObject() obj.location.playfield.long_name = playfield_name # lookup attacker name = obj.attacker.name faction = obj.attacker.faction org_name = obj.attacker.org_name char_info = self.pork_service.get_character_info(name) obj.attacker = char_info or DictObject() obj.attacker.name = name obj.attacker.faction = faction or obj.attacker.get( "faction", "Unknown") obj.attacker.org_name = org_name obj.location.site_number = self.find_closest_site_number( obj.location.playfield.id, obj.location.x_coord, obj.location.y_coord) attacker = obj.attacker or {} defender = obj.defender location = obj.location t = int(time.time()) battle = self.find_or_create_battle(obj.location.playfield.id, obj.location.site_number, defender.org_name, defender.faction, "attack", t) obj.battle_id = battle.id self.db.exec( "INSERT INTO tower_attacker (att_org_name, att_faction, att_char_id, att_char_name, att_level, att_ai_level, att_profession, " "x_coord, y_coord, is_victory, tower_battle_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ attacker.get("org_name", ""), attacker.get("faction", ""), attacker.get("char_id", 0), attacker.get("name", ""), attacker.get("level", 0), attacker.get("ai_level", 0), attacker.get("profession", ""), location.x_coord, location.y_coord, 0, battle.id, t ]) attacker_id = self.db.last_insert_id() attacker_row = self.db.query_single( "SELECT * FROM tower_attacker WHERE id = ?", [attacker_id]) more_info = self.text.paginate_single( ChatBlob( "More Info", self.text.make_tellcmd("More Info", f"attacks battle {battle.id}")), self.bot.get_primary_conn()) msg = "%s attacked %s [%s] at %s %s %s" % ( self.format_attacker(attacker_row), defender.org_name, self.text.get_formatted_faction(defender.faction), location.playfield.get("short_name", location.playfield.get("long_name")), obj.location.site_number or "?", more_info) self.message_hub_service.send_message(self.MESSAGE_SOURCE, None, None, msg) self.event_service.fire_event(self.TOWER_ATTACK_EVENT, obj) def get_attack_event(self, packet: server_packets.PublicChannelMessage): t = int(time.time()) if packet.extended_message and [ packet.extended_message.category_id, packet.extended_message.instance_id ] == self.ATTACK_1: params = packet.extended_message.params return DictObject({ "timestamp": t, "attacker": { "name": params[2], "faction": params[0].capitalize(), "org_name": params[1] }, "defender": { "faction": params[3].capitalize(), "org_name": params[4] }, "location": { "playfield": { "long_name": params[5] }, "x_coord": params[6], "y_coord": params[7] } }) else: match = self.ATTACK_2.match(packet.message) if match: return DictObject({ "timestamp": t, "attacker": { "name": match.group(1), "faction": "", "org_name": "" }, "defender": { "faction": match.group(2).capitalize(), "org_name": match.group(3) }, "location": { "playfield": { "long_name": match.group(4) }, "x_coord": match.group(5), "y_coord": match.group(6) } }) # Unknown attack self.logger.warning("Unknown tower attack: " + str(packet)) return None def get_victory_event(self, packet: server_packets.PublicChannelMessage): match = self.VICTORY_1.match(packet.message) if match: return None t = int(time.time()) match = self.VICTORY_2.match(packet.message) if match: return DictObject({ "type": "attack", "timestamp": t, "winner": { "faction": match.group(1).capitalize(), "org_name": match.group(2) }, "loser": { "faction": match.group(3).capitalize(), "org_name": match.group(4) }, "location": { "playfield": { "long_name": match.group(5) } } }) if packet.extended_message and [ packet.extended_message.category_id, packet.extended_message.instance_id ] == self.VICTORY_3: params = packet.extended_message.params return DictObject({ "type": "terminated", "timestamp": t, "winner": { "faction": params[0].capitalize(), "org_name": params[1] }, "loser": { "faction": params[0].capitalize(), "org_name": params[1] }, "location": { "playfield": { "long_name": params[2] } } }) # Unknown victory self.logger.warning("Unknown tower victory: " + str(packet)) return None def format_attacker(self, row): level = ("%d/<green>%d</green>" % (row.att_level, row.att_ai_level) ) if row.att_ai_level > 0 else "%d" % row.att_level org = row.att_org_name + " " if row.att_org_name else "" return "%s (%s %s) %s[%s]" % ( row.att_char_name or "Unknown attacker", level, row.att_profession or "Unknown", org, self.text.get_formatted_faction( row.att_faction)) def find_closest_site_number(self, playfield_id, x_coord, y_coord): sql = "SELECT site_number FROM tower_site_bounds " \ "WHERE playfield_id = ? AND x_coord1 <= ? AND x_coord2 >= ? AND y_coord1 >= ? AND y_coord2 <= ? " \ "LIMIT 1" row = self.db.query_single( sql, [playfield_id, x_coord, x_coord, y_coord, y_coord]) if row: return row.site_number sql = """ SELECT site_number, ((x_distance * x_distance) + (y_distance * y_distance)) radius FROM (SELECT playfield_id, site_number, min_ql, max_ql, x_coord, y_coord, site_name, (x_coord - ?) as x_distance, (y_coord - ?) as y_distance FROM tower_site WHERE playfield_id = ?) t ORDER BY radius ASC LIMIT 1""" row = self.db.query_single(sql, [x_coord, y_coord, playfield_id]) if row: return row.site_number else: return 0 def find_or_create_battle(self, playfield_id, site_number, org_name, faction, battle_type, t): last_updated = t - (8 * 3600) is_finished = 0 sql = """ SELECT id FROM tower_battle WHERE playfield_id = ? AND site_number = ? AND is_finished = ? AND def_org_name = ? AND def_faction = ? AND last_updated >= ? """ battle = self.db.query_single(sql, [ playfield_id, site_number, is_finished, org_name, faction, last_updated ]) if battle: self.db.exec( "UPDATE tower_battle SET last_updated = ? WHERE id = ?", [t, battle.id]) battle_id = battle.id else: self.db.exec( "INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)", [ playfield_id, site_number, org_name, faction, is_finished, battle_type, t ]) battle_id = self.db.last_insert_id() return self.get_battle(battle_id) def get_last_attack(self, att_faction, att_org_name, def_faction, def_org_name, playfield_id, last_updated): is_finished = 0 sql = """ SELECT b.id AS battle_id, a.id AS attack_id, b.site_number FROM tower_battle b JOIN tower_attacker a ON a.tower_battle_id = b.id WHERE a.att_faction = ? AND a.att_org_name = ? AND b.def_faction = ? AND b.def_org_name = ? AND b.playfield_id = ? AND b.is_finished = ? AND b.last_updated >= ? ORDER BY last_updated DESC LIMIT 1""" return self.db.query_single(sql, [ att_faction, att_org_name, def_faction, def_org_name, playfield_id, is_finished, last_updated ]) def format_battle_info(self, row, t, verbose=False): blob = "" defeated = " - <notice>Defeated!</notice>" if row.is_finished else "" blob += f"Site: %s " % self.text.make_tellcmd( f"{row.short_name} {row.site_number}", f"lc {row.short_name} {row.site_number}") if not verbose: blob += self.text.make_tellcmd("More Info", "attacks battle %d" % row.id) blob += "\n" if verbose: if row.site_number: blob += f"Long name: <highlight>{row.site_name}, {row.long_name}</highlight>\n" blob += f"Level range: <highlight>{row.min_ql}-{row.max_ql}</highlight>\n" blob += "Coordinates: %s\n" % self.text.make_chatcmd( f"{row.x_coord}x{row.y_coord}", f"/waypoint {row.x_coord} {row.y_coord} {row.playfield_id}" ) else: blob += f"Long name: Unknown\n" blob += f"Level range: Unknown\n" blob += "Coordinates: Unknown\n" blob += f"Defender: %s [%s]%s\n" % (row.def_org_name, self.text.get_formatted_faction( row.def_faction), defeated) blob += "Last Activity: %s\n" % self.format_timestamp( row.last_updated, t) return blob def format_timestamp(self, t, current_t): return "<highlight>%s</highlight> (%s ago)" % ( self.util.format_datetime(t), self.util.time_to_readable(current_t - t)) def check_for_all_towers_channel(self): if self.ALL_TOWERS_ID not in self.bot.get_primary_conn().channels: return "Notice: The primary bot must belong to an org and be promoted to a rank that is high enough to have the All Towers channel (e.g., Squad Commander) in order for this command to work correctly.\n\n" else: return "" def get_battle_blob(self, battle): t = int(time.time()) attackers = self.db.query( "SELECT * FROM tower_attacker WHERE tower_battle_id = ? ORDER BY created_at DESC", [battle.id]) first_activity = attackers[-1].created_at if len( attackers) > 0 else battle.last_updated blob = "" blob += self.format_battle_info(battle, t, verbose=True) blob += "Duration: <highlight>%s</highlight>\n\n" % self.util.time_to_readable( battle.last_updated - first_activity) blob += "<header2>Attackers:</header2>\n" for row in attackers: blob += "<tab>" + self.format_attacker(row) blob += " " + self.format_timestamp(row.created_at, t) if row.is_victory: blob += " - <notice>Winner!</notice>" blob += "\n" return blob def get_battle(self, battle_id): return self.db.query_single( "SELECT b.*, p.short_name, p.long_name, t.site_name, t.x_coord, t.y_coord, t.min_ql, t.max_ql " "FROM tower_battle b " "LEFT JOIN playfields p ON p.id = b.playfield_id " "LEFT JOIN tower_site t ON b.playfield_id = t.playfield_id AND b.site_number = t.site_number " "WHERE b.id = ?", [battle_id])
class UsageController: def __init__(self): self.logger = Logger(__name__) def inject(self, registry): self.bot: Tyrbot = registry.get_instance("bot") self.db = registry.get_instance("db") self.text = registry.get_instance("text") self.util = registry.get_instance("util") self.character_service = registry.get_instance("character_service") @command(command="usage", params=[Time("max_time", is_optional=True), NamedParameters(["cmd", "page"])], access_level="moderator", description="Show command usage summary") def usage_command(self, request, max_time, named_params): page_size = 20 page_number = int(named_params.page or "1") params = [] sql = "SELECT command, count(1) AS count FROM command_usage WHERE 1=1" if max_time: sql += " AND created_at > ?" t = int(time.time()) params.append(t - max_time) if named_params.cmd: sql += " AND command LIKE ?" params.append(named_params.cmd) sql += " GROUP BY command ORDER BY count(1) DESC LIMIT ?, ?" offset, limit = self.util.get_offset_limit(page_size, page_number) params.append(offset) params.append(limit) data = self.db.query(sql, params) rows = [] for row in data: rows.append([f"<highlight>{row.count}</highlight>", row.command]) display_table = self.text.pad_table(rows, " ", pad_right=False) cmd_string = "usage" if max_time: cmd_string += f" {max_time}" if named_params.cmd: cmd_string += f" --cmd={named_params.cmd}" blob = "" blob += self.text.get_paging_links(cmd_string, page_number, len(data) == page_size) + "\n\n" for cols in display_table: blob += " ".join(cols) + "\n" if max_time: time_str = self.util.time_to_readable(max_time) title = f"Command Usage for {time_str}" else: title = "Command Usage" return ChatBlob(title, blob) @command(command="usage", params=[Const("history"), NamedParameters(["cmd", "char", "page"])], access_level="moderator", description="Show command usage history") def usage_history_command(self, request, _, named_params): page_size = 20 page_number = int(named_params.page or "1") params = [] sql = "SELECT channel, command, created_at, handler, p.name, c.char_id FROM command_usage c " \ "LEFT JOIN player p ON c.char_id = p.char_id " \ "WHERE 1=1" if named_params.cmd: sql += " AND (command LIKE ? OR handler LIKE ?)" params.append(named_params.cmd) params.append(named_params.cmd) if named_params.char: char_id = self.character_service.resolve_char_to_id(named_params.char) if not char_id: return StandardMessage.char_not_found(named_params.char) sql += " AND c.char_id = ?" params.append(char_id) sql += " ORDER BY created_at DESC LIMIT ?, ?" offset, limit = self.util.get_offset_limit(page_size, page_number) params.append(offset) params.append(limit) data = self.db.query(sql, params) rows = [] for row in data: rows.append([row.command, row.channel, row.handler, (row.name or "Unknown(%s)" % row.char_id), self.util.format_datetime(row.created_at)]) display_table = self.text.pad_table(rows, " ", pad_right=False) blob = "" cmd_string = "usage history" if named_params.cmd: cmd_string += f" --cmd={named_params.cmd}" if named_params.char: cmd_string += f" --char={named_params.char}" blob += self.text.get_paging_links(cmd_string, page_number, len(data) == page_size) + "\n\n" for cols in display_table: blob += " ".join(cols) + "\n" title = "Command History" return ChatBlob(title, blob)
class RandomController: def inject(self, registry): self.db: DB = registry.get_instance("db") self.util = registry.get_instance("util") self.character_service = registry.get_instance("character_service") self.command_alias_service = registry.get_instance( "command_alias_service") def start(self): self.db.exec( "CREATE TABLE IF NOT EXISTS roll (id INT PRIMARY KEY AUTO_INCREMENT, created_at INT NOT NULL, char_id INT NOT NULL, options VARCHAR(2048), result VARCHAR(255))" ) self.command_alias_service.add_alias("verify", "roll verify") self.command_alias_service.add_alias("lootorder", "random") @command(command="random", params=[Any("items"), NamedParameters(["separator"])], access_level="all", description="Randomly order a list of elements", extended_description="Default separator is a space.") def random_command(self, request, items, named_params): separator = named_params.separator if named_params.separator else " " items = items.split(separator) random.shuffle(items) return separator.join(items) @command(command="roll", params=[Const("verify"), Int("roll_id")], access_level="all", description="Verify a roll that happened") def roll_verify_command(self, request, _, roll_id): row = self.db.query_single("SELECT * FROM roll WHERE id = ?", [roll_id]) if not row: return "Could not find roll with id <highlight>%d</highlight>." % roll_id else: time_string = self.util.time_to_readable( int(time.time()) - row.created_at) name = self.character_service.resolve_char_to_name(row.char_id) return "<highlight>%s</highlight> rolled by <highlight>%s</highlight> %s ago. Possible options: %s." % ( row.result, name, time_string, row.options) @command( command="roll", params=[Int("start_value", is_optional=True), Int("end_value")], access_level="all", description= "Roll a number between start_value and end_value number (inclusive)", extended_description="Start_value is assumed 1 if not provided.") def roll_number_command(self, request, start_value, end_value): start_value = start_value or 1 if start_value > end_value: end = start_value start = end_value else: start = start_value end = end_value result = random.randint(start, end) options = "value between %d and %d" % (start, end) self.db.exec( "INSERT INTO roll (created_at, char_id, options, result) VALUES (?, ?, ?, ?)", [int(time.time()), request.sender.char_id, options, result]) return "The roll is <highlight>%d</highlight> out of values between %d and %d. To verify do /tell <myname> verify %d" % ( result, start, end, self.db.last_insert_id()) # Keep this method at the bottom of file otherwise it will precede over all other commands @command( command="roll", params=[Any("items")], access_level="all", description="Roll a random value from a list of space-delimited values" ) def roll_text_variables_command(self, request, items): options = items.split(" ") result = random.choice(options) self.db.exec( "INSERT INTO roll (created_at, char_id, options, result) VALUES (?, ?, ?, ?)", [int(time.time()), request.sender.char_id, items, result]) return "The roll is <highlight>%s</highlight> out of possible options: %s. To verify do /tell <myname> verify %d" % ( result, items, self.db.last_insert_id())
class TowerAttackController: MESSAGE_SOURCE = "tower_attacks" def __init__(self): self.logger = Logger(__name__) def inject(self, registry): self.bot = registry.get_instance("bot") self.db = registry.get_instance("db") self.text = registry.get_instance("text") self.util = registry.get_instance("util") self.setting_service = registry.get_instance("setting_service") self.event_service = registry.get_instance("event_service") self.command_alias_service = registry.get_instance( "command_alias_service") self.public_channel_service = registry.get_instance( "public_channel_service") self.message_hub_service = registry.get_instance("message_hub_service") def pre_start(self): self.message_hub_service.register_message_source(self.MESSAGE_SOURCE) def start(self): self.db.exec( "CREATE TABLE IF NOT EXISTS tower_attacker (id INT PRIMARY KEY AUTO_INCREMENT, att_org_name VARCHAR(50) NOT NULL, att_faction VARCHAR(10) NOT NULL, " "att_char_id INT, att_char_name VARCHAR(20) NOT NULL, att_level INT NOT NULL, att_ai_level INT NOT NULL, att_profession VARCHAR(15) NOT NULL, " "x_coord INT NOT NULL, y_coord INT NOT NULL, is_victory SMALLINT NOT NULL, " "tower_battle_id INT NOT NULL, created_at INT NOT NULL)") self.db.exec( "CREATE TABLE IF NOT EXISTS tower_battle (id INT PRIMARY KEY AUTO_INCREMENT, playfield_id INT NOT NULL, site_number INT NOT NULL, " "def_org_name VARCHAR(50) NOT NULL, def_faction VARCHAR(10) NOT NULL, is_finished INT NOT NULL, battle_type VARCHAR(20) NOT NULL, last_updated INT NOT NULL)" ) self.command_alias_service.add_alias("victory", "attacks") self.setting_service.register(self.module_name, "show_tower_attack_messages", True, BooleanSettingType(), "Show tower attack messages") @command(command="attacks", params=[NamedParameters(["page"])], access_level="all", description="Show recent tower attacks and victories") def attacks_cmd(self, request, named_params): page_number = int(named_params.page or "1") page_size = 10 offset = (page_number - 1) * page_size sql = """SELECT b.*, p.long_name, p.short_name FROM tower_battle b LEFT JOIN playfields p ON b.playfield_id = p.id ORDER bY b.last_updated DESC LIMIT ?, ?""" data = self.db.query(sql, [offset, page_size]) t = int(time.time()) blob = self.check_for_all_towers_channel() blob += self.text.get_paging_links(f"attacks", page_number, page_size == len(data)) blob += "\n\n" for row in data: blob += "\n<pagebreak>" blob += self.format_battle_info(row, t) blob += self.text.make_tellcmd("More Info", "attacks battle %d" % row.id) + "\n" blob += "<header2>Attackers:</header2>\n" sql2 = """SELECT a.*, COALESCE(a.att_level, 0) AS att_level, COALESCE(a.att_ai_level, 0) AS att_ai_level FROM tower_attacker a WHERE a.tower_battle_id = ? ORDER BY created_at DESC""" data2 = self.db.query(sql2, [row.id]) for row2 in data2: blob += "<tab>" + self.format_attacker(row2) + "\n" if not data2: blob += "<tab>Unknown attacker\n" return ChatBlob("Tower Attacks", blob) @command(command="attacks", params=[Const("battle"), Int("battle_id")], access_level="all", description="Show battle info for a specific battle") def attacks_battle_cmd(self, request, _, battle_id): battle = self.get_battle(battle_id) if not battle: return "Could not find battle with ID <highlight>%d</highlight>." % battle_id blob = self.check_for_all_towers_channel() + self.get_battle_blob( battle) return ChatBlob("Battle Info %d" % battle_id, blob) @event(event_type=TowerController.TOWER_ATTACK_EVENT, description="Record tower attacks", is_hidden=True) def tower_attack_event(self, event_type, event_data): t = int(time.time()) site_number = self.find_closest_site_number( event_data.location.playfield.id, event_data.location.x_coord, event_data.location.y_coord) attacker = event_data.attacker or {} defender = event_data.defender location = event_data.location battle = self.find_or_create_battle(event_data.location.playfield.id, site_number, defender.org_name, defender.faction, "attack", t) self.db.exec( "INSERT INTO tower_attacker (att_org_name, att_faction, att_char_id, att_char_name, att_level, att_ai_level, att_profession, " "x_coord, y_coord, is_victory, tower_battle_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ attacker.get("org_name", ""), attacker.get("faction", ""), attacker.get("char_id", 0), attacker.get("name", ""), attacker.get("level", 0), attacker.get("ai_level", 0), attacker.get("profession", ""), location.x_coord, location.y_coord, 0, battle.id, t ]) attacker_id = self.db.last_insert_id() if self.setting_service.get("show_tower_attack_messages").get_value(): attacker_row = self.db.query_single( "SELECT * FROM tower_attacker WHERE id = ?", [attacker_id]) more_info = self.text.paginate_single( ChatBlob( "More Info", self.text.make_tellcmd("More Info", f"attacks battle {battle.id}")), self.bot.get_primary_conn()) msg = "%s attacked %s [%s] at %s %s %s" % ( self.format_attacker(attacker_row), defender.org_name, self.text.get_formatted_faction(defender.faction), location.playfield.get("short_name", location.playfield.get("long_name")), site_number or "?", more_info) self.message_hub_service.send_message(self.MESSAGE_SOURCE, None, None, msg) @event(event_type=TowerController.TOWER_VICTORY_EVENT, description="Record tower victories", is_hidden=True) def tower_victory_event(self, event_type, event_data): t = int(time.time()) if event_data.type == "attack": row = self.get_last_attack(event_data.winner.faction, event_data.winner.org_name, event_data.loser.faction, event_data.loser.org_name, event_data.location.playfield.id, t) if not row: site_number = 0 is_finished = 1 self.db.exec( "INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)", [ event_data.location.playfield.id, site_number, event_data.loser.org_name, event_data.loser.faction, is_finished, event_data.type, t ]) battle_id = self.db.last_insert_id() attacker = event_data.winner or {} self.db.exec( "INSERT INTO tower_attacker (att_org_name, att_faction, att_char_id, att_char_name, att_level, att_ai_level, att_profession, " "x_coord, y_coord, is_victory, tower_battle_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ attacker.get("org_name", ""), attacker.get("faction", ""), attacker.get("char_id", 0), attacker.get("name", ""), attacker.get("level", 0), attacker.get("ai_level", 0), attacker.get("profession", ""), 0, 0, 0, battle_id, t ]) else: is_victory = 1 self.db.exec( "UPDATE tower_attacker SET is_victory = ? WHERE id = ?", [is_victory, row.attack_id]) is_finished = 1 self.db.exec( "UPDATE tower_battle SET is_finished = ?, last_updated = ? WHERE id = ?", [is_finished, t, row.battle_id]) elif event_data.type == "terminated": site_number = 0 is_finished = 1 self.db.exec( "INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)", [ event_data.location.playfield.id, site_number, event_data.loser.org_name, event_data.loser.faction, is_finished, event_data.type, t ]) else: raise Exception("Unknown victory event type: '%s'" % event_data.type) def format_attacker(self, row): level = ("%d/<green>%d</green>" % (row.att_level, row.att_ai_level) ) if row.att_ai_level > 0 else "%d" % row.att_level org = row.att_org_name + " " if row.att_org_name else "" victor = " - <notice>Winner!</notice>" if row.is_victory else "" return "%s (%s %s) %s[%s]%s" % ( row.att_char_name or "Unknown attacker", level, row.att_profession, org, self.text.get_formatted_faction(row.att_faction), victor) def find_closest_site_number(self, playfield_id, x_coord, y_coord): sql = """ SELECT site_number, ((x_distance * x_distance) + (y_distance * y_distance)) radius FROM (SELECT playfield_id, site_number, min_ql, max_ql, x_coord, y_coord, site_name, (x_coord - ?) as x_distance, (y_coord - ?) as y_distance FROM tower_site WHERE playfield_id = ?) t ORDER BY radius ASC LIMIT 1""" row = self.db.query_single(sql, [x_coord, y_coord, playfield_id]) if row: return row.site_number else: return 0 def find_or_create_battle(self, playfield_id, site_number, org_name, faction, battle_type, t): last_updated = t - (8 * 3600) is_finished = 0 sql = """ SELECT id FROM tower_battle WHERE playfield_id = ? AND site_number = ? AND is_finished = ? AND def_org_name = ? AND def_faction = ? AND last_updated >= ? """ battle = self.db.query_single(sql, [ playfield_id, site_number, is_finished, org_name, faction, last_updated ]) if battle: self.db.exec( "UPDATE tower_battle SET last_updated = ? WHERE id = ?", [t, battle.id]) battle_id = battle.id else: self.db.exec( "INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)", [ playfield_id, site_number, org_name, faction, is_finished, battle_type, t ]) battle_id = self.db.last_insert_id() return self.get_battle(battle_id) def get_last_attack(self, att_faction, att_org_name, def_faction, def_org_name, playfield_id, t): last_updated = t - (8 * 3600) is_finished = 0 sql = """ SELECT b.id AS battle_id, a.id AS attack_id FROM tower_battle b JOIN tower_attacker a ON a.tower_battle_id = b.id WHERE a.att_faction = ? AND a.att_org_name = ? AND b.def_faction = ? AND b.def_org_name = ? AND b.playfield_id = ? AND b.is_finished = ? AND b.last_updated >= ? ORDER BY last_updated DESC LIMIT 1""" return self.db.query_single(sql, [ att_faction, att_org_name, def_faction, def_org_name, playfield_id, is_finished, last_updated ]) def format_battle_info(self, row, t, verbose=False): blob = "" defeated = " - <notice>Defeated!</notice>" if row.is_finished else "" blob += "Site: <highlight>%s %s</highlight>\n" % ( row.short_name, row.site_number or "?") if verbose: if row.site_number: blob += f"Long name: <highlight>{row.site_name}, {row.long_name}</highlight>\n" blob += f"Level range: <highlight>{row.min_ql}-{row.max_ql}</highlight>\n" blob += "Coordinates: %s\n" % self.text.make_chatcmd( f"{row.x_coord}x{row.y_coord}", f"/waypoint {row.x_coord} {row.y_coord} {row.playfield_id}" ) else: blob += f"Long name: Unknown\n" blob += f"Level range: Unknown\n" blob += "Coordinates: Unknown\n" blob += f"Defender: %s [%s]%s\n" % (row.def_org_name, self.text.get_formatted_faction( row.def_faction), defeated) blob += "Last Activity: %s\n" % self.format_timestamp( row.last_updated, t) return blob def format_timestamp(self, t, current_t): return "<highlight>%s</highlight> (%s ago)" % ( self.util.format_datetime(t), self.util.time_to_readable(current_t - t)) def get_chat_command(self, page): return "/tell <myname> attacks --page=%d" % page def check_for_all_towers_channel(self): if TowerController.ALL_TOWERS_ID not in self.bot.get_primary_conn( ).channels: return "Notice: The primary bot must belong to an org and be promoted to a rank that is high enough to have the All Towers channel (e.g., Squad Commander) in order for the <symbol>attacks command to work correctly.\n\n" else: return "" def get_battle_blob(self, battle): t = int(time.time()) attackers = self.db.query( "SELECT * FROM tower_attacker WHERE tower_battle_id = ? ORDER BY created_at DESC", [battle.id]) first_activity = attackers[-1].created_at if len( attackers) > 0 else battle.last_updated blob = "" blob += self.format_battle_info(battle, t, verbose=True) blob += "Duration: <highlight>%s</highlight>\n\n" % self.util.time_to_readable( battle.last_updated - first_activity) blob += "<header2>Attackers:</header2>\n" for row in attackers: blob += "<tab>" + self.format_attacker(row) blob += " " + self.format_timestamp(row.created_at, t) blob += "\n" return blob def get_battle(self, battle_id): return self.db.query_single( "SELECT b.*, p.short_name, p.long_name, t.site_name, t.x_coord, t.y_coord, t.min_ql, t.max_ql " "FROM tower_battle b " "LEFT JOIN playfields p ON p.id = b.playfield_id " "LEFT JOIN tower_site t ON b.playfield_id = t.playfield_id AND b.site_number = t.site_number " "WHERE b.id = ?", [battle_id])
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): blob = self.getresp("module/help", "about_head") blob += self.getresp("module/help", "about_body") blob += self.getresp("module/help", "about_special_ones") blob += self.getresp("module/help", "about_improvers") blob += self.getresp("module/help", "about_bottom") return ChatBlob( self.getresp("module/help", "blob_title", {"ver": self.bot.version}), blob) @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 + "<end>" if module != current_module: current_module = module blob += "\n" + module + ":" if row.command != current_command: current_command = row.command blob += " " + self.text.make_chatcmd( row.command, "/tell <myname> help " + row.command) return ChatBlob("Help (main)", blob) @command(command="help", params=[Any("command"), NamedParameters(["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 show_regex = named_params.show_regex and named_params.show_regex.lower( ) == "true" help_text = self.command_service.get_help_text(request.sender.char_id, help_topic, request.channel, 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})
class NanoController: def inject(self, registry): self.db = registry.get_instance("db") self.util = registry.get_instance("util") self.text = registry.get_instance("text") self.command_alias_service = registry.get_instance( "command_alias_service") def start(self): self.command_alias_service.add_alias("nl", "nanolines") self.command_alias_service.add_alias("nanoline", "nanolines") @command(command="nano", params=[Any("search"), NamedParameters(["page"])], access_level="all", description="Search for a nano") def nano_cmd(self, request, search, named_params): page = int(named_params.page or "1") page_size = 30 offset = (page - 1) * page_size sql = "SELECT n1.lowid, n1.lowql, n1.name, n1.location, n1.profession, n3.id AS nanoline_id, n3.name AS nanoline_name " \ "FROM nanos n1 " \ "LEFT JOIN nanos_nanolines_ref n2 ON n1.lowid = n2.lowid " \ "LEFT JOIN nanolines n3 ON n2.nanolines_id = n3.id " \ "WHERE n1.name <EXTENDED_LIKE=0> ? " \ "ORDER BY n1.profession, n3.name, n1.lowql DESC, n1.name ASC" data = self.db.query(sql, [search], extended_like=True) count = len(data) paged_data = data[offset:offset + page_size] blob = "" if count > page_size: if page > 1 and len(paged_data) > 0: blob += " " + self.text.make_chatcmd( "<< Page %d" % (page - 1), self.get_chat_command(search, page - 1)) if offset + page_size < len(data): blob += " Page " + str(page) blob += " " + self.text.make_chatcmd( "Page %d >>" % (page + 1), self.get_chat_command(search, page + 1)) blob += "\n\n" current_nanoline = -1 for row in paged_data: if current_nanoline != row.nanoline_id: if row.nanoline_name: blob += "\n<header2>%s<end> - %s\n" % ( row.profession, self.text.make_chatcmd( row.nanoline_name, "/tell <myname> nanolines %d" % row.nanoline_id)) else: blob += "\n<header2>Unknown/General<end>\n" current_nanoline = row.nanoline_id blob += "%s [%d] %s\n" % (self.text.make_item( row.lowid, row.lowid, row.lowql, row.name), row.lowql, row.location) blob += self.get_footer() return ChatBlob( "Nano Search Results for '%s' (%d - %d of %d)" % (search, offset + 1, min(offset + page_size, count), count), blob) @command(command="nanoloc", params=[], access_level="all", description="Show all nano locations") def nanoloc_list_cmd(self, request): data = self.db.query( "SELECT location, COUNT(location) AS cnt FROM nanos GROUP BY location ORDER BY location ASC" ) blob = "" for row in data: blob += "%s (%d)\n" % (self.text.make_chatcmd( row.location, "/tell <myname> nanoloc %s" % row.location), row.cnt) blob += self.get_footer() return ChatBlob("Nano Locations", blob) @command(command="nanoloc", params=[Any("location")], access_level="all", description="Show nanos by location") def nanoloc_show_cmd(self, request, location): sql = "SELECT n1.lowid, n1.lowql, n1.name, n1.location, n3.profession " \ "FROM nanos n1 LEFT JOIN nanos_nanolines_ref n2 ON n1.lowid = n2.lowid LEFT JOIN nanolines n3 ON n2.nanolines_id = n3.id " \ "WHERE n1.location LIKE ? " \ "ORDER BY n1.profession ASC, n1.name ASC" data = self.db.query(sql, [location]) cnt = len(data) blob = "" for row in data: blob += "%s [%d] %s" % (self.text.make_item( row.lowid, row.lowid, row.lowql, row.name), row.lowql, row.location) if row.profession: blob += " - <highight>%s<end>" % row.profession blob += "\n" return ChatBlob("Nanos for Location '%s' (%d)" % (location, cnt), blob) @command(command="nanolines", params=[], access_level="all", description="Show nanos by nanoline") def nanolines_list_cmd(self, request): data = self.db.query( "SELECT DISTINCT profession FROM nanolines ORDER BY profession ASC" ) blob = "" for row in data: blob += self.text.make_chatcmd( row.profession, "/tell <myname> nanolines %s" % row.profession) + "\n" blob += self.get_footer() return ChatBlob("Nanolines", blob) @command(command="nanolines", params=[Int("nanoline_id")], access_level="all", description="Show nanos by nanoline id") def nanolines_id_cmd(self, request, nanoline_id): nanoline = self.db.query_single("SELECT * FROM nanolines WHERE id = ?", [nanoline_id]) if not nanoline: return "Could not find nanoline with ID <highlight>%d<end>." % nanoline_id data = self.db.query( "SELECT n1.lowid, n1.lowql, n1.name, n1.location " "FROM nanos n1 JOIN nanos_nanolines_ref n2 ON n1.lowid = n2.lowid " "WHERE n2.nanolines_id = ? " "ORDER BY n1.lowql DESC, n1.name ASC", [nanoline_id]) blob = "" for row in data: blob += "%s [%d] %s\n" % (self.text.make_item( row.lowid, row.lowid, row.lowql, row.name), row.lowql, row.location) blob += self.get_footer() return ChatBlob("%s %s Nanos" % (nanoline.profession, nanoline.name), blob) @command(command="nanolines", params=[Any("profession")], access_level="all", description="Show nanolines by profession") def nanolines_profession_cmd(self, request, prof_name): profession = self.util.get_profession(prof_name) if not profession: return "Could not find profession <highlight>%s<end>." % prof_name data = self.db.query( "SELECT * FROM nanolines WHERE profession = ? ORDER BY name ASC", [profession]) blob = "" for row in data: blob += self.text.make_chatcmd( row.name, "/tell <myname> nanolines %d" % row.id) + "\n" blob += self.get_footer() return ChatBlob("%s Nanolines" % profession, blob) def get_footer(self): return "\n\nNanos DB provided by Saavick & Lucier" def get_chat_command(self, search, page): return "/tell <myname> nano %s --page=%d" % (search, page)
class OrgActivityController: def __init__(self): self.logger = Logger(__name__) 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") def start(self): self.db.exec("CREATE TABLE IF NOT EXISTS org_activity (id INT PRIMARY KEY AUTO_INCREMENT, actor_char_id INT NOT NULL, actee_char_id INT NOT NULL, " "action VARCHAR(20) NOT NULL, created_at INT NOT NULL, org_id INT NOT NULL)") self.command_alias_service.add_alias("orghistory", "orgactivity") @command(command="orgactivity", params=[Character("char", is_optional=True), NamedParameters(["action", "page"])], access_level="org_member", description="Show org member activity") def orgactivity_cmd(self, request, char, named_params): page_size = 20 page_number = int(named_params.page or "1") params = [] filter_sql = "WHERE 1=1" if char: if not char.char_id: return StandardMessage.char_not_found(char.name) else: filter_sql += " AND (o.actor_char_id = ? OR o.actee_char_id = ?)" params.append(char.char_id) params.append(char.char_id) if named_params.action: named_params.action = named_params.action.lower() filter_sql += " AND o.action LIKE ?" params.append(named_params.action) offset, limit = self.util.get_offset_limit(page_size, page_number) params.append(offset) params.append(limit) sql = f""" SELECT p1.name AS actor, p2.name AS actee, o.action, o.created_at, o.org_id FROM org_activity o LEFT JOIN player p1 ON o.actor_char_id = p1.char_id LEFT JOIN player p2 ON o.actee_char_id = p2.char_id {filter_sql} ORDER BY o.created_at DESC LIMIT ?, ? """ data = self.db.query(sql, params) command_str = "orgactivity" if char: command_str += " " + char.name if named_params.action: command_str += " --action=" + named_params.action blob = self.text.get_paging_links(command_str, page_number, len(data) == page_size) + "\n\n" for row in data: blob += self.format_org_action(row) + "\n" title = "Org Activity" if char: title += " for " + char.name if named_params.action: title += " [" + named_params.action + "]" return ChatBlob(title, blob) @event(PublicChannelService.ORG_MSG_EVENT, "Record org member activity", is_system=True) def org_msg_event(self, event_type, event_data): ext_msg = event_data.extended_message org_id = event_data.conn.org_id if [ext_msg.category_id, ext_msg.instance_id] == OrgMemberController.LEFT_ORG: self.save_activity(ext_msg.params[0], ext_msg.params[0], "left", org_id) elif [ext_msg.category_id, ext_msg.instance_id] == OrgMemberController.KICKED_FROM_ORG: self.save_activity(ext_msg.params[0], ext_msg.params[1], "kicked", org_id) elif [ext_msg.category_id, ext_msg.instance_id] == OrgMemberController.INVITED_TO_ORG: self.save_activity(ext_msg.params[0], ext_msg.params[1], "invited", org_id) elif [ext_msg.category_id, ext_msg.instance_id] == OrgMemberController.KICKED_INACTIVE_FROM_ORG: self.save_activity(ext_msg.params[0], ext_msg.params[1], "removed", org_id) elif [ext_msg.category_id, ext_msg.instance_id] == OrgMemberController.KICKED_ALIGNMENT_CHANGED: self.save_activity(ext_msg.params[0], ext_msg.params[0], "alignment changed", org_id) elif [ext_msg.category_id, ext_msg.instance_id] == OrgMemberController.JOINED_ORG: self.save_activity(ext_msg.params[0], ext_msg.params[0], "joined", org_id) def save_activity(self, actor, actee, action, org_id): actor_id = self.character_service.resolve_char_to_id(actor) actee_id = self.character_service.resolve_char_to_id(actee) if actee else 0 if not actor_id: self.logger.error("Could not get char_id for actor '%s'" % actor) if not actee_id: self.logger.error("Could not get char_id for actee '%s'" % actee) t = int(time.time()) self.db.exec("INSERT INTO org_activity (actor_char_id, actee_char_id, action, created_at, org_id) VALUES (?, ?, ?, ?, ?)", [actor_id, actee_id, action, t, org_id]) def format_org_action(self, row): org_name = self.get_org_name(row.org_id) created_at_str = self.util.format_datetime(row.created_at) if row.action == "left" or row.action == "alignment changed" or row.action == "joined": return f"<highlight>{row.actor}</highlight> {row.action}. [{org_name}] {created_at_str}" else: return f"<highlight>{row.actor}</highlight> {row.action} <highlight>{row.actee}</highlight>. [{org_name}] {created_at_str}" def get_org_name(self, org_id): conn: Conn = self.bot.get_conn_by_org_id(org_id) if conn: return conn.get_org_name() else: return f"UnknownOrg({org_id})"
class ItemsController: def inject(self, registry): self.db: DB = registry.get_instance("db") self.text: Text = registry.get_instance("text") self.command_alias_service = registry.get_instance( "command_alias_service") def start(self): self.command_alias_service.add_alias("item", "items") self.command_alias_service.add_alias("i", "items") @command(command="items", params=[ Int("ql", is_optional=True), Any("search"), NamedParameters(["page"]) ], access_level="all", description="Search for an item") def items_cmd(self, request, ql, search, named_params): page = int(named_params.page or "1") search = html.unescape(search) page_size = 30 offset = (page - 1) * page_size all_items = self.find_items(search, ql) if search.isdigit(): all_items += [self.get_by_item_id(int(search))] + all_items items = self.sort_items(search, all_items)[offset:offset + page_size] cnt = len(items) if cnt == 0: if ql: return "No QL <highlight>%d<end> items found matching <highlight>%s<end>." % ( ql, search) else: return "No items found matching <highlight>%s<end>." % search else: blob = "" blob += "Version: <highlight>%s<end>\n" % "unknown" if ql: blob += "Search: <highlight>QL %d %s<end>\n" % (ql, search) else: blob += "Search: <highlight>%s<end>\n" % search blob += "\n" if page > 1: blob += " " + self.text.make_chatcmd( "<< Page %d" % (page - 1), self.get_chat_command(ql, search, page - 1)) if offset + page_size < len(all_items): blob += " Page " + str(page) blob += " " + self.text.make_chatcmd( "Page %d >>" % (page + 1), self.get_chat_command(ql, search, page + 1)) blob += "\n" blob += self.format_items(items, ql) blob += "\nItem DB rips created using the %s tool." % self.text.make_chatcmd( "Budabot Items Extractor", "/start https://github.com/Budabot/ItemsExtractor") return ChatBlob( "Item Search Results (%d - %d of %d)" % (offset + 1, min(offset + page_size, len(all_items)), len(all_items)), blob) def format_items(self, items, ql=None): blob = "" for item in items: blob += "<pagebreak>" blob += self.text.make_image(item.icon) + "\n" if ql: blob += "QL %d " % ql blob += self.text.make_item(item.lowid, item.highid, ql, item.name) else: blob += self.text.make_item(item.lowid, item.highid, item.highql, item.name) if item.lowql != item.highql: blob += " (QL%d - %d)\n" % (item.lowql, item.highql) else: blob += " (QL%d)\n" % item.highql blob += "\n" return blob def find_items(self, name, ql=None): params = [name] sql = "SELECT * FROM aodb WHERE name LIKE ? " if ql: sql += " AND lowql <= ? AND highql >= ?" params.append(ql) params.append(ql) sql += " UNION SELECT * FROM aodb WHERE name <EXTENDED_LIKE=%d> ?" % len( params) params.append(name) if ql: sql += " AND lowql <= ? AND highql >= ?" params.append(ql) params.append(ql) sql += " ORDER BY name ASC, highql DESC" return self.db.query(sql, params, extended_like=True) def sort_items(self, search, items): search = search.lower() search_parts = search.split(" ") # if item name matches search exactly (case-insensitive) then priority = 0 # if item name contains every whole word from search (case-insensitive) then priority = 1 # +1 priority for each whole word from search that item name does not contain for row in items: row.priority = 0 row_name = row.name.lower() if row_name != search: row.priority += 1 row_parts = row_name.split(" ") for search_part in search_parts: if search_part not in row_parts: row.priority += 1 items.sort(key=lambda x: x.priority, reverse=False) return items def get_by_item_id(self, item_id): return self.db.query_single( "SELECT * FROM aodb WHERE highid = ? OR lowid = ? ORDER BY highid = ?", [item_id, item_id, item_id]) def find_by_name(self, name, ql=None): if ql: return self.db.query_single( "SELECT * FROM aodb WHERE name = ? AND lowql <= ? AND highql >= ? ORDER BY highid DESC", [name, ql, ql]) else: return self.db.query_single( "SELECT * FROM aodb WHERE name = ? ORDER BY highql DESC, highid DESC", [name]) def get_chat_command(self, ql, search, page): if ql: return "/tell <myname> items %d %s --page=%d" % (ql, search, page) else: return "/tell <myname> items %s --page=%d" % (search, page)
class AltsController: SORT_OPTIONS = ["level", "name", "profession"] def inject(self, registry): self.bot = registry.get_instance("bot") self.alts_service: AltsService = registry.get_instance("alts_service") self.buddy_service = registry.get_instance("buddy_service") self.util = registry.get_instance("util") @command(command="alts", params=[NamedParameters(["sort_by"])], access_level="all", description="Show your alts", extended_description="Sort_by param can be one of: " + ", ".join(SORT_OPTIONS)) def alts_list_cmd(self, request, named_params): if named_params.sort_by: if named_params.sort_by not in self.SORT_OPTIONS: return "Sort_by parameter must be one of: " + ", ".join( self.SORT_OPTIONS) else: named_params.sort_by = "level" alts = self.alts_service.get_alts(request.sender.char_id, named_params.sort_by) blob = self.format_alt_list(alts) return ChatBlob(f"Alts of {alts[0].name} ({len(alts)})", blob) def get_alt_status(self, status): if status == AltsService.MAIN: return " - [main]" else: return "" @command( command="alts", params=[Const("setmain")], access_level="all", description="Set a new main", extended_description= "You must run this from the character you want to be your new main") def alts_setmain_cmd(self, request, _): msg, result = self.alts_service.set_as_main(request.sender.char_id) if result: return f"Character <highlight>{request.sender.name}</highlight> has been set as your main." elif msg == "not_an_alt": return "Error! Character <highlight>{request.sender.name}</highlight> cannot be set as your main since you do not have any alts." elif msg == "already_main": return "Error! Character <highlight>{request.sender.name}</highlight> is already set as your main." else: raise Exception("Unknown msg: " + msg) @command(command="alts", params=[Const("add"), Multiple(Character("character"))], access_level="all", description="Add one or more alts") def alts_add_cmd(self, request, _, alt_chars): responses = [] for alt_char in alt_chars: if not alt_char.char_id: responses.append(StandardMessage.char_not_found(alt_char.name)) elif alt_char.char_id == request.sender.char_id: responses.append( "Error! You cannot register yourself as an alt.") else: msg, result = self.alts_service.add_alt( request.sender.char_id, alt_char.char_id) if result: self.bot.send_private_message( alt_char.char_id, f"Character <highlight>{request.sender.name}</highlight> has added you as an alt.", conn=request.conn) responses.append( f"Character <highlight>{alt_char.name}</highlight> has been added as your alt." ) elif msg == "another_main": responses.append( f"Error! Character <highlight>{alt_char.name}</highlight> already has alts." ) else: raise Exception("Unknown msg: " + msg) return "\n".join(responses) @command(command="alts", params=[Options(["rem", "remove"]), Character("character")], access_level="all", description="Remove an alt") def alts_remove_cmd(self, request, _, alt_char): if not alt_char.char_id: return StandardMessage.char_not_found(alt_char.name) msg, result = self.alts_service.remove_alt(request.sender.char_id, alt_char.char_id) if result: return f"Character <highlight>{alt_char.name}</highlight> has been removed as your alt." elif msg == "not_alt": return f"Error! Character <highlight>{alt_char.name}</highlight> is not your alt." elif msg == "remove_main": return "Error! You cannot remove your main." else: raise Exception("Unknown msg: " + msg) @command(command="alts", params=[Character("character"), NamedParameters(["sort_by"])], access_level="member", description="Show alts of another character", sub_command="show", extended_description="Sort_by param can be one of: " + ", ".join(SORT_OPTIONS)) def alts_list_other_cmd(self, request, char, named_params): if not char.char_id: return StandardMessage.char_not_found(char.name) if named_params.sort_by: if named_params.sort_by not in self.SORT_OPTIONS: return "Sort_by parameter must be one of: " + ", ".join( self.SORT_OPTIONS) else: named_params.sort_by = "level" alts = self.alts_service.get_alts(char.char_id, named_params.sort_by) blob = self.format_alt_list(alts) return ChatBlob(f"Alts of {alts[0].name} ({len(alts)})", blob) @command(command="altadmin", params=[Const("add"), Character("main"), Character("alt")], access_level="moderator", description="Add alts to main") def altadmin_add_cmd(self, request, _, main, alt): if not main.char_id: return StandardMessage.char_not_found(main.name) if not alt.char_id: return StandardMessage.char_not_found(alt.name) elif main.char_id == alt.char_id: return "Error! Alt and main are identical." msg, result = self.alts_service.add_alt(main.char_id, alt.char_id) if result: return f"Character <highlight>{alt.name}</highlight> was added as an alt of <highlight>{main.name}</highlight> successfully." elif msg == "another_main": return f"Error! Character <highlight>{alt.name}</highlight> already has alts." else: raise Exception("Unknown msg: " + msg) @command(command="altadmin", params=[ Options(["rem", "remove"]), Character("main"), Character("alt") ], access_level="moderator", description="Remove alts from main") def altadmin_remove_cmd(self, request, _, main, alt): if not main.char_id: return StandardMessage.char_not_found(main.name) if not alt.char_id: return StandardMessage.char_not_found(alt.name) msg, result = self.alts_service.remove_alt(main.char_id, alt.char_id) if result: return f"Character <highlight>{alt.name}</highlight> was added as an alt of <highlight>{main.name}</highlight> successfully." elif msg == "not_alt": return f"Error! Character <highlight>{alt.name}</highlight> is not an alt of <highlight>{main.name}</highlight>." elif msg == "remove_main": return "Error! Main characters may not be removed from their alt list." else: raise Exception("Unknown msg: " + msg) def format_alt_list(self, alts): blob = "" for alt in alts: blob += "<highlight>%s</highlight> (%d/<green>%d</green>) %s %s" % ( alt.name, alt.level, alt.ai_level, alt.faction, alt.profession) if self.buddy_service.is_online(alt.char_id): blob += " [<green>Online</green>]" blob += "\n" return blob
class ConfigEventsController: 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.ts: TranslationService = registry.get_instance("translation_service") self.getresp = self.ts.get_response @command(command="config", params=[Const("event"), Any("event_type"), Any("event_handler"), Options(["enable", "disable"])], access_level="admin", description="Enable or disable an event") def config_event_status_cmd(self, request, _, event_type, event_handler, action): event_type = event_type.lower() event_handler = event_handler.lower() action = action.lower() event_base_type, event_sub_type = self.event_service.get_event_type_parts(event_type) enabled = 1 if action == "enable" else 0 if not self.event_service.is_event_type(event_base_type): return self.getresp("module/config", "unknown event", {"type", event_type}) count = self.event_service.update_event_status(event_base_type, event_sub_type, event_handler, enabled) if count == 0: return self.getresp("module/config", "event_enable_fail", {"type": event_type, "handler": event_handler}) else: action = self.getresp("module/config", "enabled_high" if action == "enable" else "disabled_high") return self.getresp("module/config", "event_enable_success", {"type": event_type, "handler": event_handler, "changedto": action}) @command(command="config", params=[Const("event"), Any("event_type"), Any("event_handler"), Const("run")], access_level="admin", description="Execute a timed event immediately") def config_event_run_cmd(self, request, _1, event_type, event_handler, _2): action = "run" event_type = event_type.lower() event_handler = event_handler.lower() event_base_type, event_sub_type = self.event_service.get_event_type_parts(event_type) if not self.event_service.is_event_type(event_base_type): return self.getresp("module/config", "unknown event", {"type", event_type}) row = self.db.query_single("SELECT e.event_type, e.event_sub_type, e.handler, t.next_run FROM timer_event t " "JOIN event_config e ON t.event_type = e.event_type AND t.handler = e.handler " "WHERE e.event_type = ? AND e.event_sub_type = ? AND e.handler LIKE ?", [event_base_type, event_sub_type, event_handler]) if not row: return self.getresp("module/config", "event_enable_fail", {"type": event_type, "handler": event_handler}) elif row.event_type != "timer": return self.getresp("module/config", "event_manual") else: self.event_service.execute_timed_event(row, int(time.time())) action = self.getresp("module/config", "run") return self.getresp("module/config", "event_enable_success", {"type": event_type, "handler": event_handler, "changedto": action}) @command(command="config", params=[Const("eventlist"), NamedParameters(["event_type"])], access_level="admin", description="List all events") def config_eventlist_cmd(self, request, _, named_params): params = [] sql = "SELECT module, event_type, event_sub_type, handler, description, enabled, is_hidden FROM event_config" #sql += " WHERE is_hidden = 0" if named_params.event_type: sql += " WHERE event_type = ?" params.append(named_params.event_type) sql += " ORDER BY module, is_hidden, event_type, event_sub_type, handler" data = self.db.query(sql, params) blob = "Asterisk (*) denotes a hidden event. Only change these events if you understand the implications.\n" current_module = "" for row in data: if current_module != row.module: blob += "\n<pagebreak><header2>%s</header2>\n" % row.module current_module = row.module event_type_key = self.format_event_type(row) on_link = self.text.make_tellcmd("On", "config event %s %s enable" % (event_type_key, row.handler)) off_link = self.text.make_tellcmd("Off", "config event %s %s disable" % (event_type_key, row.handler)) if row.is_hidden == 1: blob += "*" blob += "%s [%s] %s %s - %s\n" % (event_type_key, self.format_enabled(row.enabled), on_link, off_link, row.description) return ChatBlob(self.getresp("module/config", "blob_events", {"amount": len(data)}), blob) def format_enabled(self, enabled): return "<green>E</green>" if enabled else "<red>D</red>" def format_event_type(self, row): if row.event_sub_type: return row.event_type + ":" + row.event_sub_type else: return row.event_type
class BuddyController: def __init__(self): self.logger = Logger(__name__) def inject(self, registry): self.bot = registry.get_instance("bot") self.character_service = registry.get_instance("character_service") self.buddy_service = registry.get_instance("buddy_service") @command(command="buddylist", params=[Const("add"), Character("character"), Any("type")], access_level="admin", description="Add a character to the buddy list") def buddylist_add_cmd(self, request, _, char, buddy_type): buddy_type = buddy_type.lower() if char.char_id: self.buddy_service.add_buddy(char.char_id, buddy_type) return f"Character <highlight>{char.name}</highlight> has been added to the buddy list for type <highlight>{buddy_type}</highlight>." else: return StandardMessage.char_not_found(char.name) @command(command="buddylist", params=[Options(["rem", "remove"]), Const("all")], access_level="admin", description="Remove all characters from the buddy list") def buddylist_remove_all_cmd(self, request, _1, _2): buddies = self.buddy_service.get_all_buddies().items() for char_id, buddy in buddies: self.buddy_service.remove_buddy(char_id, None, True) return f"Removed all <highlight>{len(buddies)}</highlight> buddies from the buddy list." @command(command="buddylist", params=[Options(["rem", "remove"]), Character("character"), Any("type")], access_level="admin", description="Remove a character from the buddy list by type") def buddylist_remove_cmd(self, request, _, char, buddy_type): buddy_type = buddy_type.lower() if char.char_id: self.buddy_service.remove_buddy(char.char_id, buddy_type) return f"Character <highlight>{char.name}</highlight> has been removed from the buddy list for type <highlight>{buddy_type}</highlight>." else: return StandardMessage.char_not_found(char.name) @command(command="buddylist", params=[Options(["rem", "remove"]), Character("character")], access_level="admin", description="Remove a character from the buddy list forcefully") def buddylist_remove_force_cmd(self, request, _, char): if char.char_id: self.buddy_service.remove_buddy(char.char_id, None, force_remove=True) return f"Character <highlight>{char.name}</highlight> has been removed from the buddy list forcefully." else: return StandardMessage.char_not_found(char.name) @command(command="buddylist", params=[Const("clean")], access_level="admin", description="Remove all orphaned buddies from the buddy list") def buddylist_clean_cmd(self, request, _): num_removed = self.remove_orphaned_buddies() return f"Removed <highlight>{num_removed}</highlight> orphaned buddies from the buddy list." @command(command="buddylist", params=[Const("search", is_optional=True), Any("character", allowed_chars="[a-z0-9-]", is_optional=True), NamedParameters(["inactive"])], access_level="admin", description="Search for characters on the buddy list", extended_description="Use --inactive=include (default), --inactive=exclude, or --inactive=only to control if inactive buddies are shown") def buddylist_search_cmd(self, request, _, search, named_params): is_search = False if search: is_search = True search = search.lower() include_active = True include_inactive = True if named_params.inactive: if named_params.inactive.lower() == "exclude": include_inactive = False elif named_params.inactive.lower() == "only": include_active = False elif named_params.inactive.lower() != "include": return "Named parameter <highlight>--inactive</highlight> only allows values <highlight>include</highlight>, " \ "<highlight>exclude</highlight>, or <highlight>only</highlight>." is_search = True buddy_list = [] for char_id, buddy in self.buddy_service.get_all_buddies().items(): is_active = buddy["online"] is not None if not include_active and is_active: continue elif not include_inactive and not is_active: continue char_name = self.character_service.resolve_char_to_name(char_id, "Unknown(%d)" % char_id) if not search or search in char_name.lower(): buddy_list.append([char_name, buddy]) blob = self.format_buddies(buddy_list) if is_search: return ChatBlob(f"Buddy List Search Results ({len(buddy_list)})", blob) else: return ChatBlob(f"Buddy list ({len(buddy_list)})", blob) @timerevent(budatime="24h", description="Remove orphaned buddies", is_system=True) def remove_orphaned_buddies_event(self, event_type, event_data): if self.bot.is_ready(): self.logger.debug("Removing %d orphaned buddies" % self.remove_orphaned_buddies()) def remove_orphaned_buddies(self): count = 0 for char_id, buddy in self.buddy_service.get_all_buddies().items(): if len(buddy["types"]) == 0: self.buddy_service.remove_buddy(char_id, None, True) count += 1 return count def format_buddies(self, buddy_list): buddy_list = sorted(buddy_list, key=lambda x: x[0]) blob = "" for name, buddy in buddy_list: pending = "*" if buddy["online"] is None else "" blob += "%s%s [%s] - %s\n" % (name, pending, buddy["conn_id"], ",".join(buddy["types"])) blob += "\nAsterisk (*) indicates the buddy is pending and may not be active." return blob
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", "r") as f: return ChatBlob("About Tyrbot %s" % 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 + "<end>" if module != current_module: current_module = module blob += "\n" + module + ":" if row.command != current_command: current_command = row.command blob += " " + self.text.make_chatcmd( row.command, "/tell <myname> help " + row.command) return ChatBlob("Help (main)", blob) @command(command="help", params=[Any("command"), NamedParameters(["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 show_regex = named_params.show_regex and named_params.show_regex.lower( ) == "true" help_text = self.command_service.get_help_text(request.sender.char_id, help_topic, request.channel, show_regex) if help_text: return self.command_service.format_help_text(help_topic, help_text) else: return "Could not find help on <highlight>" + help_topic + "<end>."
class PointsController: def __init__(self): pass 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.character_service: CharacterService = registry.get_instance("character_service") self.util: Util = registry.get_instance("util") self.setting_service: SettingService = registry.get_instance("setting_service") self.alts_service: AltsService = registry.get_instance("alts_service") def start(self): self.db.exec("CREATE TABLE IF NOT EXISTS points (char_id BIGINT PRIMARY KEY, points INT DEFAULT 0, created_at INT NOT NULL, " "disabled SMALLINT DEFAULT 0)") self.db.exec("CREATE TABLE IF NOT EXISTS points_log (log_id INT PRIMARY KEY, char_id BIGINT NOT NULL, audit INT NOT NULL, " "leader_id BIGINT NOT NULL, reason VARCHAR(255), created_at INT NOT NULL)") self.db.exec("CREATE TABLE IF NOT EXISTS points_presets (preset_id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, " "points INT DEFAULT 1, UNIQUE(name))") if self.db.query_single("SELECT COUNT(*) AS count FROM points_presets").count < 1: # Populate with pre-made presets if empty presets = ["s13", "s28", "s35", "s42", "zodiac", "zod", "tnh", "beast", "12m", "tara", "pvp", "towers", "wipe", "clanwipe", "clan", "omniwipe", "omni", "bonus", "early"] sql = "INSERT INTO points_presets (name) VALUES (?)" for preset in presets: self.db.exec(sql, [preset]) @command(command="account", params=[Const("create"), Character("char")], access_level="moderator", description="Create a new account for given character name", sub_command="modify") def bank_create_cmd(self, request, _, char: SenderObj): alts_info = self.alts_service.get_alts(char.char_id) for alt in alts_info: sql = "SELECT char_id, disabled FROM points WHERE char_id = ? LIMIT 1" row = self.db.query_single(sql, [alt.char_id]) if row: was_disabled = False if row.disabled == 1: if self.db.exec("UPDATE points SET disabled = 0 WHERE char_id = ?", [alt.char_id]): was_disabled = True if alt.char_id == char.char_id: if was_disabled: self.add_log_entry(alt.char_id, request.sender.char_id, "Account was re-enabled by %s" % request.sender.name) return "<highlight>%s</highlight>'s account has been re-enabled." % char.name else: return "<highlight>%s</highlight> already has an account." % char.name else: if was_disabled: self.add_log_entry(alt.char_id, request.sender.char_id, "Account was re-enabled by %s" % request.sender.name) return "<highlight>%s</highlight>'s (%s) account has been re-enabled." % (char.name, alt.name) else: return "<highlight>%s</highlight> (%s) already has an account." % (char.name, alt.name) main_info = alts_info[0] changed_to_main = main_info.char_id == char.char_id self.create_account(main_info.char_id, request.sender) name_reference = "%s (%s)" % (char.name, main_info.name) if changed_to_main else char.name return "A new account has been created for <highlight>%s</highlight>." % name_reference @command(command="account", params=[Const("close"), Character("char")], access_level="moderator", description="Close the account for given character name", sub_command="modify") def close_account_cmd(self, request, _, char: SenderObj): main = self.alts_service.get_main(char.char_id) sql = "UPDATE points SET disabled = 1 WHERE char_id = ?" if self.db.exec(sql, [main.char_id]) > 0: reason = f"Account was closed by {request.sender.name}" self.add_log_entry(main.char_id, request.sender.char_id, reason) name_reference = "%s (%s)" % (char.name, main.name) if main.char_id != char.char_id else char.name return f"<highlight>{name_reference}</highlight> has had their account disabled. Logs have been preserved." else: return "<highlight>%s</highlight> does not have an open account." % char.name @command(command="account", params=[Const("history"), Int("log_id")], access_level="moderator", description="Look up specific account history record", sub_command="modify") def account_history_cmd(self, request, _, log_id: int): log_entry = self.db.query_single("SELECT log_id, char_id, audit, leader_id, reason, created_at FROM points_log WHERE log_id = ?", [log_id]) if not log_entry: return "No account history record with given ID <highlight>%d</highlight>." % log_id char_name = self.character_service.resolve_char_to_name(log_entry.char_id) leader_name = self.character_service.resolve_char_to_name(log_entry.leader_id) blob = f"ID: <highlight>{log_entry.log_id}</highlight>\n" blob += f"Account: <highlight>{char_name}</highlight>\n" blob += f"Action by: <highlight>{leader_name}</highlight>\n" blob += "Type: <highlight>%s</highlight>\n" % ("Management" if log_entry.audit == 0 else "Altering of points") blob += f"Reason: <highlight>{log_entry.reason}</highlight>\n" blob += f"Audit: <highlight>{log_entry.audit}</highlight>\n" action_links = None if log_entry.audit == 0: if "closed" in log_entry.reason: action_links = self.text.make_tellcmd("Open the account", "account create %s" % char_name) elif "re-enabled" in log_entry.reason: action_links = self.text.make_tellcmd("Close the account", "account close %s" % char_name) else: reason = f"Points from event ({log_id}) have been retracted" if log_entry.audit < 0: action_links = self.text.make_tellcmd("Retract", f"account add {char_name} {-log_entry.audit} {reason}") else: action_links = self.text.make_tellcmd("Retract", f"account rem {char_name} {log_entry.audit} {reason}") blob += "Actions available: [%s]\n" % (action_links if action_links is not None else "No actions available") return ChatBlob(f"Account History Record ({log_id})", blob) @command(command="account", params=[Const("add"), Character("char"), Int("amount"), Any("reason")], access_level="moderator", description="Add points to an account", sub_command="modify") def account_add_cmd(self, request, _, char: SenderObj, amount: int, reason: str): main = self.alts_service.get_main(char.char_id) row = self.get_account(main.char_id, request.conn) if not row: return f"<highlight>{char.name}</highlight> does not have an account." if row.disabled == 1: return f"Account for <highlight>{char.name}</highlight> is disabled and cannot be altered." self.alter_points(main.char_id, request.sender.char_id, reason, amount) return f"<highlight>{char.name}</highlight> has had <highlight>{amount}</highlight> points added to their account." @command(command="account", params=[Options(["rem", "remove"]), Character("char"), Int("amount"), Any("reason")], access_level="moderator", description="Remove points from an account", sub_command="modify") def account_remove_cmd(self, request, _, char: SenderObj, amount: int, reason: str): main = self.alts_service.get_main(char.char_id) row = self.get_account(main.char_id, request.conn) if not row: return f"<highlight>{char.name}</highlight> does not have an account." if row.disabled == 1: return f"Account for <highlight>{char.name}</highlight> is disabled and cannot be altered." if amount > row.points: return f"<highlight>{char.name}</highlight> only has <highlight>{row.points}</highlight> points." self.alter_points(main.char_id, request.sender.char_id, reason, -amount) return f"<highlight>{char.name}</highlight> has had <highlight>{amount}</highlight> points removed from their account." @command(command="account", params=[NamedParameters(["page"])], access_level="all", description="Look up your account") def account_self_cmd(self, request, named_params): return self.get_account_display(request.sender, named_params.page) @command(command="account", params=[Character("char"), NamedParameters(["page"])], access_level="moderator", description="Look up account of another char", sub_command="modify") def account_other_cmd(self, request, char: SenderObj, named_params): return self.get_account_display(char, named_params.page) @command(command="raid", params=[Const("presets"), Const("add"), Any("name"), Int("points")], access_level="admin", description="Add new points preset", sub_command="manage_points") def presets_add_cmd(self, request, _1, _2, name: str, points: int): count = self.db.query_single("SELECT COUNT(*) AS count FROM points_presets WHERE name = ?", [name]).count if count > 0: return "A preset already exists with the name <highlight>%s</highlight>." % name sql = "INSERT INTO points_presets (name, points) VALUES (?,?)" self.db.exec(sql, [name, points]) return "A preset with the name <highlight>%s</highlight> was added, worth <green>%d</green> points." % (name, points) @command(command="raid", params=[Const("presets"), Const("rem"), Int("preset_id")], access_level="admin", description="Delete preset", sub_command="manage_points") def presets_rem_cmd(self, request, _1, _2, preset_id: int): if self.db.exec("DELETE FROM points_presets WHERE preset_id = ?", [preset_id]) > 0: return "Successfully removed preset with ID <highlight>%d</highlight>." % preset_id else: return "No preset with given ID <highlight>%d</highlight>." % preset_id @command(command="raid", params=[Const("presets"), Const("alter"), Int("preset_id"), Int("new_points")], access_level="admin", description="Alter the points dished out by given preset", sub_command="manage_points") def presets_alter_cmd(self, request, _1, _2, preset_id: int, new_points: int): preset = self.db.query_single("SELECT * FROM points_presets WHERE preset_id = ?", [preset_id]) if not preset: return f"Preset with ID <highlight>{preset_id}</highlight> does not exist." self.db.exec("UPDATE points_presets SET points = ? WHERE preset_id = ?", [new_points, preset_id]) return "Successfully updated the preset, <highlight>%s</highlight>, to dish out " \ "<green>%d</green> points instead of <red>%d</red>." % (preset.name, new_points, preset.points) @command(command="raid", params=[Options(["presets", "addpts"])], access_level="member", description="See list of points presets") def presets_cmd(self, request, _): return ChatBlob("Raid Points Presets", self.build_preset_list()) def build_preset_list(self): presets = self.db.query("SELECT * FROM points_presets ORDER BY name ASC, points DESC") if presets: blob = "" for preset in presets: add_points_link = self.text.make_tellcmd("Add pts", "raid addpts %s" % preset.name) blob += "<highlight>%s</highlight> worth <green>%d</green> points %s [id: %d]\n\n" \ % (preset.name, preset.points, add_points_link, preset.preset_id) return blob return "No presets available. To add new presets use <highlight><symbol>presets add preset_name preset_points</highlight>." def add_log_entry(self, char_id: int, leader_id: int, reason: str, amount=0): sql = "INSERT INTO points_log (char_id, audit, leader_id, reason, created_at) VALUES (?,?,?,?,?)" return self.db.exec(sql, [char_id, amount, leader_id, reason, int(time.time())]) def alter_points(self, char_id: int, leader_id: int, reason: str, amount: int): sql = "UPDATE points SET points = points + ? WHERE char_id = ?" self.db.exec(sql, [amount, char_id]) self.add_log_entry(char_id, leader_id, reason, amount) def get_account(self, main_id, conn): sql = "SELECT p.char_id, p.points, p.disabled FROM points p WHERE p.char_id = ?" row = self.db.query_single(sql, [main_id]) if not row: self.create_account(main_id, SenderObj(conn.get_char_id(), conn.get_char_name(), None)) row = self.db.query_single(sql, [main_id]) return row def create_account(self, main_id, sender): sql = "INSERT INTO points (char_id, points, created_at) VALUES (?,?,?)" self.db.exec(sql, [main_id, 0, int(time.time())]) self.add_log_entry(main_id, sender.char_id, "Account opened by %s" % sender.name) def get_account_display(self, char: SenderObj, page): main = self.alts_service.get_main(char.char_id) if not main: return "Could not find character <highlight>%s</highlight>." % char.name page = int(page) if page else 1 page_size = 20 offset = (page - 1) * page_size points = self.db.query_single("SELECT points, disabled FROM points WHERE char_id = ?", [main.char_id]) if not points: return "Could not find raid account for <highlight>%s</highlight>." % char.name alts_link = self.text.make_tellcmd("Alts", "alts %s" % main.name) blob = "" blob += "Account: %s [%s]\n" % (main.name, alts_link) blob += "Points: %d\n" % points.points blob += "Status: %s\n\n" % ("<green>Open</green>" if points.disabled == 0 else "<red>Disabled</red>") points_log = self.db.query("SELECT * FROM points_log WHERE char_id = ? ORDER BY created_at DESC LIMIT ?, ?", [main.char_id, offset, page_size]) blob += "<header2>Account log</header2>\n" if points_log is None: blob += "No entries in log." else: for entry in points_log: if entry.audit > 0: pts = "<green>+%d</green>" % entry.audit blob += "<grey>[%s]</grey> %s points by <highlight>%s</highlight>; <orange>%s</orange>" \ % (self.util.format_datetime(entry.created_at), pts, self.character_service.resolve_char_to_name(entry.leader_id), entry.reason) elif entry.audit < 0: pts = "<red>-%d</red>" % (-1 * entry.audit) blob += "<grey>[%s]</grey> %s points by <highlight>%s</highlight>; <orange>%s</orange>" \ % (self.util.format_datetime(entry.created_at), pts, self.character_service.resolve_char_to_name(entry.leader_id), entry.reason) else: # If points is 0, then it's a general case log blob += "<grey>[%s]</grey> <orange>%s</orange>" % (self.util.format_datetime(entry.created_at), entry.reason) log_entry_link = self.text.make_tellcmd(entry.log_id, f"account history {entry.log_id}") blob += " [%s]\n" % log_entry_link return ChatBlob("%s Account" % char.name, blob)
class RecipeController: def __init__(self): self.recipe_name_regex = re.compile(r"(\d+)\.(txt|json)") self.recipe_item_regex = re.compile(r"#L \"([^\"]+)\" \"([\d+]+)\"") self.recipe_link_regex = re.compile(r"#L \"([^\"]+)\" \"([^\"]+)\"") def inject(self, registry): self.db = registry.get_instance("db") self.text = registry.get_instance("text") self.items_controller = registry.get_instance("items_controller") self.command_alias_service = registry.get_instance( "command_alias_service") def start(self): self.command_alias_service.add_alias("r", "recipe") self.command_alias_service.add_alias("tradeskill", "recipe") recipe_dir = os.path.dirname(os.path.realpath(__file__)) + "/recipes/" recipes = self.db.query("SELECT id, dt FROM recipe") for file in os.listdir(recipe_dir): m = self.recipe_name_regex.match(file) if m: recipe_id = m.group(1) file_type = m.group(2) dt = int(os.path.getmtime(recipe_dir + file)) recipe = self.find_recipe(recipe_id, recipes) if recipe: recipes.remove(recipe) if recipe.dt == dt: continue self.update_recipe(recipe_dir, file, recipe_id, file_type, dt) elif file.startswith("_"): pass else: raise Exception("Unknown recipe format for '%s'" % file) @command(command="recipe", params=[Int("recipe_id")], access_level="all", description="Show a recipe") def recipe_show_cmd(self, request, recipe_id): recipe = self.get_recipe(recipe_id) if not recipe: return "Could not find recipe with ID <highlight>%d<end>." % recipe_id return self.format_recipe(recipe) @command(command="recipe", params=[Any("search"), NamedParameters(["page"])], access_level="all", description="Search for a recipe") def recipe_search_cmd(self, request, search, named_params): page = int(named_params.page or "1") page_size = 30 offset = (page - 1) * page_size data = self.db.query( "SELECT * FROM recipe WHERE recipe <EXTENDED_LIKE=0> ? ORDER BY name ASC", [search], extended_like=True) count = len(data) paged_data = data[offset:offset + page_size] blob = "" if count > page_size: if page > 1 and len(paged_data) > 0: blob += " " + self.text.make_chatcmd( "<< Page %d" % (page - 1), self.get_chat_command(search, page - 1)) if offset + page_size < len(data): blob += " Page " + str(page) blob += " " + self.text.make_chatcmd( "Page %d >>" % (page + 1), self.get_chat_command(search, page + 1)) blob += "\n\n" for row in paged_data: blob += self.text.make_chatcmd( row.name, "/tell <myname> recipe %d" % row.id) + "\n" return ChatBlob( "Recipes Matching '%s' (%d - %d of %d)" % (search, offset + 1, min(offset + page_size, count), count), blob) def get_recipe(self, recipe_id): return self.db.query_single("SELECT * FROM recipe WHERE id = ?", [recipe_id]) def format_recipe(self, recipe): blob = "Recipe ID: <highlight>%d<end>\n" % recipe.id blob += "Author: <highlight>%s<end>\n\n" % (recipe.author or "Unknown") blob += self.format_recipe_text(recipe.recipe) return ChatBlob("Recipe for '%s'" % recipe.name, blob) def format_recipe_text(self, recipe_text): recipe_text = recipe_text.replace("\\n", "\n") recipe_text = self.recipe_item_regex.sub(self.lookup_item, recipe_text) recipe_text = self.recipe_link_regex.sub( "<a href='chatcmd://\\2'>\\1</a>", recipe_text) return recipe_text def lookup_item(self, m): name = m.group(1) item_id = m.group(2) item = self.items_controller.get_by_item_id(item_id) if item: return self.text.make_item(item.lowid, item.highid, item.highql, item.name) else: return name def get_chat_command(self, search, page): return "/tell <myname> recipe %s --page=%d" % (search, page) def find_recipe(self, recipe_id, recipes): for row in recipes: if str(row.id) == recipe_id: return row return None def update_recipe(self, recipe_dir, file, recipe_id, file_type, dt): if file_type == "txt": with open(recipe_dir + file) as f: lines = f.readlines() name = lines.pop(0).strip()[6:] author = lines.pop(0).strip()[8:] content = "".join(lines) #self.db.exec("INSERT INTO recipe (id, name, author, recipe) VALUES (?, ?, ?, ?)", [recipe_id, name, author, content]) elif file_type == "json": with open(recipe_dir + file) as f: recipe = json.load(f) name = recipe["name"] author = recipe["author"] items = {} for i in recipe["items"]: item = self.items_controller.get_by_item_id( i["item_id"], i.get("ql")) if not item: raise Exception( "Could not find recipe item '%d' for recipe id %s" % (i["item_id"], recipe_id)) item.ql = i.get("ql") or (item.highql if i["item_id"] == item.highid else item.lowql) items[i["alias"]] = item content = "<font color=#FFFF00>------------------------------</font>\n" content += "<font color=#FF0000>Ingredients</font>\n" content += "<font color=#FFFF00>------------------------------</font>\n\n" ingredients = items.copy() for step in recipe["steps"]: del ingredients[step["result"]] for _, ingredient in ingredients.items(): content += self.text.make_image(ingredient["icon"]) + "<tab>" content += self.text.make_item( ingredient["lowid"], ingredient["highid"], ingredient["ql"], ingredient["name"]) + "\n" content += "\n" content += "<font color=#FFFF00>------------------------------</font>\n" content += "<font color=#FF0000>Recipe</font>\n" content += "<font color=#FFFF00>------------------------------</font>\n\n" for step in recipe["steps"]: source = items[step["source"]] target = items[step["target"]] result = items[step["result"]] content += "<a href='itemref://%d/%d/%d'>%s</a>" % \ (source["lowid"], source["highid"], source["ql"], self.text.make_image(source["icon"])) + "" content += "<font color=#FFFFFF><tab>+<tab></font> " content += "<a href='itemref://%d/%d/%d'>%s</a>" % \ (target["lowid"], target["highid"], target["ql"], self.text.make_image(target["icon"])) + "" content += "<font color=#FFFFFF><tab>=<tab></font> " content += "<a href='itemref://%d/%d/%d'>%s</a>" % \ (result["lowid"], result["highid"], result["ql"], self.text.make_image(result["icon"])) content += "\n<tab><tab>" + self.text.make_item( source["lowid"], source["highid"], source["ql"], source["name"]) content += "\n + <tab>" + self.text.make_item( target["lowid"], target["highid"], target["ql"], target["name"]) content += "\n = <tab>" + self.text.make_item( result["lowid"], result["highid"], result["ql"], result["name"]) + "\n" if "skills" in step: content += "<font color=#FFFF00>Skills: | %s |</font>\n" % step[ "skills"] content += "\n\n" if "details" in recipe: content += "<font color=#FFFF00>------------------------------</font>\n" content += "<font color=#FF0000>Details</font>\n" content += "<font color=#FFFF00>------------------------------</font>\n\n" last_type = "" for detail in recipe["details"]: if "item" in detail: last_type = "item" i = detail["item"] item = None if "ql" in i: item = self.items_controller.get_by_item_id( i["id"], i["ql"]) else: item = self.items_controller.get_by_item_id( i["id"]) item["ql"] = item["highql"] content += "<font color=#009B00>%s</font>" % \ self.text.make_item(item["lowid"], item["highid"], item["ql"], item["name"]) if "comment" in i: content += " - " + i["comment"] content += "\n" elif "text" in detail: if last_type == "item": content += "\n" last_type = "text" content += "<font color=#FFFFFF>%s</font>\n" % detail[ "text"] self.db.exec( "REPLACE INTO recipe (id, name, author, recipe, dt) VALUES (?, ?, ?, ?, ?)", [recipe_id, name, author, content, dt])
class ItemsController: PAGE_SIZE = 30 def inject(self, registry): self.db: DB = registry.get_instance("db") self.text: Text = registry.get_instance("text") self.command_alias_service = registry.get_instance( "command_alias_service") def pre_start(self): self.db.load_sql_file(self.module_dir + "/" + "aodb.sql") def start(self): self.command_alias_service.add_alias("item", "items") self.command_alias_service.add_alias("i", "items") @command(command="items", params=[Int("item_id")], access_level="all", description="Search for an item by item id") def items_id_cmd(self, request, item_id): item = self.get_by_item_id(item_id) if item: return self.format_items_response(None, str(item_id), [item], 0, 1) else: return "Could not find item with ID <highlight>%d</highlight>." % item_id @command(command="items", params=[ Int("ql", is_optional=True), Any("search"), NamedParameters(["page"]) ], access_level="all", description="Search for an item") def items_search_cmd(self, request, ql, search, named_params): page = int(named_params.page or "1") search = html.unescape(search) offset = (page - 1) * self.PAGE_SIZE all_items = self.find_items(search, ql) return self.format_items_response(ql, search, all_items, offset, page) def format_items_response(self, ql, search, all_items, offset, page_number): items = self.sort_items(search, all_items)[offset:offset + self.PAGE_SIZE] cnt = len(items) if cnt == 0: if ql: return "No QL <highlight>%d</highlight> items found matching <highlight>%s</highlight>." % ( ql, search) else: return "No items found matching <highlight>%s</highlight>." % search elif cnt == 1: item = items[0] return self.format_single_item([item], ql) else: blob = "" # blob += "Version: <highlight>%s</highlight>\n" % "unknown" if ql: blob += "Search: <highlight>QL %d %s</highlight>\n" % (ql, search) else: blob += "Search: <highlight>%s</highlight>\n" % search blob += "\n" blob += self.text.get_paging_links( self.get_chat_command(ql, search), page_number, (offset + self.PAGE_SIZE) < len(all_items)) blob += "\n\n" blob += self.format_items(items, ql) blob += "\nItem DB rips created using the %s tool." % self.text.make_chatcmd( "Budabot Items Extractor", "/start https://github.com/Budabot/ItemsExtractor") return ChatBlob( "Item Search Results (%d - %d of %d)" % (offset + 1, min(offset + self.PAGE_SIZE, len(all_items)), len(all_items)), blob) def format_items(self, items, ql=None): blob = "" for item_group in ItemIter(items): blob += "<pagebreak>" blob += self.text.make_image(item_group[0].icon) + "\n" blob += self.format_single_item(item_group, ql) blob += "\n\n" return blob def format_single_item(self, item_group, ql): msg = "" msg += item_group[0].name for item in reversed(item_group): if ql: if item.lowql != item.highql: msg += " %s" % self.text.make_item(item.lowid, item.highid, ql, ql) msg += " [%s - %s]" % (self.text.make_item( item.lowid, item.highid, item.lowql, item.lowql), self.text.make_item( item.lowid, item.highid, item.highql, item.highql)) elif item.lowql == item.highql: msg += " [%s]" % self.text.make_item( item.lowid, item.highid, item.highql, item.highql) elif item.lowql == item.highql: msg += " [%s]" % self.text.make_item(item.lowid, item.highid, item.highql, item.highql) else: msg += " [%s - %s]" % (self.text.make_item( item.lowid, item.highid, item.lowql, item.lowql), self.text.make_item( item.lowid, item.highid, item.highql, item.highql)) return msg def find_items(self, name, ql=None): params = [name] sql = "SELECT * FROM aodb WHERE name LIKE ? " if ql: sql += " AND lowql <= ? AND highql >= ?" params.append(ql) params.append(ql) sql += " UNION SELECT * FROM aodb WHERE name <EXTENDED_LIKE=%d> ?" % len( params) params.append(name) if ql: sql += " AND lowql <= ? AND highql >= ?" params.append(ql) params.append(ql) sql += " ORDER BY name ASC, highql DESC" return self.db.query(sql, params, extended_like=True) def sort_items(self, search, items): search = search.lower() search_parts = search.split(" ") # if item name matches search exactly (case-insensitive) then priority = 0 # if item name contains every whole word from search (case-insensitive) then priority = 1 # +1 priority for each whole word from search that item name does not contain for row in items: row.priority = 0 row_name = row.name.lower() if row_name != search: row.priority += 1 row_parts = row_name.split(" ") for search_part in search_parts: if search_part not in row_parts: row.priority += 1 items.sort(key=lambda x: x.priority, reverse=False) return items def get_by_item_id(self, item_id, ql=None): if ql: return self.db.query_single( "SELECT * FROM aodb WHERE (highid = ? OR lowid = ?) AND (lowql <= ? AND highql >= ?) ORDER BY highid = ? DESC LIMIT 1", [item_id, item_id, ql, ql, item_id]) else: return self.db.query_single( "SELECT * FROM aodb WHERE highid = ? OR lowid = ? ORDER BY highid = ? DESC LIMIT 1", [item_id, item_id, item_id]) def find_by_name(self, name, ql=None): if ql: return self.db.query_single( "SELECT * FROM aodb WHERE name = ? AND lowql <= ? AND highql >= ? ORDER BY highid DESC LIMIT 1", [name, ql, ql]) else: return self.db.query_single( "SELECT * FROM aodb WHERE name = ? ORDER BY highql DESC, highid DESC LIMIT 1", [name]) def get_chat_command(self, ql, search): if ql: return "items %d %s" % (ql, search) else: return "items %s" % search
class RecipeController: def __init__(self): self.logger = Logger(__name__) self.recipe_name_regex = re.compile(r"(\d+)\.(txt|json)") self.recipe_item_regex = re.compile(r"#L \"([^\"]+)\" \"([\d+]+)\"") self.recipe_link_regex = re.compile(r"#L \"([^\"]+)\" \"([^\"]+)\"") def inject(self, registry): self.db = registry.get_instance("db") self.text = registry.get_instance("text") self.items_controller = registry.get_instance("items_controller") self.command_alias_service = registry.get_instance( "command_alias_service") def start(self): self.command_alias_service.add_alias("r", "recipe") self.command_alias_service.add_alias("tradeskill", "recipe") self.db.exec( "CREATE TABLE IF NOT EXISTS recipe (id INT NOT NULL PRIMARY KEY, name VARCHAR(50) NOT NULL, author VARCHAR(50) NOT NULL, recipe TEXT NOT NULL, dt INT NOT NULL DEFAULT 0)" ) recipe_dir = os.path.dirname(os.path.realpath(__file__)) + "/recipes/" recipes = self.db.query("SELECT id, dt FROM recipe") for file in os.listdir(recipe_dir): if file.startswith("_"): continue m = self.recipe_name_regex.match(file) if m: recipe_id = m.group(1) file_type = m.group(2) dt = int(os.path.getmtime(recipe_dir + file)) # convert txt format to json if file_type == "txt": if self.convert_to_json(recipe_dir, recipe_id, file): file_type = "json" dt = int(time.time()) recipe = self.find_recipe(recipe_id, recipes) if recipe: recipes.remove(recipe) if recipe.dt == dt: continue self.update_recipe(recipe_dir, recipe_id, file_type, dt) else: raise Exception("Unknown recipe format for '%s'" % file) @command(command="recipe", params=[Int("recipe_id")], access_level="all", description="Show a recipe") def recipe_show_cmd(self, request, recipe_id): recipe = self.get_recipe(recipe_id) if not recipe: return "Could not find recipe with ID <highlight>%d</highlight>." % recipe_id return self.format_recipe(recipe) @command(command="recipe", params=[Item("item"), NamedParameters(["page"])], access_level="all", description="Search for a recipe") def recipe_search_item_cmd(self, request, item, named_params): page_number = int(named_params.page or "1") return self.get_search_results_blob(item.name, page_number) @command(command="recipe", params=[Any("search"), NamedParameters(["page"])], access_level="all", description="Search for a recipe") def recipe_search_cmd(self, request, search, named_params): page_number = int(named_params.page or "1") return self.get_search_results_blob(search, page_number) def get_search_results_blob(self, search, page_number): page_size = 30 offset = (page_number - 1) * page_size data = self.db.query( "SELECT * FROM recipe WHERE recipe <EXTENDED_LIKE=0> ? ORDER BY name ASC", [search], extended_like=True) count = len(data) paged_data = data[offset:offset + page_size] blob = "" if len(data) > 0: blob += self.text.get_paging_links(f"recipe {search}", page_number, offset + page_size < len(data)) blob += "\n\n" for row in paged_data: blob += self.text.make_tellcmd(row.name, "recipe %d" % row.id) + "\n" return ChatBlob( "Recipes Matching '%s' (%d - %d of %d)" % (search, offset + 1, min(offset + page_size, count), count), blob) def get_recipe(self, recipe_id): return self.db.query_single("SELECT * FROM recipe WHERE id = ?", [recipe_id]) def format_recipe(self, recipe): blob = "Recipe ID: <highlight>%d</highlight>\n" % recipe.id blob += "Author: <highlight>%s</highlight>\n\n" % (recipe.author or "Unknown") blob += self.format_recipe_text(recipe.recipe) return ChatBlob("Recipe for '%s'" % recipe.name, blob) def format_recipe_text(self, recipe_text): recipe_text = recipe_text.replace("\\n", "\n") recipe_text = self.recipe_item_regex.sub(self.lookup_item, recipe_text) recipe_text = self.recipe_link_regex.sub( "<a href='chatcmd://\\2'>\\1</a>", recipe_text) return recipe_text def lookup_item(self, m): name = m.group(1) item_id = m.group(2) item = self.items_controller.get_by_item_id(item_id) if item: return self.text.make_item(item.lowid, item.highid, item.highql, item.name) else: return name def get_chat_command(self, search, page): return "/tell <myname> recipe %s --page=%d" % (search, page) def find_recipe(self, recipe_id, recipes): for row in recipes: if str(row.id) == recipe_id: return row return None def update_recipe(self, recipe_dir, recipe_id, file_type, dt): with open(recipe_dir + recipe_id + ".json", mode="r", encoding="UTF-8") as f: recipe = json.load(f) name = recipe["name"] author = recipe["author"] if "raw" in recipe: content = recipe["raw"] else: content = self.format_json_recipe(recipe_id, recipe) self.db.exec( "REPLACE INTO recipe (id, name, author, recipe, dt) VALUES (?, ?, ?, ?, ?)", [recipe_id, name, author, content, dt]) def format_json_recipe(self, recipe_id, recipe): items = {} for i in recipe["items"]: item = self.items_controller.get_by_item_id( i["item_id"], i.get("ql")) if not item: raise Exception( "Could not find recipe item '%d' for recipe id %s" % (i["item_id"], recipe_id)) item.ql = i.get("ql") or (item.highql if i["item_id"] == item.highid else item.lowql) items[i["alias"]] = item content = "" ingredients = items.copy() for step in recipe["steps"]: del ingredients[step["result"]] content += self.format_ingredients(ingredients.items()) content += "\n" content += self.format_steps(items, recipe["steps"]) if "details" in recipe: content += self.format_details(recipe["details"]) return content def format_ingredients(self, ingredients): content = "<font color=#FFFF00>------------------------------</font>\n" content += "<font color=#FF0000>Ingredients</font>\n" content += "<font color=#FFFF00>------------------------------</font>\n\n" for _, ingredient in ingredients: content += self.text.make_image(ingredient["icon"]) + "<tab>" content += self.text.make_item( ingredient["lowid"], ingredient["highid"], ingredient["ql"], ingredient["name"]) + "\n" return content def format_steps(self, items, steps): content = "" content += "<font color=#FFFF00>------------------------------</font>\n" content += "<font color=#FF0000>Recipe</font>\n" content += "<font color=#FFFF00>------------------------------</font>\n\n" for step in steps: source = items[step["source"]] target = items[step["target"]] result = items[step["result"]] content += "<a href='itemref://%d/%d/%d'>%s</a>" % \ (source["lowid"], source["highid"], source["ql"], self.text.make_image(source["icon"])) + "" content += "<font color=#FFFFFF><tab>+<tab></font> " content += "<a href='itemref://%d/%d/%d'>%s</a>" % \ (target["lowid"], target["highid"], target["ql"], self.text.make_image(target["icon"])) + "" content += "<font color=#FFFFFF><tab>=<tab></font> " content += "<a href='itemref://%d/%d/%d'>%s</a>" % \ (result["lowid"], result["highid"], result["ql"], self.text.make_image(result["icon"])) content += "\n<tab><tab>" + self.text.make_item( source["lowid"], source["highid"], source["ql"], source["name"]) content += "\n + <tab>" + self.text.make_item( target["lowid"], target["highid"], target["ql"], target["name"]) content += "\n = <tab>" + self.text.make_item( result["lowid"], result["highid"], result["ql"], result["name"]) + "\n" if "skills" in step: content += "<font color=#FFFF00>Skills: | %s |</font>\n" % step[ "skills"] content += "\n\n" return content def format_details(self, details): content = "" content += "<font color=#FFFF00>------------------------------</font>\n" content += "<font color=#FF0000>Details</font>\n" content += "<font color=#FFFF00>------------------------------</font>\n\n" last_type = "" for detail in details: if "item" in detail: last_type = "item" i = detail["item"] item = None if "ql" in i: item = self.items_controller.get_by_item_id( i["id"], i["ql"]) else: item = self.items_controller.get_by_item_id(i["id"]) item["ql"] = item["highql"] content += "<font color=#009B00>%s</font>" % \ self.text.make_item(item["lowid"], item["highid"], item["ql"], item["name"]) if "comment" in i: content += " - " + i["comment"] content += "\n" elif "text" in detail: if last_type == "item": content += "\n" last_type = "text" content += "<font color=#FFFFFF>%s</font>\n" % detail["text"] return content def convert_to_json(self, recipe_dir, recipe_id, file): with open(recipe_dir + file, mode="r", encoding="UTF-8") as f: lines = f.readlines() recipe = { "name": lines.pop(0).strip()[6:], "author": lines.pop(0).strip()[8:], "items": list(), "steps": list(), "details": list(), "raw": None } content = "".join(lines) items = {} matches = self.recipe_item_regex.findall(content) for item_name, item_id in matches: item = self.items_controller.get_by_item_id(item_id) if not item: self.logger.warning( "Could not find recipe item '%s - %s' for recipe id %s" % (item_id, item_name, recipe_id)) else: items[item.highid] = { "alias": item.name, "item_id": item.highid } recipe["items"].extend(items.values()) recipe["raw"] = content with open(recipe_dir + recipe_id + ".json", mode="w", encoding="UTF-8") as f: f.write(json.dumps(recipe, indent=4)) # delete file os.remove(recipe_dir + file) return True
class TowerAttackController: def __init__(self): self.logger = Logger(__name__) def inject(self, registry): self.bot = registry.get_instance("bot") self.db = registry.get_instance("db") self.text = registry.get_instance("text") self.util = registry.get_instance("util") self.event_service = registry.get_instance("event_service") self.playfield_controller = registry.get_instance( "playfield_controller") self.command_alias_service = registry.get_instance( "command_alias_service") self.public_channel_service = registry.get_instance( "public_channel_service") def start(self): self.db.exec( "CREATE TABLE IF NOT EXISTS tower_attacker (id INT PRIMARY KEY AUTO_INCREMENT, att_org_name VARCHAR(50) NOT NULL, att_faction VARCHAR(10) NOT NULL, " "att_char_id INT, att_char_name VARCHAR(20) NOT NULL, att_level INT NOT NULL, att_ai_level INT NOT NULL, att_profession VARCHAR(15) NOT NULL, " "x_coord INT NOT NULL, y_coord INT NOT NULL, is_victory SMALLINT NOT NULL, " "tower_battle_id INT NOT NULL, created_at INT NOT NULL)") self.db.exec( "CREATE TABLE IF NOT EXISTS tower_battle (id INT PRIMARY KEY AUTO_INCREMENT, playfield_id INT NOT NULL, site_number INT NOT NULL, " "def_org_name VARCHAR(50) NOT NULL, def_faction VARCHAR(10) NOT NULL, is_finished INT NOT NULL, battle_type VARCHAR(20) NOT NULL, last_updated INT NOT NULL)" ) self.command_alias_service.add_alias("victory", "attacks") @command(command="attacks", params=[NamedParameters(["page"])], access_level="all", description="Show recent tower attacks and victories") def attacks_cmd(self, request, named_params): page = int(named_params.page or "1") page_size = 30 offset = (page - 1) * page_size sql = """ SELECT b.*, a.*, COALESCE(a.att_level, 0) AS att_level, COALESCE(a.att_ai_level, 0) AS att_ai_level, p.short_name, b.id AS battle_id FROM tower_battle b LEFT JOIN tower_attacker a ON a.tower_battle_id = b.id LEFT JOIN playfields p ON p.id = b.playfield_id ORDER BY b.last_updated DESC, a.created_at DESC LIMIT %d, %d """ % (offset, page_size) data = self.db.query(sql) t = int(time.time()) blob = self.check_for_all_towers_channel() if page > 1: blob += " " + self.text.make_chatcmd( "<< Page %d" % (page - 1), self.get_chat_command(page - 1)) if len(data) > 0: blob += " Page " + str(page) blob += " " + self.text.make_chatcmd( "Page %d >>" % (page + 1), self.get_chat_command(page + 1)) blob += "\n" current_battle_id = -1 for row in data: if current_battle_id != row.battle_id: blob += "\n<pagebreak>" current_battle_id = row.battle_id blob += self.format_battle_info(row, t) blob += self.text.make_chatcmd( "More Info", "/tell <myname> attacks battle %d" % row.battle_id) + "\n" blob += "<header2>Attackers:<end>\n" blob += "<tab>" + self.format_attacker(row) + "\n" return ChatBlob("Tower Attacks", blob) @command(command="attacks", params=[Const("battle"), Int("battle_id")], access_level="all", description="Show battle info for a specific battle") def attacks_battle_cmd(self, request, _, battle_id): battle = self.db.query_single( "SELECT b.*, p.short_name FROM tower_battle b LEFT JOIN playfields p ON p.id = b.playfield_id WHERE b.id = ?", [battle_id]) if not battle: return "Could not find battle with ID <highlight>%d<end>." % battle_id t = int(time.time()) attackers = self.db.query( "SELECT * FROM tower_attacker WHERE tower_battle_id = ? ORDER BY created_at DESC", [battle_id]) first_activity = attackers[-1].created_at if len( attackers) > 0 else battle.last_updated blob = self.check_for_all_towers_channel() blob += self.format_battle_info(battle, t) blob += "Duration: <highlight>%s<end>\n\n" % self.util.time_to_readable( battle.last_updated - first_activity) blob += "<header2>Attackers:<end>\n" for row in attackers: blob += "<tab>" + self.format_attacker(row) blob += " " + self.format_timestamp(row.created_at, t) blob += "\n" return ChatBlob("Battle Info %d" % battle_id, blob) @event(event_type=TowerController.TOWER_ATTACK_EVENT, description="Record tower attacks", is_hidden=True) def tower_attack_event(self, event_type, event_data): t = int(time.time()) site_number = self.find_closest_site_number( event_data.location.playfield.id, event_data.location.x_coord, event_data.location.y_coord) attacker = event_data.attacker or {} defender = event_data.defender battle = self.find_or_create_battle(event_data.location.playfield.id, site_number, defender.org_name, defender.faction, "attack", t) self.db.exec( "INSERT INTO tower_attacker (att_org_name, att_faction, att_char_id, att_char_name, att_level, att_ai_level, att_profession, " "x_coord, y_coord, is_victory, tower_battle_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ attacker.get("org_name", ""), attacker.get("faction", ""), attacker.get("char_id", 0), attacker.get("name", ""), attacker.get("level", 0), attacker.get("ai_level", 0), attacker.get("profession", ""), event_data.location.x_coord, event_data.location.y_coord, 0, battle.id, t ]) @event(event_type=TowerController.TOWER_VICTORY_EVENT, description="Record tower victories", is_hidden=True) def tower_victory_event(self, event_type, event_data): t = int(time.time()) if event_data.type == "attack": row = self.get_last_attack(event_data.winner.faction, event_data.winner.org_name, event_data.loser.faction, event_data.loser.org_name, event_data.location.playfield.id, t) if not row: site_number = 0 is_finished = 1 self.db.exec( "INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)", [ event_data.location.playfield.id, site_number, event_data.loser.org_name, event_data.loser.faction, is_finished, event_data.type, t ]) battle_id = self.db.last_insert_id() attacker = event_data.winner or {} self.db.exec( "INSERT INTO tower_attacker (att_org_name, att_faction, att_char_id, att_char_name, att_level, att_ai_level, att_profession, " "x_coord, y_coord, is_victory, tower_battle_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ attacker.get("org_name", ""), attacker.get("faction", ""), attacker.get("char_id", 0), attacker.get("name", ""), attacker.get("level", 0), attacker.get("ai_level", 0), attacker.get("profession", ""), 0, 0, 0, battle_id, t ]) else: is_victory = 1 self.db.exec( "UPDATE tower_attacker SET is_victory = ? WHERE id = ?", [is_victory, row.attack_id]) is_finished = 1 self.db.exec( "UPDATE tower_battle SET is_finished = ?, last_updated = ? WHERE id = ?", [is_finished, t, row.battle_id]) elif event_data.type == "terminated": site_number = 0 is_finished = 1 self.db.exec( "INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)", [ event_data.location.playfield.id, site_number, event_data.loser.org_name, event_data.loser.faction, is_finished, event_data.type, t ]) else: raise Exception("Unknown victory event type: '%s'" % event_data.type) def format_attacker(self, row): if row.att_char_name: level = ("%d/<green>%d<end>" % (row.att_level, row.att_ai_level) ) if row.att_ai_level > 0 else "%d" % row.att_level org = row.att_org_name + " " if row.att_org_name else "" victor = " - <notice>Winner!<end>" if row.is_victory else "" return "%s (%s %s) %s(%s)%s" % (row.att_char_name, level, row.att_profession, org, row.att_faction, victor) else: return "Unknown attacker" def find_closest_site_number(self, playfield_id, x_coord, y_coord): sql = """ SELECT site_number, ((x_distance * x_distance) + (y_distance * y_distance)) radius FROM (SELECT playfield_id, site_number, min_ql, max_ql, x_coord, y_coord, site_name, (x_coord - ?) as x_distance, (y_coord - ?) as y_distance FROM tower_site WHERE playfield_id = ?) t ORDER BY radius ASC LIMIT 1""" row = self.db.query_single(sql, [x_coord, y_coord, playfield_id]) if row: return row.site_number else: return 0 def find_or_create_battle(self, playfield_id, site_number, org_name, faction, battle_type, t): last_updated = t - (8 * 3600) is_finished = 0 sql = """ SELECT * FROM tower_battle WHERE playfield_id = ? AND site_number = ? AND is_finished = ? AND def_org_name = ? AND def_faction = ? AND last_updated >= ? """ battle = self.db.query_single(sql, [ playfield_id, site_number, is_finished, org_name, faction, last_updated ]) if battle: self.db.exec( "UPDATE tower_battle SET last_updated = ? WHERE id = ?", [t, battle.id]) return battle else: self.db.exec( "INSERT INTO tower_battle (playfield_id, site_number, def_org_name, def_faction, is_finished, battle_type, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?)", [ playfield_id, site_number, org_name, faction, is_finished, battle_type, t ]) return self.db.query_single( "SELECT * FROM tower_battle WHERE id = ?", [self.db.last_insert_id()]) def get_last_attack(self, att_faction, att_org_name, def_faction, def_org_name, playfield_id, t): last_updated = t - (8 * 3600) is_finished = 0 sql = """ SELECT b.id AS battle_id, a.id AS attack_id FROM tower_battle b JOIN tower_attacker a ON a.tower_battle_id = b.id WHERE a.att_faction = ? AND a.att_org_name = ? AND b.def_faction = ? AND b.def_org_name = ? AND b.playfield_id = ? AND b.is_finished = ? AND b.last_updated >= ? ORDER BY last_updated DESC LIMIT 1""" return self.db.query_single(sql, [ att_faction, att_org_name, def_faction, def_org_name, playfield_id, is_finished, last_updated ]) def format_battle_info(self, row, t): blob = "" defeated = " - <notice>Defeated!<end>" if row.is_finished else "" blob += "Site: <highlight>%s %s<end>\n" % (row.short_name, row.site_number or "?") blob += "Defender: <highlight>%s<end> (%s)%s\n" % ( row.def_org_name, row.def_faction, defeated) blob += "Last Activity: %s\n" % self.format_timestamp( row.last_updated, t) return blob def format_timestamp(self, t, current_t): return "<highlight>%s<end> (%s ago)" % (self.util.format_datetime( t), self.util.time_to_readable(current_t - t)) def get_chat_command(self, page): return "/tell <myname> attacks --page=%d" % page def check_for_all_towers_channel(self): if not self.public_channel_service.get_channel_name( TowerController.ALL_TOWERS_ID): return "Notice: The bot must belong to an org and be promoted to a rank that is high enough to have the All Towers channel (e.g., Squad Commander) in order for the <symbol>attacks command to work correctly.\n\n" else: return ""
class RaidController: NO_RAID_RUNNING_RESPONSE = "No raid is running." def __init__(self): self.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.character_service: CharacterService = registry.get_instance("character_service") self.points_controller: PointsController = registry.get_instance("points_controller") self.util: Util = registry.get_instance("util") @setting(name="default_min_lvl", value="1", description="Default minimum level for joining raids") def default_min_lvl(self): return NumberSettingType() @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 blob = "" blob += "Name: <highlight>%s<end>\n" % self.raid.raid_name blob += "Started By: <highlight>%s<end>\n" % self.raid.started_by.name blob += "Started At: <highlight>%s<end> (%s ago)\n" % (self.util.format_datetime(self.raid.started_at), self.util.time_to_readable(int(time.time()) - self.raid.started_at)) blob += "Minimum Level: <highlight>%d<end>\n" % self.raid.raid_min_lvl blob += "Maximum Characters: <highlight>%s<end>\n" % (self.raid.raid_limit or "No Limit") blob += "Status: %s" % ("<green>Open<end>" if self.raid.is_open else "<red>Closed<end>") if self.raid.is_open: blob += " (%s)" % self.text.make_chatcmd("Join", "/tell <myname> raid join") blob += "\n\n" if self.raid.raid_orders: blob += "<header2>Orders<end>\n" blob += self.raid.raid_orders + "\n\n" blob += "<header2>Raiders<end>\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"), NamedParameters(["num_characters", "min_level"])], description="Start new raid", access_level="moderator", sub_command="manage") def raid_start_cmd(self, request, _, raid_name: str, named_params): if self.raid: return "The raid, <yellow>%s<end>, is already running." % self.raid.raid_name if named_params.min_level: raid_min_lvl = int(named_params.min_level) else: raid_min_lvl = self.setting_service.get("default_min_lvl").get_value() if named_params.num_characters: num_characters = int(named_params.num_characters) else: num_characters = None self.raid = Raid(raid_name, request.sender, raid_min_lvl, num_characters) 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.get_raid_join_blob("Click here") msg = "\n<yellow>----------------------------------------<end>\n" msg += "<yellow>%s<end> has just started the raid <yellow>%s<end>.\n" % (request.sender.name, raid_name) msg += "Raider limit: <highlight>%s<end>\n" % self.raid.raid_limit msg += "Min lvl: <highlight>%d<end>\n" % self.raid.raid_min_lvl msg += "%s to join\n" % join_link msg += "<yellow>----------------------------------------<end>" self.bot.send_org_message(msg) self.bot.send_private_channel_message(msg) @command(command="raid", params=[Options(["end", "cancel"])], description="End 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 raid_name = self.raid.raid_name self.raid = None self.bot.send_org_message("%s canceled the raid <yellow>%s<end> prematurely." % (request.sender.name, raid_name)) self.bot.send_private_channel_message("%s canceled the raid <yellow>%s<end> prematurely." % (request.sender.name, raid_name)) @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) player_level = self.db.query_single("SELECT level FROM player WHERE char_id = ?", [request.sender.char_id]) player_level = player_level.level \ if player_level is not None \ else self.setting_service.get("default_min_lvl").get_value() if player_level < self.raid.raid_min_lvl: return "Your level (%d) does not meet the requirements of the raid (%d)." % (player_level, self.raid.raid_min_lvl) 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.bot.send_private_channel_message("%s returned to actively participating in the raid." % request.sender.name) self.bot.send_org_message("%s returned to actively participating in the raid." % request.sender.name) 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.bot.send_private_channel_message("%s joined the raid with a different alt, <yellow>%s<end>." % (former_active_name, request.sender.name)) self.bot.send_org_message("%s joined the raid with a different alt, <yellow>%s<end>." % (former_active_name, request.sender.name)) elif not in_raid.is_active: if not self.raid.is_open: return "Raid is closed." 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.bot.send_private_channel_message("%s returned to actively participate with a different alt, <yellow>%s<end>." % (former_active_name, request.sender.name)) self.bot.send_org_message("%s returned to actively participate with a different alt, <yellow>%s<end>." % (former_active_name, request.sender.name)) elif self.raid.raid_limit is not None and len(self.raid.raiders) >= self.raid.raid_limit: return "Raid is full." 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.bot.send_private_channel_message("<yellow>%s<end> joined the raid." % request.sender.name) self.bot.send_org_message("<yellow>%s<end> joined the raid." % request.sender.name) 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, _): in_raid = self.is_in_raid(self.alts_service.get_main(request.sender.char_id).char_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.bot.send_private_channel_message("<yellow>%s<end> left the raid." % request.sender.name) self.bot.send_org_message("<yellow>%s<end> left the raid." % request.sender.name) 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 * FROM points_presets WHERE name = ?", [name]) if not preset: return ChatBlob("No such preset - see list of presets", self.points_controller.build_preset_list()) for raider in self.raid.raiders: current_points = self.db.query_single("SELECT points, disabled FROM points WHERE char_id = ?", [raider.main_id]) if raider.is_active: if current_points and current_points.disabled == 0: self.points_controller.alter_points(current_points.points, raider.main_id, preset.points, request.sender.char_id, preset.name) raider.accumulated_points += preset.points else: self.points_controller.add_log_entry(raider.main_id, request.sender.char_id, "Participated in raid without an open account, " "missed points from %s." % preset.name) else: if current_points: self.points_controller.add_log_entry(raider.main_id, request.sender.char_id, "Was inactive during raid, %s, when points " "for %s was dished out." % (self.raid.raid_name, 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 " "was dished out - did not have an active account at " "the given time." % (self.raid.raid_name, preset.name)) return "<green>%d<end> points added to all active raiders." % preset.points @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 += "[<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_chatcmd("Active kick", "/tell <myname> raid kick %s inactive" % raider.main_id) warn_link = self.text.make_chatcmd("Warn", "/tell <myname> cmd %s missed active check, please give notice." % raider_name) blob += "<highlight>%s<end> [%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 += "[<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("kick"), Character("char"), Any("reason")], description="Set raider as kicked with a reason", access_level="moderator", sub_command="manage") def raid_kick_cmd(self, _1, _2, 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) try: int(char.name) char.name = self.character_service.resolve_char_to_name(char.name) except ValueError: pass if in_raid is not None: if not in_raid.is_active: return "%s 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 return "%s has been kicked from the raid with reason \"%s\"" % (char.name, reason) else: return "%s is not participating." % char.name @command(command="raid", params=[Options(["open", "close"])], description="Open/close raid for new participants", access_level="moderator", sub_command="manage") def raid_open_close_cmd(self, request, action): if not self.raid: return self.NO_RAID_RUNNING_RESPONSE if action == "open": if self.raid.is_open: return "Raid is already open." self.raid.is_open = True self.bot.send_private_channel_message("Raid has been opened by %s." % request.sender.name) self.bot.send_org_message("Raid has been opened by %s." % request.sender.name) return elif action == "close": if self.raid.is_open: self.raid.is_open = False self.bot.send_private_channel_message("Raid has been closed by %s." % request.sender.name) self.bot.send_org_message("Raid has been closed by %s." % request.sender.name) return return "Raid is already closed." @command(command="raid", params=[Const("save")], description="Save and log running raid", access_level="moderator", sub_command="manage") def raid_save_cmd(self, _1, _2): if not self.raid: return self.NO_RAID_RUNNING_RESPONSE sql = "INSERT INTO raid_log (raid_name, started_by, raid_limit, raid_min_lvl, " \ "raid_start, raid_end) VALUES (?,?,?,?,?,?)" num_rows = self.db.exec(sql, [self.raid.raid_name, self.raid.started_by.char_id, self.raid.raid_limit, self.raid.raid_min_lvl, self.raid.started_at, int(time.time())]) if num_rows > 0: raid_id = self.db.query_single("SELECT raid_id FROM raid_log ORDER BY raid_id DESC LIMIT 1").raid_id with_errors = len(self.raid.raiders) 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 (?,?,?,?,?,?)" with_errors -= self.db.exec(sql, [raid_id, raider.active_id, raider.accumulated_points, raider.left_raid, raider.was_kicked, raider.was_kicked_reason]) self.raid = None return "Raid saved%s." % ("" if with_errors == 0 else " with %d errors when logging participants" % with_errors) else: return "Failed to log raid. Try again or cancel raid to end raid." @command(command="raid", params=[Const("logentry"), Int("raid_id"), Character("char", is_optional=True)], description="Show log entry for raid, with possibility of narrowing down the log for character in raid", access_level="moderator", sub_command="manage") def raid_log_entry_cmd(self, _1, _2, raid_id: int, char: SenderObj): log_entry_spec = None if char: sql = "SELECT * FROM raid_log r LEFT JOIN raid_log_participants p ON r.raid_id = p.raid_id WHERE r.raid_id = ? AND p.raider_id = ?" log_entry_spec = self.db.query_single(sql, [raid_id, char.char_id]) sql = "SELECT * FROM raid_log r LEFT JOIN raid_log_participants p ON r.raid_id = p.raid_id WHERE r.raid_id = ? ORDER BY p.accumulated_points DESC" log_entry = self.db.query(sql, [raid_id]) pts_sum = self.db.query_single("SELECT SUM(p.accumulated_points) AS sum FROM raid_log_participants p WHERE p.raid_id = ?", [raid_id]).sum if not log_entry: return "No such log entry." blob = "Raid name: <highlight>%s<end>\n" % log_entry[0].raid_name blob += "Raid limit: <highlight>%s<end>\n" % log_entry[0].raid_limit blob += "Raid min lvl: <highlight>%s<end>\n" % log_entry[0].raid_min_lvl blob += "Started by: <highlight>%s<end>\n" % self.character_service.resolve_char_to_name(log_entry[0].started_by) blob += "Start time: <highlight>%s<end>\n" % self.util.format_datetime(log_entry[0].raid_start) blob += "End time: <highlight>%s<end>\n" % self.util.format_datetime(log_entry[0].raid_end) blob += "Run time: <highlight>%s<end>\n" % self.util.time_to_readable(log_entry[0].raid_end - log_entry[0].raid_start) blob += "Total points: <highlight>%d<end>\n\n" % pts_sum if char and log_entry_spec: raider_name = self.character_service.resolve_char_to_name(log_entry_spec.raider_id) main_info = self.alts_service.get_main(log_entry_spec.raider_id) alt_link = "Alt of %s" % main_info.name if main_info.char_id != log_entry_spec.raider_id else "Alts" alt_link = self.text.make_chatcmd(alt_link, "/tell <myname> alts %s" % main_info.name) blob += "<header2>Log entry for %s<end>\n" % raider_name blob += "Raider: <highlight>%s<end> [%s]\n" % (raider_name, alt_link) blob += "Left raid: %s\n" % ("n/a" if log_entry_spec.left_raid is None else self.util.format_datetime(log_entry_spec.left_raid)) blob += "Was kicked: %s\n" % ("No" if log_entry_spec.was_kicked is None else "Yes [%s]" % (self.util.format_datetime(log_entry_spec.was_kicked))) blob += "Kick reason: %s\n\n" % ("n/a" if log_entry_spec.was_kicked_reason is None else log_entry_spec.was_kicked_reason) blob += "<header2>Participants<end>\n" for raider in log_entry: raider_name = self.character_service.resolve_char_to_name(raider.raider_id) main_info = self.alts_service.get_main(raider.raider_id) alt_link = "Alt of %s" % main_info.name if main_info.char_id != raider.raider_id else "Alts" alt_link = self.text.make_chatcmd(alt_link, "/tell <myname> alts %s" % main_info.name) log_link = self.text.make_chatcmd("Log", "/tell <myname> raid logentry %d %s" % (raid_id, raider_name)) account_link = self.text.make_chatcmd("Account", "/tell <myname> account %s" % raider_name) blob += "%s - %d points earned [%s] [%s] [%s]\n" % (raider_name, raider.accumulated_points, log_link, account_link, alt_link) log_entry_reference = "the raid %s" % log_entry[0].raid_name \ if char is None \ else "%s in raid %s" \ % (self.character_service.resolve_char_to_name(char.char_id), log_entry[0].raid_name) return ChatBlob("Log entry for %s" % log_entry_reference, blob) @command(command="raid", params=[Const("history")], description="Show a list of recent raids", access_level="member") def raid_history_cmd(self, _1, _2): 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_chatcmd("Log", "/tell <myname> raid logentry %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] <orange>%s<end> started by <yellow>%s<end> [%s]\n" % (raid.raid_id, timestamp, raid.raid_name, leader_name, participant_link) return ChatBlob("Raid history", blob) 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, link_txt: str): blob = "<header2>1. Join the raid<end>\n" \ "To join the current raid <yellow>%s<end>, 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<end>\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<end>\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<end>\nFinally, move towards the starting location of " \ "the raid.\n<highlight>Ask for help<end> if you're in doubt of where to go." % self.raid.raid_name return self.text.paginate(ChatBlob(link_txt, blob), 5000, 1)[0]