def test_named_flag_parameters(self): param = NamedFlagParameters(["test1", "test2", "test3"]) self.assertEqual({'test1': True, 'test2': True, 'test3': True}, self.param_test_helper(param, "--test1 --test2 --test3")) self.assertEqual({'test1': True, 'test2': True, 'test3': True}, self.param_test_helper(param, "--test3 --test2 --test1")) self.assertEqual({'test1': False, 'test2': True, 'test3': False}, self.param_test_helper(param, "--test2")) self.assertEqual({'test1': False, 'test2': True, 'test3': True}, self.param_test_helper(param, "--test2 --test3")) self.assertEqual({'test1': True, 'test2': True, 'test3': False}, self.param_test_helper(param, "--test2 --test1")) self.assertIsNone(self.param_test_helper(param, ""))
class TimeController: def __init__(self): self.time_format = "%Y-%m-%d %H:%M:%S %Z%z" @command(command="time", params=[NamedFlagParameters(["all_timezones"])], access_level="all", description="Show the current time") def time_cmd(self, request, flag_params): dt = datetime.now() t = int(time.time()) if not flag_params.all_timezones: return "The current time is <highlight>%s</highlight> [%d]." % ( dt.astimezone(pytz.utc).strftime("%Y-%m-%d %H:%M:%S %Z"), t) else: blob = "Unixtime => %d\n\n" % t current_region = "" for tz in pytz.common_timezones: result = tz.split("/", 2) if len(result) == 2: region, city = result else: region = result[0] city = result[0] if current_region != region: blob += "\n<pagebreak><header2>%s</header2>\n" % region current_region = region blob += "%s => %s\n" % (city, dt.astimezone( pytz.timezone(tz)).strftime(self.time_format)) return ChatBlob("Timezones", blob) @command(command="time", params=[Any("timezone")], access_level="all", description="Show time for the specified timezone") def time_zone_cmd(self, request, timezone_str): timezone_str = timezone_str.lower() for tz in pytz.common_timezones: if tz.lower() == timezone_str: return "%s => %s" % (tz, datetime.now( tz=pytz.timezone(tz)).strftime(self.time_format)) return f"Unknown timezone <highlight>{timezone_str}</highlight>. Use <highlight><symbol>time --all_timezones</highlight> to see a list of timezones."
class CloakController: MESSAGE_SOURCE = "cloak_reminder" CLOAK_EVENT = "cloak" CLOAK_STATUS_OFF = "off" CLOAK_STATUS_ON = "on" CLOAK_STATUS_MANUAL = "on*" def inject(self, registry): self.bot = registry.get_instance("bot") self.db = registry.get_instance("db") self.util = registry.get_instance("util") self.text = registry.get_instance("text") self.character_service = registry.get_instance("character_service") self.command_alias_service = registry.get_instance("command_alias_service") self.timer_controller = registry.get_instance("timer_controller", is_optional=True) self.event_service = registry.get_instance("event_service") self.access_service = registry.get_instance("access_service") self.message_hub_service = registry.get_instance("message_hub_service") def pre_start(self): self.bot.register_packet_handler(PublicChannelMessage.id, self.handle_public_message) self.event_service.register_event_type(self.CLOAK_EVENT) self.message_hub_service.register_message_source(self.MESSAGE_SOURCE) def start(self): self.db.exec("CREATE TABLE IF NOT EXISTS cloak_status (char_id INT NOT NULL, action VARCHAR(10) NOT NULL, created_at INT NOT NULL, org_id INT NOT NULL)") self.command_alias_service.add_alias("city", "cloak") @command(command="cloak", params=[Const("history"), Int("org_id")], access_level="org_member", description="Shows the cloak history") def cloak_history_command(self, request, _, org_id): data = self.db.query("SELECT c.*, p.name FROM cloak_status c LEFT JOIN player p ON c.char_id = p.char_id " "WHERE c.org_id = ? ORDER BY created_at DESC LIMIT 20", [org_id]) blob = "" for row in data: action = self.get_cloak_status_display(row.action) blob += "%s turned the device %s at %s.\n" % (row.name, action, self.util.format_datetime(row.created_at)) conn = self.bot.get_conn_by_org_id(org_id) org_name = conn.get_org_name() return ChatBlob(f"Cloak History for {org_name}", blob) @command(command="cloak", params=[NamedFlagParameters(["all"])], access_level="org_member", description="Shows the cloak status") def cloak_command(self, request, flag_params): t = int(time.time()) if flag_params.all: blob = "" for _id, conn in self.bot.get_conns(lambda x: x.is_main and x.org_id): row = self.db.query_single("SELECT c.char_id, c.action, c.created_at, p.name FROM cloak_status c LEFT JOIN player p ON c.char_id = p.char_id " "WHERE c.org_id = ? ORDER BY created_at DESC LIMIT 1", [conn.org_id]) org_name = conn.get_org_name() if row: action = self.get_cloak_status_display(row.action) time_str = self.util.time_to_readable(t - row.created_at) history_link = self.text.make_tellcmd("History", f"cloak history {conn.org_id}") blob += f"{org_name} - {action} [{time_str} ago] {history_link}\n" else: blob += f"{org_name} - Unknown\n" title = "Cloak Status" return ChatBlob(title, blob) else: conn = request.conn if not conn.org_id: return "This bot is not a member of an org." row = self.db.query_single("SELECT c.char_id, c.action, c.created_at, p.name FROM cloak_status c LEFT JOIN player p ON c.char_id = p.char_id " "WHERE c.org_id = ? ORDER BY created_at DESC LIMIT 1", [conn.org_id]) org_name = conn.get_org_name() if row: action = self.get_cloak_status_display(row.action) time_str = self.util.time_to_readable(t - row.created_at) return f"{org_name} - {action} [{time_str} ago]" else: return f"{org_name} - Unknown cloak status" @command(command="cloak", params=[Options(["raise", "on"]), Int("org_id", is_optional=True)], access_level="org_member", description="Manually raises the cloak status on the bot") def cloak_raise_command(self, request, _, org_id): org_id = org_id or request.conn.org_id if not org_id: return "This bot is not a member of an org." row = self.db.query_single("SELECT action FROM cloak_status WHERE org_id = ?", [org_id]) if row and (row.action == self.CLOAK_STATUS_ON or row.action == self.CLOAK_STATUS_MANUAL): return "The cloaking device is already <green>enabled</green>." self.db.exec("INSERT INTO cloak_status (char_id, action, created_at, org_id) VALUES (?, ?, ?, ?)", [request.sender.char_id, self.CLOAK_STATUS_MANUAL, int(time.time()), org_id]) return "The cloaking device has been set to enabled in the bot (you must still enable the cloak if it is disabled)." @event(event_type=CLOAK_EVENT, description="Record when the city cloak is turned off and on", is_system=True) def city_cloak_event(self, event_type, event_data): self.db.exec("INSERT INTO cloak_status (char_id, action, created_at, org_id) VALUES (?, ?, ?, ?)", [event_data.char_id, event_data.action, int(time.time()), event_data.conn.org_id]) @timerevent(budatime="15m", description="Reminds the players when cloak can be raised") def cloak_reminder_event(self, event_type, event_data): messages = [] for _id, conn in self.bot.get_conns(lambda x: x.is_main and x.org_id): row = self.db.query_single("SELECT c.*, p.name FROM cloak_status c LEFT JOIN player p ON c.char_id = p.char_id " "WHERE c.org_id = ? ORDER BY created_at DESC LIMIT 1", [conn.org_id]) if row: one_hour = 3600 t = int(time.time()) time_until_change = row.created_at + one_hour - t if row.action == self.CLOAK_STATUS_OFF and 0 >= time_until_change > (one_hour * 6 * -1): time_str = self.util.time_to_readable(t - row.created_at) org_name = conn.get_org_name() messages.append(f"The cloaking device for org <highlight>{org_name}</highlight> is <orange>disabled</orange> but can be enabled. " f"<highlight>{row.name}</highlight> disabled it {time_str} ago.") if messages: self.message_hub_service.send_message(self.MESSAGE_SOURCE, None, None, "\n".join(messages)) @event(event_type=CLOAK_EVENT, description="Set a timer for when cloak can be raised and lowered") def city_cloak_timer_event(self, event_type, event_data): if event_data.action == self.CLOAK_STATUS_OFF: timer_name = f"Raise City Cloak ({event_data.conn.get_org_name()})" elif event_data.action == self.CLOAK_STATUS_ON: timer_name = f"Lower City Cloak ({event_data.conn.get_org_name()})" else: raise Exception(f"Unknown cloak action '{event_data.action}'") timer_name = self.timer_controller.get_timer_name(timer_name) self.timer_controller.add_timer(timer_name, event_data.char_id, PublicChannelService.ORG_CHANNEL_COMMAND, int(time.time()), 3600) def handle_public_message(self, conn: Conn, packet: PublicChannelMessage): if not conn.is_main: return ext_msg = packet.extended_message if ext_msg and ext_msg.category_id == 1001 and ext_msg.instance_id == 1: char_name = ext_msg.params[0] char_id = self.character_service.resolve_char_to_id(char_name) action = ext_msg.params[1] self.event_service.fire_event(self.CLOAK_EVENT, DictObject({"char_id": char_id, "name": char_name, "action": action, "conn": conn})) def get_cloak_status_display(self, status): if status == self.CLOAK_STATUS_ON: return "<green>on</green>" elif status == self.CLOAK_STATUS_MANUAL: return "<green>on*</green>" elif status == self.CLOAK_STATUS_OFF: return "<orange>off</orange>" else: return "<highlight>Unknown</highlight>"
class LastSeenController: def __init__(self): self.logger = Logger(__name__) def inject(self, registry): self.db = registry.get_instance("db") self.util = registry.get_instance("util") self.buddy_service = registry.get_instance("buddy_service") def start(self): self.db.exec("CREATE TABLE IF NOT EXISTS last_seen (char_id INT NOT NULL PRIMARY KEY, " "dt INT NOT NULL DEFAULT 0)") @command(command="lastseen", params=[Character("character"), NamedFlagParameters(["show_all"])], access_level="org_member", description="Show the last time an org member was online (on any alt)") def lastseen_cmd(self, request, char, flag_params): sql = "SELECT p.*, a.group_id, a.status, l.dt FROM player p " \ "LEFT JOIN alts a ON p.char_id = a.char_id " \ "LEFT JOIN last_seen l ON p.char_id = l.char_id " \ "WHERE p.char_id = ? OR a.group_id = (SELECT group_id FROM alts WHERE char_id = ?) " \ "ORDER BY l.dt DESC, p.name ASC" data = self.db.query(sql, [char.char_id, char.char_id]) blob = "" if len(data) == 0: return f"No lastseen information for <highlight>{char.name}</highlight> has been recorded." else: if flag_params.show_all: for row in data: blob += f"<highlight>{row.name}</highlight>" if row.dt: blob += " last seen at " + self.util.format_datetime(row.dt) else: blob += " unknown" blob += "\n\n" return ChatBlob("Last Seen Info for %s (%d)" % (char.name, len(data)), blob) else: online_alts = list(filter(lambda x: self.buddy_service.is_online(x.char_id), data)) if online_alts: online_alts_str = ", ".join(map(lambda x: f"<highlight>{x.name}</highlight>", online_alts)) return f"<highlight>{char.name}</highlight> is <green>online</green> with: {online_alts_str}." else: alt_name = data[0].name if data[0].dt: last_seen = self.util.format_datetime(data[0].dt) return f"<highlight>{char.name}</highlight> was last seen online with <highlight>{alt_name}</highlight> at <highlight>{last_seen}</highlight>." else: return f"No lastseen information for <highlight>{char.name}</highlight> has been recorded." @event(event_type=BuddyService.BUDDY_LOGON_EVENT, description="Record last seen info") def handle_buddy_logon_event(self, event_type, event_data): self.update_last_seen(event_data.char_id) def update_last_seen(self, char_id): t = int(time.time()) if self.db.exec("UPDATE last_seen SET dt = ? WHERE char_id = ?", [t, char_id]) == 0: self.db.exec("INSERT IGNORE INTO last_seen (char_id, dt) VALUES (?, ?)", [char_id, t]) def get_last_seen(self, char_id): return self.db.query_single("SELECT dt FROM last_seen WHERE char_id = ?", [char_id])
class OnlineController: ORG_CHANNEL = "Org" PRIVATE_CHANNEL = "Private" def __init__(self): self.afk_regex = re.compile("^(afk|brb) ?(.*)$", re.IGNORECASE) self.channels = [(self.ORG_CHANNEL, 0), (self.PRIVATE_CHANNEL, 1)] def inject(self, registry): self.bot = registry.get_instance("bot") self.db: DB = registry.get_instance("db") self.text = registry.get_instance("text") self.util = registry.get_instance("util") self.pork_service = registry.get_instance("pork_service") self.character_service = registry.get_instance("character_service") self.command_alias_service = registry.get_instance( "command_alias_service") self.alts_service = registry.get_instance("alts_service") self.alts_controller = registry.get_instance("alts_controller") def start(self): self.db.exec("DROP TABLE IF EXISTS online") self.db.exec( "CREATE TABLE online (char_id INT NOT NULL, afk_dt INT NOT NULL, afk_reason VARCHAR(255) DEFAULT '', channel CHAR(50) NOT NULL, dt INT NOT NULL, UNIQUE(char_id, channel))" ) self.command_alias_service.add_alias("o", "online") @command(command="online", params=[], access_level="member", description="Show the list of online characters") def online_cmd(self, request): return self.get_online_output() @command( command="online", params=[ Any("profession"), NamedFlagParameters(["include_offline_alts"]) ], access_level="member", description= "Show the list of online characters with alts of a certain profession") def online_profession_cmd(self, request, prof, flag_params): profession = self.util.get_profession(prof) if not profession: return "Error! Unknown profession <highlight>%s</highlight>." % prof num_org_bots = len( self.bot.get_conns(lambda x: x.is_main and x.org_id)) blob = "" blob += self.text.make_tellcmd( "Include offline alts", f"online {profession} --include_offline_alts") + "\n\n" count = 0 for channel, _ in self.channels: online_list = self.get_online_alts( channel, profession, flag_params.include_offline_alts) if len(online_list) > 0: blob += "<header2>%s Channel</header2>\n" % channel current_main = "" for row in online_list: if current_main != row.main: count += 1 blob += "\n%s\n" % row.main current_main = row.main org_info = "" if channel == self.PRIVATE_CHANNEL or num_org_bots > 1: if row.org_name: org_info = ", %s (%s)" % (row.org_name, row.org_rank_name) blob += " <highlight>%s</highlight> (%d/<green>%d</green>) %s%s" % ( row.name, row.level or 0, row.ai_level or 0, row.profession, org_info) if row.online: blob += " [<green>Online</green>]" blob += "\n" blob += "\n\n" return ChatBlob("Online %s (%d)" % (profession, count), blob) @command( command="count", params=[], access_level="member", description= "Show counts of players by title level, profession, and organization") def count_cmd(self, request): data = self.db.query( "SELECT p.*, o.channel, COALESCE(p.name, o.char_id) AS name FROM online o LEFT JOIN player p ON o.char_id = p.char_id ORDER BY channel ASC" ) title_levels = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0} profs = { "Adventurer": 0, "Agent": 0, "Bureaucrat": 0, "Doctor": 0, "Enforcer": 0, "Engineer": 0, "Fixer": 0, "Keeper": 0, "Martial Artist": 0, "Meta-Physicist": 0, "Nano-Technician": 0, "Soldier": 0, "Trader": 0, "Shade": 0, "Unknown": 0 } orgs = {} # populate counts for row in data: prof_name = row.profession if row.profession else "Unknown" profs[prof_name] += 1 title_levels[self.util.get_title_level(row.level)] += 1 org_name = row.org_name if row.org_name else "<None>" org_count = orgs.get(org_name, 0) orgs[org_name] = org_count + 1 blob = "" blob += "<header>Title Levels</header>\n" for title_level, count in title_levels.items(): if count > 0: blob += "%d: %d\n" % (title_level, count) blob += "\n\n<header>Professions</header>\n" for prof, count in profs.items(): if count > 0: blob += "%s: %d\n" % (prof, count) blob += "\n\n<header>Organizations</header>\n" for org, count in orgs.items(): if count > 0: blob += "%s: %d\n" % (org, count) return ChatBlob("Count (%d)" % len(data), blob) def get_online_characters(self, channel): sql = "SELECT " \ "p1.*, " \ "o.afk_dt, " \ "o.afk_reason, " \ "COALESCE(p2.name, p1.name, o.char_id) AS main, " \ "COALESCE(p1.name, o.char_id) AS name " \ "FROM online o " \ "LEFT JOIN alts a1 ON o.char_id = a1.char_id " \ "LEFT JOIN player p1 ON o.char_id = p1.char_id " \ "LEFT JOIN alts a2 ON a1.group_id = a2.group_id AND a2.status = ? " \ "LEFT JOIN player p2 ON a2.char_id = p2.char_id " \ "WHERE channel = ? " \ "ORDER BY COALESCE(p2.name, p1.name, o.char_id) ASC, COALESCE(p1.name, o.char_id) ASC" return self.db.query(sql, [AltsService.MAIN, channel]) @event(PrivateChannelService.JOINED_PRIVATE_CHANNEL_EVENT, "Record in database when someone joins private channel", is_system=True) def private_channel_joined_event(self, event_type, event_data): self.pork_service.load_character_info(event_data.char_id) self.db.exec( "INSERT INTO online (char_id, afk_dt, afk_reason, channel, dt) VALUES (?, ?, ?, ?, ?)", [ event_data.char_id, 0, "", self.PRIVATE_CHANNEL, int(time.time()) ]) @event(PrivateChannelService.LEFT_PRIVATE_CHANNEL_EVENT, "Record in database when someone leaves private channel", is_system=True) def private_channel_left_event(self, event_type, event_data): self.db.exec("DELETE FROM online WHERE char_id = ? AND channel = ?", [event_data.char_id, self.PRIVATE_CHANNEL]) @event(OrgMemberController.ORG_MEMBER_LOGON_EVENT, "Record in database when org member logs on", is_system=True) def org_member_logon_record_event(self, event_type, event_data): self.pork_service.load_character_info(event_data.char_id) self.db.exec( "INSERT INTO online (char_id, afk_dt, afk_reason, channel, dt) VALUES (?, ?, ?, ?, ?)", [event_data.char_id, 0, "", self.ORG_CHANNEL, int(time.time())]) @event(OrgMemberController.ORG_MEMBER_LOGOFF_EVENT, "Record in database when org member logs off", is_system=True) def org_member_logoff_record_event(self, event_type, event_data): self.db.exec("DELETE FROM online WHERE char_id = ? AND channel = ?", [event_data.char_id, self.ORG_CHANNEL]) @event(PrivateChannelService.PRIVATE_CHANNEL_MESSAGE_EVENT, "Check for afk messages in private channel") def afk_check_private_channel_event(self, event_type, event_data): if not self.bot.get_conn_by_char_id(event_data.char_id): self.afk_check( event_data.char_id, event_data.message, lambda msg: self.bot. send_private_channel_message(msg, conn=event_data.conn)) @event(PublicChannelService.ORG_CHANNEL_MESSAGE_EVENT, "Check for afk messages in org channel") def afk_check_org_channel_event(self, event_type, event_data): if not self.bot.get_conn_by_char_id(event_data.char_id): self.afk_check( event_data.char_id, event_data.message, lambda msg: self.bot.send_org_message(msg, conn=event_data.conn)) @event(OrgMemberController.ORG_MEMBER_LOGON_EVENT, "Send online list to org members logging in") def org_member_send_logon_event(self, event_type, event_data): if self.bot.is_ready(): self.bot.send_private_message(event_data.char_id, self.get_online_output(), conn=event_data.conn) @event(PrivateChannelService.JOINED_PRIVATE_CHANNEL_EVENT, "Send online list to characters joining the private channel") def private_channel_send_logon_event(self, event_type, event_data): self.bot.send_private_message(event_data.char_id, self.get_online_output(), conn=event_data.conn) def afk_check(self, char_id, message, channel_reply): matches = self.afk_regex.search(message) if matches: char_name = self.character_service.resolve_char_to_name(char_id) self.set_afk(char_id, int(time.time()), message) # channel_reply("<highlight>%s</highlight> is now afk." % char_name) else: # will handle multiple rows since set_afk() will update multiple row = self.db.query_single( "SELECT * FROM online WHERE char_id = ? AND afk_dt > 0", [char_id]) if row: self.set_afk(char_id, 0, "") char_name = self.character_service.resolve_char_to_name( char_id) time_string = self.util.time_to_readable( int(time.time()) - row.afk_dt) channel_reply("<highlight>%s</highlight> is back after %s." % (char_name, time_string)) def set_afk(self, char_id, dt, reason): self.db.exec( "UPDATE online SET afk_dt = ?, afk_reason = ? WHERE char_id = ?", [dt, reason, char_id]) def get_online_output(self): num_org_bots = len( self.bot.get_conns(lambda x: x.is_main and x.org_id)) blob = "" count = 0 for channel, _ in self.channels: online_list = self.get_online_characters(channel) if len(online_list) == 0: continue blob += "<header2>%s Channel</header2>\n" % channel current_main = "" for row in online_list: if current_main != row.main: count += 1 blob += "\n<pagebreak>%s\n" % self.text.make_tellcmd( row.main, "alts %s" % row.main) current_main = row.main afk = "" if row.afk_dt > 0: afk = " - <highlight>%s (%s ago)</highlight>" % ( row.afk_reason, self.util.time_to_readable( int(time.time()) - row.afk_dt)) org_info = "" if channel == self.PRIVATE_CHANNEL or num_org_bots > 1: if row.org_name: org_info = ", %s (%s)" % (row.org_name, row.org_rank_name) blob += " %s (%d/<green>%d</green>) %s%s%s\n" % ( row.name, row.level or 0, row.ai_level or 0, row.profession, org_info, afk) blob += "\n\n" return ChatBlob("Online (%d)" % count, blob) def get_char_info_display(self, char_id, conn: Conn): char_info = self.pork_service.get_character_info(char_id) if char_info: name = self.text.format_char_info(char_info) else: char_name = self.character_service.resolve_char_to_name(char_id) name = "<highlight>%s</highlight>" % char_name alts = self.alts_service.get_alts(char_id) cnt = len(alts) if cnt > 1: if alts[0].char_id == char_id: main = "Alts (%d)" % cnt else: main = "Alts of %s (%d)" % (alts[0].name, cnt) name += " - " + self.text.paginate_single( ChatBlob(main, self.alts_controller.format_alt_list(alts)), conn) return name def get_online_alts(self, channel, profession, include_offline=False): sql = "SELECT DISTINCT " \ "p2.*, " \ "(CASE WHEN o2.char_id IS NULL THEN 0 ELSE 1 END) AS online, " \ "COALESCE(p1.name, o.char_id) AS main, " \ "COALESCE(p2.name, o.char_id) AS name, " \ "o.channel " \ "FROM online o " \ "LEFT JOIN alts a1 ON o.char_id = a1.char_id " \ "LEFT JOIN alts a2 ON a1.group_id = a2.group_id AND a2.status = ? " \ "LEFT JOIN player p1 ON p1.char_id = COALESCE(a2.char_id, o.char_id) " \ "LEFT JOIN alts a3 ON a2.group_id = a3.group_id " \ "LEFT JOIN player p2 ON p2.char_id = COALESCE(a3.char_id, o.char_id) " \ "LEFT JOIN online o2 ON p2.char_id = o2.char_id " \ "WHERE o.channel = ? AND p2.profession = ?" if not include_offline: sql += " AND o2.char_id IS NOT NULL" sql += " ORDER BY p1.name ASC, p2.name ASC" return self.db.query(sql, [AltsService.MAIN, channel, profession]) def sort_channels(self, item): channel, priority = item return str(priority) + channel def register_online_channel(self, channel_name, sort_priority=2): for name, _ in self.channels: if name == channel_name: return self.channels.append((channel_name, sort_priority)) self.channels.sort(key=self.sort_channels) def deregister_online_channel(self, channel): self.db.query("DELETE FROM online WHERE channel = ?", [channel]) for i, obj in enumerate(self.channels): if obj[0] == channel: del self.channels[i]
class RaidController: MESSAGE_SOURCE = "raid" NO_RAID_RUNNING_RESPONSE = "No raid is running." def __init__(self): self.raid: Raid = None def inject(self, registry): self.bot: Tyrbot = registry.get_instance("bot") self.db: DB = registry.get_instance("db") self.text: Text = registry.get_instance("text") self.setting_service: SettingService = registry.get_instance("setting_service") self.alts_service: AltsService = registry.get_instance("alts_service") self.buddy_service = registry.get_instance("buddy_service") self.character_service: CharacterService = registry.get_instance("character_service") self.private_channel_service = registry.get_instance("private_channel_service") self.points_controller: PointsController = registry.get_instance("points_controller") self.util: Util = registry.get_instance("util") self.message_hub_service = registry.get_instance("message_hub_service") self.leader_controller = registry.get_instance("leader_controller") self.topic_controller = registry.get_instance("topic_controller") self.member_controller = registry.get_instance("member_controller") def pre_start(self): self.message_hub_service.register_message_source(self.MESSAGE_SOURCE) self.db.exec("CREATE TABLE IF NOT EXISTS raid_log (raid_id INT PRIMARY KEY AUTO_INCREMENT, raid_name VARCHAR(255) NOT NULL, " "started_by BIGINT NOT NULL, raid_start INT NOT NULL, raid_end INT NOT NULL)") self.db.exec("CREATE TABLE IF NOT EXISTS raid_log_participants (raid_id INT NOT NULL, raider_id BIGINT NOT NULL, " "accumulated_points INT DEFAULT 0, left_raid INT, was_kicked INT, was_kicked_reason VARCHAR(500))") self.db.load_sql_file(self.module_dir + "/" + "raid_loot.sql") @command(command="raid", params=[], access_level="member", description="Show the current raid status") def raid_cmd(self, request): if not self.raid: return self.NO_RAID_RUNNING_RESPONSE t = int(time.time()) blob = "" blob += "Name: <highlight>%s</highlight>\n" % self.raid.raid_name blob += "Started By: <highlight>%s</highlight>\n" % self.raid.started_by.name blob += "Started At: <highlight>%s</highlight> (%s ago)\n" % (self.util.format_datetime(self.raid.started_at), self.util.time_to_readable(t - self.raid.started_at)) blob += "Status: %s" % ("<green>Open</green>" if self.raid.is_open else "<red>Closed</red>") if self.raid.is_open: blob += " (%s)" % self.text.make_tellcmd("Join", "raid join") blob += "\n\n" topic = self.topic_controller.get_topic() if topic: time_str = self.util.time_to_readable(int(time.time()) - topic["created_at"]) blob += "<header2>Orders</header2>\n" blob += "%s\n- <highlight>%s</highlight> %s ago\n\n" % (topic["topic_message"], topic["created_by"]["name"], time_str) blob += "<header2>Raiders</header2>\n" for raider in self.raid.raiders: if raider.is_active: blob += self.text.format_char_info(raider.get_active_char()) + "\n" return ChatBlob("Raid Status", blob) @command(command="raid", params=[Const("start"), Any("raid_name")], description="Start new raid", access_level="moderator", sub_command="manage") def raid_start_cmd(self, request, _, raid_name: str): if self.raid: return f"The raid <highlight>{self.raid.raid_name}</highlight> is already running." # if a leader is already set, only start raid if sender can take leader from current leader msg = self.leader_controller.set_raid_leader(request.sender, request.sender, request.conn) request.reply(msg) leader = self.leader_controller.get_leader(request.conn) if leader and leader.char_id != request.sender.char_id: return None self.raid = Raid(raid_name, request.sender) sql = "INSERT INTO raid_log (raid_name, started_by, raid_start, raid_end) VALUES (?,?,?,?)" self.db.exec(sql, [self.raid.raid_name, self.raid.started_by.char_id, self.raid.started_at, 0]) self.raid.raid_id = self.db.last_insert_id() leader_alts = self.alts_service.get_alts(request.sender.char_id) self.raid.raiders.append(Raider(leader_alts, request.sender.char_id)) join_link = self.text.paginate_single(ChatBlob("Click here", self.get_raid_join_blob()), request.conn) msg = "\n<highlight>----------------------------------------</highlight>\n" msg += "<highlight>%s</highlight> has started the raid <highlight>%s</highlight>.\n" % (request.sender.name, raid_name) msg += "%s to join\n" % join_link msg += "<highlight>----------------------------------------</highlight>" self.send_message(msg, request.conn) @command(command="raid", params=[Const("cancel")], description="Cancel the raid without saving/logging", access_level="moderator", sub_command="manage") def raid_cancel_cmd(self, request, _): if self.raid is None: return self.NO_RAID_RUNNING_RESPONSE self.send_message("<highlight>%s</highlight> canceled the raid <highlight>%s</highlight>." % (request.sender.name, self.raid.raid_name), request.conn) self.raid = None self.topic_controller.clear_topic() @command(command="raid", params=[Const("join")], description="Join the ongoing raid", access_level="member") def raid_join_cmd(self, request, _): if not self.raid: return self.NO_RAID_RUNNING_RESPONSE main_id = self.alts_service.get_main(request.sender.char_id).char_id in_raid = self.is_in_raid(main_id) if in_raid is not None: if in_raid.active_id == request.sender.char_id: if in_raid.is_active: return "You are already participating in the raid." else: if not self.raid.is_open: return "Raid is closed." in_raid.is_active = True in_raid.was_kicked = None in_raid.was_kicked_reason = None in_raid.left_raid = None self.points_controller.add_log_entry(main_id, request.sender.char_id, f"Joined raid {self.raid.raid_name}") self.send_message("%s returned to actively participating in the raid." % request.sender.name, request.conn) elif in_raid.is_active: former_active_name = self.character_service.resolve_char_to_name(in_raid.active_id) in_raid.active_id = request.sender.char_id self.points_controller.add_log_entry(main_id, request.sender.char_id, f"Switched to alt {request.sender.name} ({request.sender.char_id} in raid {self.raid.raid_name}") self.send_message("<highlight>%s</highlight> joined the raid with a different alt, <highlight>%s</highlight>." % (former_active_name, request.sender.name), request.conn) elif not in_raid.is_active: if not self.raid.is_open: return "Raid is closed." self.points_controller.add_log_entry(main_id, request.sender.char_id, f"Switched to alt {request.sender.name} in raid {self.raid.raid_name}") self.points_controller.add_log_entry(main_id, request.sender.char_id, f"Joined raid {self.raid.raid_name}") former_active_name = self.character_service.resolve_char_to_name(in_raid.active_id) in_raid.active_id = request.sender.char_id in_raid.was_kicked = None in_raid.was_kicked_reason = None in_raid.left_raid = None self.send_message("%s returned to actively participate with a different alt, <highlight>%s</highlight>." % (former_active_name, request.sender.name), request.conn) elif self.raid.is_open: alts = self.alts_service.get_alts(request.sender.char_id) self.raid.raiders.append(Raider(alts, request.sender.char_id)) self.points_controller.add_log_entry(main_id, request.sender.char_id, f"Joined raid {self.raid.raid_name}") self.send_message("<highlight>%s</highlight> joined the raid." % request.sender.name, request.conn) if request.sender.char_id not in self.bot.get_primary_conn().private_channel: self.private_channel_service.invite(request.sender.char_id, self.bot.get_primary_conn()) else: return "Raid is closed." @command(command="raid", params=[Const("leave")], description="Leave the ongoing raid", access_level="member") def raid_leave_cmd(self, request, _): main_id = self.alts_service.get_main(request.sender.char_id).char_id in_raid = self.is_in_raid(main_id) if in_raid: if not in_raid.is_active: return "You are not active in the raid." in_raid.is_active = False in_raid.left_raid = int(time.time()) self.points_controller.add_log_entry(main_id, request.sender.char_id, f"Left raid {self.raid.raid_name}") self.send_message("<highlight>%s</highlight> left the raid." % request.sender.name, request.conn) else: return "You are not in the raid." @command(command="raid", params=[Const("addpts"), Any("name")], description="Add points to all active participants", access_level="moderator", sub_command="manage") def points_add_cmd(self, request, _, name: str): if not self.raid: return self.NO_RAID_RUNNING_RESPONSE preset = self.db.query_single("SELECT name, points FROM points_presets WHERE name = ?", [name]) if not preset: return ChatBlob("No such preset - see list of presets", self.points_controller.build_preset_list()) self.raid.added_points = True for raider in self.raid.raiders: account = self.points_controller.get_account(raider.main_id, request.conn) if raider.is_active: if account.disabled == 0: self.points_controller.alter_points(raider.main_id, request.sender.char_id, preset.name, preset.points) raider.accumulated_points += preset.points else: self.points_controller.add_log_entry(raider.main_id, request.sender.char_id, "Participated in raid with a disabled account, missed points from %s." % preset.name) else: self.points_controller.add_log_entry(raider.main_id, request.sender.char_id, "Was inactive during raid, %s, when points for %s were dished out." % (self.raid.raid_name, preset.name)) self.send_message("<highlight>%d</highlight> points added to all active raiders for <highlight>%s</highlight>." % (preset.points, preset.name), request.conn) @command(command="raid", params=[Const("active")], description="Get a list of raiders to do active check", access_level="moderator", sub_command="manage") def raid_active_cmd(self, request, _): if not self.raid: return self.NO_RAID_RUNNING_RESPONSE blob = "" count = 0 raider_names = [] for raider in self.raid.raiders: if count == 10: active_check_names = "/assist " active_check_names += "\\n /assist ".join(raider_names) blob += "\n[<a href='chatcmd://%s'>Active check</a>]\n\n" % active_check_names count = 0 raider_names.clear() raider_name = self.character_service.resolve_char_to_name(raider.active_id) akick_link = self.text.make_tellcmd("Active kick", "raid kick %s inactive" % raider_name) warn_link = self.text.make_chatcmd("Warn", "/tell %s You missed active check, please give notice." % raider_name) blob += "<highlight>%s</highlight> [%s] [%s]\n" % (raider_name, akick_link, warn_link) raider_names.append(raider_name) count += 1 if len(raider_names) > 0: active_check_names = "/assist " active_check_names += "\\n /assist ".join(raider_names) blob += "\n[<a href='chatcmd://%s'>Active check</a>]\n\n" % active_check_names raider_names.clear() return ChatBlob("Active check", blob) @command(command="raid", params=[Const("add"), Character("char")], description="Add a character to the raid", access_level="moderator", sub_command="manage") def raid_add_cmd(self, request, _, char): if self.raid is None: return self.NO_RAID_RUNNING_RESPONSE alts = self.alts_service.get_alts(char.char_id) main_id = alts[0].char_id in_raid = self.is_in_raid(main_id) if in_raid is None: self.raid.raiders.append(Raider(alts, char.char_id)) self.bot.send_private_message(char.char_id, f"You have been added to the raid <highlight>{self.raid.raid_name}</highlight>.", conn=request.conn) self.points_controller.add_log_entry(main_id, request.sender.char_id, f"Added to raid {self.raid.raid_name}") if char.char_id not in self.bot.get_primary_conn().private_channel: self.private_channel_service.invite(char.char_id) return "<highlight>%s</highlight> has been added to the raid." % char.name else: if not in_raid.is_active: in_raid.is_active = True in_raid.was_kicked = None in_raid.was_kicked_reason = None in_raid.left_raid = None self.points_controller.add_log_entry(main_id, request.sender.char_id, f"Added to raid {self.raid.raid_name}") self.bot.send_private_message(char.char_id, f"You have been set as active in the raid <highlight>{self.raid.raid_name}</highlight>.", conn=request.conn) return f"<highlight>{char.name}</highlight> has been set as active." else: return f"<highlight>{char.name}</highlight> is already in the raid." @command(command="raid", params=[Const("kick"), Character("char"), Any("reason")], description="Set raider as kicked with a reason", access_level="moderator", sub_command="manage") def raid_kick_cmd(self, request, _, char: SenderObj, reason: str): if self.raid is None: return self.NO_RAID_RUNNING_RESPONSE main_id = self.alts_service.get_main(char.char_id).char_id in_raid = self.is_in_raid(main_id) if in_raid is not None: if not in_raid.is_active: return "<highlight>%s</highlight> is already set as inactive." % char.name in_raid.is_active = False in_raid.was_kicked = int(time.time()) in_raid.was_kicked_reason = reason self.points_controller.add_log_entry(main_id, request.sender.char_id, f"Kicked from raid {self.raid.raid_name} with reason: {reason}") self.bot.send_private_message(char.char_id, f"You have been kicked from raid <highlight>{self.raid.raid_name}</highlight> with reason <highlight>{reason}</highlight>.", conn=request.conn) return "<highlight>%s</highlight> has been kicked from the raid with reason <highlight>%s</highlight>." % (char.name, reason) else: return "<highlight>%s</highlight> is not participating." % char.name @command(command="raid", params=[Options(["open", "unlock"])], description="Open raid for new participants", access_level="moderator", sub_command="manage") def raid_open_cmd(self, request, action): if not self.raid: return self.NO_RAID_RUNNING_RESPONSE if self.raid.is_open: return "Raid is already open." else: self.raid.is_open = True self.send_message("Raid has been opened by %s." % request.sender.name, request.conn) @command(command="raid", params=[Options(["close", "lock"])], description="Close raid for new participants", access_level="moderator", sub_command="manage") def raid_close_cmd(self, request, action): if not self.raid: return self.NO_RAID_RUNNING_RESPONSE if self.raid.is_open: self.raid.is_open = False self.send_message("Raid has been closed by %s." % request.sender.name, request.conn) else: return "Raid is already closed." @command(command="raid", params=[Options(["end", "save"]), NamedFlagParameters(["force"])], description="End raid, and log results", access_level="moderator", sub_command="manage") def raid_save_cmd(self, request, _, flag_params): if not self.raid: return self.NO_RAID_RUNNING_RESPONSE if not self.raid.added_points and not flag_params.force: blob = "You have not added any points for this raid. Are you sure you want to end this raid now? " blob += self.text.make_tellcmd("Yes", "raid end --force") return ChatBlob("End Raid Confirmation", blob) sql = "UPDATE raid_log SET raid_end = ? WHERE raid_id = ?" self.db.exec(sql, [int(time.time()), self.raid.raid_id]) for raider in self.raid.raiders: sql = "INSERT INTO raid_log_participants (raid_id, raider_id, accumulated_points, left_raid, was_kicked, was_kicked_reason) VALUES (?,?,?,?,?,?)" self.db.exec(sql, [self.raid.raid_id, raider.active_id, raider.accumulated_points, raider.left_raid, raider.was_kicked, raider.was_kicked_reason]) self.raid = None self.topic_controller.clear_topic() self.send_message("Raid saved and ended.", request.conn) @command(command="raid", params=[Const("history"), Int("raid_id")], description="Show log entry for raid", access_level="moderator", sub_command="manage") def raid_history_detail_cmd(self, request, _, raid_id: int): sql = "SELECT r.*, p.*, p2.name AS raider_name FROM raid_log r " \ "LEFT JOIN raid_log_participants p ON r.raid_id = p.raid_id " \ "LEFT JOIN player p2 ON p.raider_id = p2.char_id " \ "WHERE r.raid_id = ? ORDER BY p.accumulated_points DESC" log_entry = self.db.query(sql, [raid_id]) if not log_entry: return "No such log entry." blob = "Raid name: <highlight>%s</highlight>\n" % log_entry[0].raid_name blob += "Started by: <highlight>%s</highlight>\n" % self.character_service.resolve_char_to_name(log_entry[0].started_by) blob += "Start time: <highlight>%s</highlight>\n" % self.util.format_datetime(log_entry[0].raid_start) blob += "End time: <highlight>%s</highlight>\n" % self.util.format_datetime(log_entry[0].raid_end) blob += "Run time: <highlight>%s</highlight>\n" % self.util.time_to_readable(log_entry[0].raid_end - log_entry[0].raid_start) pts_sum = self.db.query_single("SELECT COALESCE(SUM(p.accumulated_points), 0) AS sum FROM raid_log_participants p WHERE p.raid_id = ?", [raid_id]).sum blob += "Total points: <highlight>%d</highlight>\n\n" % pts_sum blob += "<header2>Participants</header2>\n" for raider in log_entry: main_info = self.alts_service.get_main(raider.raider_id) if main_info.char_id != raider.raider_id: alt_link_text = "Alt of %s" % main_info.name else: alt_link_text = "Alts" alt_link = self.text.make_tellcmd(alt_link_text, "alts %s" % raider.raider_name) account_link = self.text.make_tellcmd("Account", "account %s" % raider.raider_name) blob += "%s - %d points earned [%s] [%s]\n" % (raider.raider_name, raider.accumulated_points, account_link, alt_link) if raider.left_raid: blob += "Left raid: %s\n" % self.util.format_datetime(raider.left_raid) if raider.was_kicked: blob += "Was kicked: %s\n" % self.util.format_datetime(raider.was_kicked) if raider.was_kicked_reason: blob += "Kick reason: %s\n" % raider.was_kicked_reason blob += "\n" return ChatBlob("Raid: %s" % log_entry[0].raid_name, blob) @command(command="raid", params=[Const("history")], description="Show a list of recent raids", access_level="member") def raid_history_cmd(self, request, _): sql = "SELECT * FROM raid_log ORDER BY raid_end DESC LIMIT 30" raids = self.db.query(sql) blob = "" for raid in raids: participant_link = self.text.make_tellcmd("Detail", "raid history %d" % raid.raid_id) timestamp = self.util.format_datetime(raid.raid_start) leader_name = self.character_service.resolve_char_to_name(raid.started_by) blob += "[%d] [%s] <highlight>%s</highlight> started by <highlight>%s</highlight> [%s]\n" % (raid.raid_id, timestamp, raid.raid_name, leader_name, participant_link) return ChatBlob("Raid History (%d)" % len(raids), blob) @command(command="raid", params=[Const("announce"), Any("message", is_optional=True)], access_level="moderator", sub_command="manage", description="Announce the current raid to members") def raid_announce_cmd(self, request, _, message): if not self.raid: return self.NO_RAID_RUNNING_RESPONSE if not self.bot.mass_message_queue: return "Could not announce raid since bot does not have mass messaging capabilities." join_link = self.text.paginate_single(ChatBlob("Click here", self.get_raid_join_blob()), request.conn) msg = "<highlight>%s</highlight> has started the raid <highlight>%s</highlight>. " % (self.raid.started_by.name, self.raid.raid_name) msg += "%s to join." % join_link if message: msg += " " + message count = 0 for member in self.member_controller.get_all_members(): main = self.alts_service.get_main(member.char_id) if self.buddy_service.is_online(member.char_id) and not self.is_in_raid(main.char_id): count += 1 self.bot.send_mass_message(member.char_id, msg, conn=request.conn) return f"Raid announcement is sending to <highlight>{count}</highlight> online members." def is_in_raid(self, main_id: int): if self.raid is None: return None for raider in self.raid.raiders: if raider.main_id == main_id: return raider def get_raid_join_blob(self): return "<header2>1. Join the raid</header2>\n" \ "To join the current raid <highlight>%s</highlight>, send the following tell to <myname>\n" \ "<tab><tab><a href='chatcmd:///tell <myname> <symbol>raid join'>/tell <myname> raid " \ "join</a>\n\n<header2>2. Enable LFT</header2>\nWhen you have joined the raid, go lft " \ "with \"<myname>\" as description\n<tab><tab><a href='chatcmd:///lft <myname>'>/lft <myname></a>\n\n" \ "<header2>3. Announce</header2>\nYou could announce to the raid leader, that you have enabled " \ "LFT\n<tab><tab><a href='chatcmd:///group <myname> I am on lft'>Announce</a> that you have enabled " \ "lft\n\n<header2>4. Rally with yer mateys</header2>\nFinally, move towards the starting location of " \ "the raid.\n<highlight>Ask for help</highlight> if you're in doubt of where to go." % self.raid.raid_name def send_message(self, msg, conn): # TODO remove once messagehub can handle ChatBlobs pages = self.bot.get_text_pages(msg, conn, self.setting_service.get("private_message_max_page_length").get_value()) for page in pages: self.message_hub_service.send_message(self.MESSAGE_SOURCE, None, None, page)
class ConfigController: def inject(self, registry): self.db: DB = registry.get_instance("db") self.text: Text = registry.get_instance("text") self.command_service = registry.get_instance("command_service") self.event_service = registry.get_instance("event_service") self.setting_service = registry.get_instance("setting_service") self.config_events_controller = registry.get_instance( "config_events_controller") self.ts: TranslationService = registry.get_instance( "translation_service") self.getresp = self.ts.get_response def start(self): self.ts.register_translation("module/config", self.load_config_msg) def load_config_msg(self): with open("modules/core/config/config.msg", mode="r", encoding="UTF-8") as f: return hjson.load(f) @command(command="config", params=[], access_level="admin", description="Show configuration options for the bot") def config_list_cmd(self, request): sql = """SELECT module, SUM(CASE WHEN enabled = 1 THEN 1 ELSE 0 END) count_enabled, SUM(CASE WHEN enabled = 0 THEN 1 ELSE 0 END) count_disabled FROM (SELECT module, enabled FROM command_config UNION SELECT module, enabled FROM event_config WHERE is_hidden = 0 UNION SELECT module, 2 FROM setting) t GROUP BY module ORDER BY module ASC""" data = self.db.query(sql) count = len(data) blob = "" current_group = "" for row in data: parts = row.module.split(".") group = parts[0] module = parts[1] if group != current_group: current_group = group blob += "\n<header2>" + current_group + "</header2>\n" blob += self.text.make_tellcmd(module, "config mod " + row.module) + " " if row.count_enabled > 0 and row.count_disabled > 0: blob += self.getresp("module/config", "partial") else: blob += self.getresp( "module/config", "enabled_high" if row.count_disabled == 0 else "disabled_high") blob += "\n" return ChatBlob( self.getresp("module/config", "config", {"count": count}), blob) @command(command="config", params=[ Options(["mod", "module"]), Any("module_name"), NamedFlagParameters(["include_hidden_events"]) ], access_level="admin", description="Show configuration options for a specific module") def config_module_list_cmd(self, request, _, module, named_params): module = module.lower() blob = "" data = self.db.query( "SELECT name FROM setting WHERE module = ? ORDER BY name ASC", [module]) if data: blob += self.getresp("module/config", "settings") for row in data: setting = self.setting_service.get(row.name) blob += "%s: %s (%s)\n" % ( setting.get_description(), setting.get_display_value(), self.text.make_tellcmd("change", "config setting " + row.name)) data = self.db.query( "SELECT DISTINCT command, sub_command FROM command_config WHERE module = ? ORDER BY command ASC", [module]) if data: blob += self.getresp("module/config", "commands") for row in data: command_key = self.command_service.get_command_key( row.command, row.sub_command) blob += self.text.make_tellcmd( command_key, "config cmd " + command_key) + "\n" blob += self.format_events(self.get_events(module, False), self.getresp("module/config", "events")) if named_params.include_hidden_events: blob += self.format_events( self.get_events(module, True), self.getresp("module/config", "hidden_events")) if blob: if not named_params.include_hidden_events: blob += "\n" + self.text.make_tellcmd( self.getresp("module/config", "include_hidden_events"), f"config mod {module} --include_hidden_events") return ChatBlob( self.getresp("module/config", "mod_title", {"mod": module}), blob) else: return self.getresp("module/config", "mod_not_found", {"mod": module}) @command(command="config", params=[Const("settinglist")], access_level="admin", description="List all settings") def config_settinglist_cmd(self, request, _): blob = "" data = self.db.query("SELECT * FROM setting ORDER BY module, name ASC") count = len(data) if data: blob += self.getresp("module/config", "settings") current_module = "" for row in data: if row.module != current_module: current_module = row.module blob += "\n<pagebreak><header2>%s</header2>\n" % row.module setting = self.setting_service.get(row.name) blob += "%s: %s (%s)\n" % ( setting.get_description(), setting.get_display_value(), self.text.make_tellcmd("change", "config setting " + row.name)) return ChatBlob( self.getresp("module/config", "settinglist_title", {"count": count}), blob) @command(command="config", params=[ Const("setting"), Any("setting_name"), Options(["set", "clear"]), Any("new_value", is_optional=True) ], access_level="admin", description="Change a setting value") def config_setting_update_cmd(self, request, _, setting_name, op, new_value): setting_name = setting_name.lower() if op == "clear": new_value = "" elif not new_value: return self.getresp("module/config", "no_new_value") setting = self.setting_service.get(setting_name) if setting: setting.set_value(new_value) if op == "clear": return self.getresp("module/config", "set_clr", {"setting": setting_name}) else: return self.getresp("module/config", "set_new", { "setting": setting_name, "value": setting.get_display_value() }) else: return self.getresp("module/config", "setting_not_found", {"setting": setting_name}) @command(command="config", params=[Const("setting"), Any("setting_name")], access_level="admin", description="Show configuration options for a setting") def config_setting_show_cmd(self, request, _, setting_name): setting_name = setting_name.lower() blob = "" setting = self.setting_service.get(setting_name) if setting: blob += self.getresp("module/config", "current_value", {"value": str(setting.get_display_value())}) blob += self.getresp("module/config", "description", {"desc": setting.get_description()}) if setting.get_extended_description(): blob += setting.get_extended_description() + "\n\n" blob += setting.get_display() return ChatBlob( self.getresp("module/config", "setting", {"setting": setting_name}), blob) else: return self.getresp("module/config", "setting_not_found", {"setting": setting_name}) def get_events(self, module, is_hidden): return self.db.query( "SELECT event_type, event_sub_type, handler, description, enabled, is_hidden " f"FROM event_config WHERE module = ? AND is_hidden = ? " "ORDER BY is_hidden, event_type, handler ASC", [module, 1 if is_hidden else 0]) def format_events(self, data, title): blob = "" if data: blob += f"\n<header2>{title}</header2>\n" for row in data: event_type_key = self.event_service.get_event_type_key( row.event_type, row.event_sub_type) enabled = self.getresp( "module/config", "enabled_high" if row.enabled == 1 else "disabled_high") blob += "%s - %s [%s]" % ( self.config_events_controller.format_event_type(row), row.description, enabled) blob += " " + self.text.make_tellcmd( "On", "config event %s %s enable" % (event_type_key, row.handler)) blob += " " + self.text.make_tellcmd( "Off", "config event %s %s disable" % (event_type_key, row.handler)) if row.event_type == "timer": blob += " " + self.text.make_tellcmd( "Run Now", "config event %s %s run" % (event_type_key, row.handler)) blob += "\n" return blob
class HelpController: def inject(self, registry): self.bot = registry.get_instance("bot") self.text = registry.get_instance("text") self.db = registry.get_instance("db") self.access_service = registry.get_instance("access_service") self.command_service = registry.get_instance("command_service") self.command_alias_service = registry.get_instance( "command_alias_service") self.ts: TranslationService = registry.get_instance( "translation_service") self.getresp = self.ts.get_response def start(self): self.ts.register_translation("module/help", self.load_help_msg) self.command_alias_service.add_alias("version", "about") def load_help_msg(self): with open("modules/core/help/help.msg", mode="r", encoding="UTF-8") as f: return hjson.load(f) @command(command="about", params=[], access_level="all", description="Show information about the development of this bot") def about_cmd(self, request): with open(os.path.dirname(os.path.realpath(__file__)) + os.sep + "about.txt", mode="r", encoding="UTF-8") as f: return ChatBlob( self.getresp("module/help", "about_title", {"version": self.bot.version}), f.read()) @command(command="help", params=[], access_level="all", description="Show a list of commands to get help with") def help_list_cmd(self, request): data = self.db.query( "SELECT command, module, access_level FROM command_config " "WHERE enabled = 1 " "ORDER BY module ASC, command ASC") blob = "" current_group = "" current_module = "" current_command = "" access_level = self.access_service.get_access_level( request.sender.char_id) for row in data: if access_level[ "level"] > self.access_service.get_access_level_by_label( row.access_level)["level"]: continue parts = row.module.split(".") group = parts[0] module = parts[1] if group != current_group: current_group = group blob += "\n\n<header2>" + current_group + "</header2>" if module != current_module: current_module = module blob += "\n" + module + ":" if row.command != current_command: current_command = row.command blob += " " + self.text.make_tellcmd(row.command, "help " + row.command) return ChatBlob("Help (main)", blob) @command(command="help", params=[Any("command"), NamedFlagParameters(["show_regex"])], access_level="all", description="Show help for a specific command") def help_detail_cmd(self, request, help_topic, named_params): help_topic = help_topic.lower() # check for alias alias = self.command_alias_service.check_for_alias(help_topic) if alias: help_topic = alias help_text = self.command_service.get_help_text(request.sender.char_id, help_topic, request.channel, named_params.show_regex) if help_text: return self.command_service.format_help_text(help_topic, help_text) else: return self.getresp("module/help", "no_help", {"topic": help_topic})
class ConfigCommandController: def inject(self, registry): self.db: DB = registry.get_instance("db") self.text: Text = registry.get_instance("text") self.access_service = registry.get_instance("access_service") self.command_service = registry.get_instance("command_service") self.ts: TranslationService = registry.get_instance( "translation_service") self.getresp = self.ts.get_response @command(command="config", params=[ Const("cmd"), Any("cmd_name"), Options(["enable", "disable"]), Any("channel") ], access_level="admin", description="Enable or disable a command") def config_cmd_status_cmd(self, request, _, cmd_name, action, cmd_channel): cmd_name = cmd_name.lower() action = action.lower() cmd_channel = cmd_channel.lower() command_str, sub_command_str = self.command_service.get_command_key_parts( cmd_name) enabled = 1 if action == "enable" else 0 if cmd_channel != "all" and not self.command_service.is_command_channel( cmd_channel): return self.getresp("module/config", "cmd_unknown_channel", {"channel": cmd_channel}) if not self.has_sufficient_access_level(request.sender.char_id, command_str, sub_command_str, cmd_channel): return "You do not have the required access level to change this command." sql = "UPDATE command_config SET enabled = ? WHERE command = ? AND sub_command = ?" params = [enabled, command_str, sub_command_str] if cmd_channel != "all": sql += " AND channel = ?" params.append(cmd_channel) count = self.db.exec(sql, params) if count == 0: return self.getresp("module/config", "cmd_unknown_for_channel", { "channel": cmd_channel, "cmd": cmd_name }) else: action = self.getresp( "module/config", "enabled_low" if action == "enable" else "disabled_low") if cmd_channel == "all": return self.getresp("module/config", "cmd_toggle_success", { "cmd": cmd_name, "changedto": action }) else: return self.getresp("module/config", "cmd_toggle_channel_success", { "channel": cmd_channel, "cmd": cmd_name, "changedto": action }) @command(command="config", params=[ Const("cmd"), Any("cmd_name"), Const("access_level"), Any("channel"), Any("access_level") ], access_level="admin", description="Change access_level for a command") def config_cmd_access_level_cmd(self, request, _1, cmd_name, _2, cmd_channel, access_level): cmd_name = cmd_name.lower() cmd_channel = cmd_channel.lower() access_level = access_level.lower() command_str, sub_command_str = self.command_service.get_command_key_parts( cmd_name) if cmd_channel != "all" and not self.command_service.is_command_channel( cmd_channel): return self.getresp("module/config", "cmd_unknown_channel", {"channel": cmd_channel}) if self.access_service.get_access_level_by_label(access_level) is None: return self.getresp("module/config", "unknown_accesslevel", {"al": access_level}) if not self.has_sufficient_access_level(request.sender.char_id, command_str, sub_command_str, cmd_channel): return "You do not have the required access level to change this command." sql = "UPDATE command_config SET access_level = ? WHERE command = ? AND sub_command = ?" params = [access_level, command_str, sub_command_str] if cmd_channel != "all": sql += " AND channel = ?" params.append(cmd_channel) count = self.db.exec(sql, params) if count == 0: return self.getresp("module/config", "cmd_unknown_for_channel", { "channel": cmd_channel, "cmd": cmd_name }) else: if cmd_channel == "all": return self.getresp("module/config", "set_accesslevel_success", { "cmd": cmd_name, "al": access_level }) else: return self.getresp("module/config", "set_accesslevel_fail", { "channel": cmd_channel, "cmd": cmd_name, "al": access_level }) @command(command="config", params=[ Const("cmd"), Any("cmd_name"), NamedFlagParameters(["show_channels"]) ], access_level="admin", description="Show command configuration") def config_cmd_show_cmd(self, request, _, cmd_name, flag_params): cmd_name = cmd_name.lower() command_str, sub_command_str = self.command_service.get_command_key_parts( cmd_name) cmd_channel_configs = self.get_command_channel_config( command_str, sub_command_str) if not cmd_channel_configs: return self.getresp("module/config", "no_cmd", {"cmd": cmd_name}) blob = "" if flag_params.show_channels or not self.are_command_channel_configs_same( cmd_channel_configs): blob += self.format_cmd_channel_configs_separate_channels( cmd_name, cmd_channel_configs) else: blob += self.format_cmd_channel_configs_consolidated( cmd_name, cmd_channel_configs) sub_commands = self.get_sub_commands(command_str, sub_command_str) if sub_commands: blob += "<header2>Subcommands</header2>\n" for row in sub_commands: command_name = self.command_service.get_command_key( row.command, row.sub_command) blob += self.text.make_tellcmd( command_name, f"config cmd {command_name}") + "\n\n" # include help text blob += "\n\n".join( map(lambda handler: handler["help"], self.command_service.get_handlers(cmd_name))) return ChatBlob("Command (%s)" % cmd_name, blob) def get_sub_commands(self, command_str, sub_command_str): return self.db.query( "SELECT DISTINCT command, sub_command FROM command_config WHERE command = ? AND sub_command != ?", [command_str, sub_command_str]) def get_command_channel_config(self, command_str, sub_command_str): result = [] for command_channel, channel_label in self.command_service.channels.items( ): cmd_configs = self.command_service.get_command_configs( command=command_str, sub_command=sub_command_str, channel=command_channel, enabled=None) if len(cmd_configs) > 0: result.append( DictObject({ "channel": command_channel, "channel_label": channel_label, "cmd_config": cmd_configs[0] })) return result def are_command_channel_configs_same(self, cmd_channel_configs): if len(cmd_channel_configs) < 2: return True access_level = cmd_channel_configs[0].cmd_config.access_level enabled = cmd_channel_configs[0].cmd_config.enabled for cmd_channel_config in cmd_channel_configs[1:]: if cmd_channel_config.cmd_config.access_level != access_level or cmd_channel_config.cmd_config.enabled != enabled: return False return True def format_cmd_channel_configs_separate_channels(self, cmd_name, cmd_channel_configs): blob = "" for obj in cmd_channel_configs: cmd_config = obj.cmd_config blob += "<header2>%s</header2> " % obj.channel_label blob += self.format_cmd_config(cmd_name, cmd_config.enabled, cmd_config.access_level, obj.channel) return blob def format_cmd_channel_configs_consolidated(self, cmd_name, cmd_channel_configs): cmd_config = cmd_channel_configs[0].cmd_config channel = "all" blob = "" blob += self.format_cmd_config(cmd_name, cmd_config.enabled, cmd_config.access_level, channel) blob += self.text.make_tellcmd( "Configure command channels individually", f"config cmd {cmd_name} --show_channels") blob += "\n\n" return blob def format_cmd_config(self, cmd_name, enabled, access_level, channel): blob = "" status = self.getresp( "module/config", "enabled_high" if enabled == 1 else "disabled_high") blob += "%s (%s: %s)\n" % ( status, self.getresp("module/config", "access_level"), access_level.capitalize()) # show status config blob += "Status:" enable_link = self.text.make_tellcmd( self.getresp("module/config", "enable"), "config cmd %s enable %s" % (cmd_name, channel)) disable_link = self.text.make_tellcmd( self.getresp("module/config", "disable"), "config cmd %s disable %s" % (cmd_name, channel)) blob += " " + enable_link + " " + disable_link # show access level config blob += "\n" + self.getresp("module/config", "access_level") for access_level in self.access_service.access_levels: # skip "None" access level if access_level["level"] == 0: continue label = access_level["label"] link = self.text.make_tellcmd( label.capitalize(), "config cmd %s access_level %s %s" % (cmd_name, channel, label)) blob += " " + link blob += "\n\n" return blob def has_sufficient_access_level(self, char_id, command_str, sub_command_str, channel): access_level = self.access_service.get_access_level(char_id) params = [command_str, sub_command_str] sql = "SELECT access_level FROM command_config WHERE command = ? AND sub_command = ?" if channel != "all": sql += " AND channel = ?" params.append(channel) data = self.db.query(sql, params) for row in data: if self.access_service.compare_access_levels( row.access_level, access_level["label"]) > 0: return False return True
class CharacterInfoController: BUDDY_IS_ONLINE_TYPE = "is_online" def __init__(self): self.name_history = [] self.waiting_for_update = {} def inject(self, registry): self.bot = registry.get_instance("bot") self.db: DB = registry.get_instance("db") self.text: Text = registry.get_instance("text") self.pork_service = registry.get_instance("pork_service") self.command_alias_service = registry.get_instance( "command_alias_service") self.util = registry.get_instance("util") self.alts_service = registry.get_instance("alts_service") self.alts_controller = registry.get_instance("alts_controller") self.buddy_service = registry.get_instance("buddy_service") def pre_start(self): self.bot.register_packet_handler(CharacterName.id, self.character_name_update) def start(self): self.db.exec( "CREATE TABLE IF NOT EXISTS name_history (char_id INT NOT NULL, name VARCHAR(20) NOT NULL, created_at INT NOT NULL, PRIMARY KEY (char_id, name))" ) self.command_alias_service.add_alias("w", "whois") self.command_alias_service.add_alias("lookup", "whois") self.command_alias_service.add_alias("is", "whois") @command(command="whois", params=[ Character("character"), Int("server_num", is_optional=True), NamedFlagParameters(["force_update"]) ], access_level="member", description="Get whois information for a character", extended_description= "Use server_num 6 for RK2019 and server_num 5 for live") def whois_cmd(self, request, char, dimension, flag_params): dimension = dimension or self.bot.dimension force_update = flag_params.force_update if dimension == self.bot.dimension and char.char_id: online_status = self.buddy_service.is_online(char.char_id) if online_status is None: self.bot.register_packet_handler(BuddyAdded.id, self.handle_buddy_status) self.waiting_for_update[char.char_id] = DictObject({ "char_id": char.char_id, "name": char.name, "callback": partial(self.show_output, char, dimension, force_update, reply=request.reply, conn=request.conn) }) self.buddy_service.add_buddy(char.char_id, self.BUDDY_IS_ONLINE_TYPE) else: self.show_output(char, dimension, force_update, online_status, request.reply, request.conn) else: self.show_output(char, dimension, force_update, None, request.reply, request.conn) def show_output(self, char, dimension, force_update, online_status, reply, conn): max_cache_age = 0 if force_update else 86400 if dimension != self.bot.dimension: char_info = self.pork_service.request_char_info( char.name, dimension) else: char_info = self.pork_service.get_character_info( char.name, max_cache_age) if char_info and char_info.source != "chat_server": alts = self.alts_controller.alts_service.get_alts(char.char_id) blob = "Name: %s [%s] [%s]\n" % ( self.get_full_name(char_info), self.text.make_tellcmd( "History", "history %s %s" % (char.name, char_info.dimension)), self.text.make_tellcmd("Alts (%s)" % len(alts), f"alts {char.name}")) blob += "Char ID: %d\n" % char_info.char_id blob += "Profession: %s\n" % char_info.profession blob += "Faction: %s\n" % self.text.get_formatted_faction( char_info.faction) blob += "Breed: %s\n" % char_info.breed blob += "Gender: %s\n" % char_info.gender blob += "Level: %d\n" % char_info.level blob += "AI Level: <green>%d</green>\n" % char_info.ai_level if char_info.org_id: orglist_link = self.text.make_tellcmd( "Orglist", f"orglist {char_info.org_id}") blob += "Org: <highlight>%s</highlight> (%d) [%s]\n" % ( char_info.org_name, char_info.org_id, orglist_link) blob += "Org Rank: %s (%d)\n" % (char_info.org_rank_name, char_info.org_rank_id) else: blob += "Org: <None>\n" blob += "Org Rank: <None>\n" # blob += "Head Id: %d\n" % char_info.head_id # blob += "PVP Rating: %d\n" % char_info.pvp_rating # blob += "PVP Title: %s\n" % char_info.pvp_title blob += "Source: %s [%s]\n" % ( self.format_source(char_info, max_cache_age), self.text.make_tellcmd( "Force Update", f"whois {char.name} {dimension} --force_update")) blob += "Dimension: %s\n" % char_info.dimension if dimension == self.bot.dimension: blob += "Status: %s\n" % ("<green>Active</green>" if char. char_id else "<red>Inactive</red>") blob += self.get_name_history(char.char_id, char.name) more_info = self.text.paginate_single(ChatBlob("More Info", blob), conn) msg = self.text.format_char_info(char_info, online_status) + " " + more_info elif char.char_id: blob = "<notice>Note: Could not retrieve detailed info for character.</notice>\n\n" blob += "Name: <highlight>%s</highlight>\n" % char.name blob += "Character ID: <highlight>%d</highlight>\n" % char.char_id if online_status is not None: blob += "Online status: %s\n" % ("<green>Online</green>" if online_status else "<red>Offline</red>") blob += self.get_name_history(char.char_id, char.name) msg = ChatBlob("Basic Info for %s" % char.name, blob) else: msg = "Could not find character <highlight>%s</highlight> on RK%d." % ( char.name, dimension) reply(msg) def get_name_history(self, char_id, name): blob = "\n<header2>Name History</header2>\n" data = self.db.query( "SELECT name, char_id, created_at FROM name_history WHERE char_id = ? OR name = ? ORDER BY created_at DESC", [char_id, name]) for row in data: blob += "[%s] %s (%s)\n" % (self.util.format_date( row.created_at), row.name, row.char_id) return blob @timerevent(budatime="1min", description="Save name history", is_system=True) def save_name_history_event(self, event_type, event_data): if not self.name_history: return with self.db.transaction(): t = int(time.time()) for entry in self.name_history: sql = "INSERT IGNORE INTO name_history (char_id, name, created_at) VALUES (?, ?, ?)" self.db.exec(sql, [entry.char_id, entry.name, t]) self.name_history = [] def get_full_name(self, char_info): name = "" if char_info.first_name: name += char_info.first_name + " " name += "\"<highlight>" + char_info.name + "</highlight>\"" if char_info.last_name: name += " " + char_info.last_name return name def format_source(self, char_info, max_cache_age): if char_info.cache_age == 0: return char_info.source elif char_info.cache_age < max_cache_age: return "%s (cache; %s old)" % (char_info.source, self.util.time_to_readable( char_info.cache_age)) elif char_info.cache_age > max_cache_age: return "%s (old cache; %s old)" % (char_info.source, self.util.time_to_readable( char_info.cache_age)) def handle_buddy_status(self, conn, packet): obj = self.waiting_for_update.get(packet.char_id) if obj: self.buddy_service.remove_buddy(packet.char_id, self.BUDDY_IS_ONLINE_TYPE) del self.waiting_for_update[packet.char_id] if not self.waiting_for_update: self.bot.remove_packet_handler(BuddyAdded.id, self.handle_buddy_status) obj.callback(packet.online == 1) def character_name_update(self, conn, packet): self.name_history.append(packet)
class UtilController: def inject(self, registry): self.bot = registry.get_instance("bot") self.db = registry.get_instance("db") self.util = registry.get_instance("util") self.text = registry.get_instance("text") self.command_service = registry.get_instance("command_service") self.buddy_service = registry.get_instance("buddy_service") self.access_service = registry.get_instance("access_service") self.event_service = registry.get_instance("event_service") self.public_channel_service = registry.get_instance( "public_channel_service") @command(command="checkaccess", params=[Character("character")], access_level="moderator", description="Check access level for a character", sub_command="other") def checkaccess_other_cmd(self, request, char): if not char.char_id: return StandardMessage.char_not_found(char.name) return "Access level for <highlight>%s</highlight> is <highlight>%s</highlight> (%s)." % \ (char.name, char.access_level["label"], self.access_service.get_single_access_level(char.char_id)["label"]) @command(command="checkaccess", params=[], access_level="all", description="Check your access level") def checkaccess_cmd(self, request): char = request.sender return "Access level for <highlight>%s</highlight> is <highlight>%s</highlight> (%s)." % \ (char.name, char.access_level["label"], self.access_service.get_single_access_level(char.char_id)["label"]) @command(command="macro", params=[Any("command1|command2|command3...")], access_level="all", description="Execute multiple commands at once") def macro_cmd(self, request, commands): commands = commands.split("|") for command_str in commands: self.command_service.process_command( self.command_service.trim_command_symbol(command_str), request.channel, request.sender.char_id, request.reply, request.conn) @command(command="echo", params=[Any("message")], access_level="all", description="Echo back a message") def echo_cmd(self, request, message): return html.escape(message) @command(command="showcommand", params=[Character("character"), Any("message")], access_level="admin", description="Show command output to another character") def showcommand_cmd(self, request, char, command_str): if not char.char_id: return StandardMessage.char_not_found(char.name) self.bot.send_private_message( char.char_id, f"<highlight>{request.sender.name}</highlight> is showing you output for command <highlight>{command_str}</highlight>:", conn=request.conn) self.command_service.process_command( self.command_service.trim_command_symbol(command_str), request.channel, request.sender.char_id, lambda msg: self.bot.send_private_message( char.char_id, msg, conn=request.conn), request.conn) return f"Command <highlight>{command_str}</highlight> output has been sent to <highlight>{char.name}</highlight>." @command(command="system", params=[NamedFlagParameters(["show_all"])], access_level="admin", description="Show system information") def system_cmd(self, request, flag_params): mass_message_queue = "None" if self.bot.mass_message_queue: mass_message_queue = str(self.bot.mass_message_queue.qsize()) python_version = str(sys.version_info.major) + "." + str( sys.version_info.minor) + "." + str( sys.version_info.micro) + "." + sys.version_info.releaselevel memory_usage = self.util.format_number( psutil.Process(os.getpid()).memory_info().rss / 1024) uptime = self.util.time_to_readable(int(time.time()) - self.bot.start_time, max_levels=None) blob = f"Version: <highlight>Tyrbot {self.bot.version}</highlight>\n" blob += f"Name: <highlight><myname></highlight>\n\n" blob += f"OS: <highlight>{platform.system()} {platform.release()}</highlight>\n" blob += f"Python: <highlight>{python_version}</highlight>\n" blob += f"Database: <highlight>{self.db.type}</highlight>\n" blob += f"Memory Usage: <highlight>{memory_usage} KB</highlight>\n\n" blob += f"Superadmin: <highlight>{self.bot.superadmin}</highlight>\n" blob += f"Buddy List: <highlight>{self.buddy_service.get_buddy_list_size()}/{self.buddy_service.buddy_list_size}</highlight>\n" blob += f"Uptime: <highlight>{uptime}</highlight>\n" blob += f"Dimension: <highlight>{self.bot.dimension}</highlight>\n" blob += f"Mass Message Queue: <highlight>{mass_message_queue}</highlight>\n\n" blob += "<pagebreak><header2>Bots Connected</header2>\n" for _id, conn in self.bot.get_conns(): blob += f"<highlight>{_id}</highlight> - {conn.char_name}({conn.char_id}) " if conn.org_id: blob += f"Org: {conn.get_org_name()}({conn.org_id})" if conn.is_main: blob += " <highlight>[main]</highlight>" blob += "\n" if flag_params.show_all: for channel_id, packet in conn.channels.items(): blob += f"{packet.args}\n" blob += "\n" if not flag_params.show_all: blob += "\n" + self.text.make_tellcmd("Show More Info", "system --show_all") + "\n" else: blob += "\n" blob += "<pagebreak><header2>Event Types</header2>\n" for event_type in self.event_service.get_event_types(): blob += "%s\n" % event_type blob += "\n" blob += "<pagebreak><header2>Access Levels</header2>\n" for access_level in self.access_service.get_access_levels(): blob += "%s (%d)\n" % (access_level["label"], access_level["level"]) return ChatBlob("System Info", blob) @command( command="htmldecode", params=[Any("command")], access_level="all", description= "Decode html entities from a command before passing to the bot for execution" ) def htmldecode_cmd(self, request, command_str): self.command_service.process_command(html.unescape(command_str), request.channel, request.sender.char_id, request.reply, request.conn)
class OrgListController: ORGLIST_BUDDY_TYPE = "orglist" DEFAULT_OFFLINE_MEMBER_DISPLAY_THRESHOLD = 200 SHOW_ALL_OFFLINE_MEMBERS = 10000 def __init__(self): self.orglist = None self.governing_types = DictObject({ "Anarchism": ["Anarchist"], "Monarchy": ["Monarch", "Counsil", "Follower"], "Feudalism": ["Lord", "Knight", "Vassal", "Peasant"], "Republic": ["President", "Advisor", "Veteran", "Member", "Applicant"], "Faction": ["Director", "Board Member", "Executive", "Member", "Applicant"], "Department": [ "President", "General", "Squad Commander", "Unit Commander", "Unit Leader", "Unit Member", "Applicant" ] }) def inject(self, registry): self.bot = registry.get_instance("bot") self.db = registry.get_instance("db") self.util = registry.get_instance("util") self.text = registry.get_instance("text") self.pork_service = registry.get_instance("pork_service") self.org_pork_service = registry.get_instance("org_pork_service") self.pork_service = registry.get_instance("pork_service") self.buddy_service: BuddyService = registry.get_instance( "buddy_service") self.character_service = registry.get_instance("character_service") @command(command="orglist", params=[Int("org_id"), NamedFlagParameters(["show_all_offline"])], access_level="all", description="Show online status of characters in an org") def orglist_cmd(self, request, org_id, flag_params): self.start_orglist_lookup( request.reply, org_id, self.SHOW_ALL_OFFLINE_MEMBERS if flag_params.show_all_offline else self.DEFAULT_OFFLINE_MEMBER_DISPLAY_THRESHOLD) @command(command="orglist", params=[ Any("character|org_name|org_id"), NamedFlagParameters(["show_all_offline"]) ], access_level="all", description="Show online status of characters in an org") def orglist_character_cmd(self, request, search, flag_params): if search.isdigit(): org_id = int(search) else: orgs = self.pork_service.find_orgs(search) num_orgs = len(orgs) if num_orgs == 0: char_info = self.pork_service.get_character_info(search) if char_info: if not char_info.org_id: return "<highlight>%s</highlight> does not appear to belong to an org." % search.capitalize( ) else: org_id = char_info.org_id else: return "Could not find character or org <highlight>%s</highlight>." % search elif num_orgs == 1: org_id = orgs[0].org_id else: blob = "" for org in orgs: blob += self.text.make_tellcmd( "%s (%d)" % (org.org_name, org.org_id), "orglist %d" % org.org_id) + "\n" return ChatBlob("Org List (%d)" % num_orgs, blob) self.start_orglist_lookup( request.reply, org_id, self.SHOW_ALL_OFFLINE_MEMBERS if flag_params.show_all_offline else self.DEFAULT_OFFLINE_MEMBER_DISPLAY_THRESHOLD) def start_orglist_lookup(self, reply, org_id, offline_member_display_threshold): if self.orglist: elapsed = int(time.time()) - self.orglist.get("started_at") if elapsed > 60 * 10: reply( "Automatically ending orglist which has been running for %s (%d remaining, %d waiting, %d finished)." % (self.util.time_to_readable(elapsed), len(self.orglist.org_members), len(self.orglist.waiting_org_members), len(self.orglist.finished_org_members))) self.orglist = None self.buddy_service.remove_all_buddies_by_type( self.ORGLIST_BUDDY_TYPE) else: reply( "There is an orglist already in progress. Elapsed time: " + self.util.time_to_readable(elapsed)) return reply("Downloading org roster for org id %d..." % org_id) self.orglist = self.org_pork_service.get_org_info(org_id) if not self.orglist: reply("Could not find org with ID <highlight>%d</highlight>." % org_id) return self.orglist.started_at = int(time.time()) self.orglist.org_members = list(self.orglist.org_members.values()) self.orglist.reply = reply self.orglist.waiting_org_members = {} self.orglist.finished_org_members = {} self.orglist.offline_member_display_threshold = offline_member_display_threshold reply( "Checking online status for %d members of <highlight>%s</highlight>..." % (len(self.orglist.org_members), self.orglist.org_info.name)) # process all name lookups while self.bot.iterate(1): pass self.check_for_orglist_end() @event(event_type=BuddyService.BUDDY_LOGON_EVENT, description="Detect online buddies for orglist command", is_system=True) def buddy_logon_event(self, event_type, event_data): if self.orglist and event_data.char_id in self.orglist.waiting_org_members: self.update_online_status(event_data.char_id, True) self.check_for_orglist_end() @event(event_type=BuddyService.BUDDY_LOGOFF_EVENT, description="Detect offline buddies for orglist command", is_system=True) def buddy_logoff_event(self, event_type, event_data): if self.orglist and event_data.char_id in self.orglist.waiting_org_members: self.update_online_status(event_data.char_id, False) self.check_for_orglist_end() def update_online_status(self, char_id, status): self.orglist.finished_org_members[ char_id] = self.orglist.waiting_org_members[char_id] self.orglist.finished_org_members[char_id].online = status del self.orglist.waiting_org_members[char_id] def check_for_orglist_end(self): if self.orglist.org_members: self.iterate_org_members() if not self.orglist.waiting_org_members: self.orglist.reply(self.format_result()) self.orglist = None def format_result(self): org_ranks = {} for rank_name in self.governing_types[ self.orglist.org_info.governing_type]: org_ranks[rank_name] = DictObject({ "online_members": [], "offline_members": [] }) org_ranks["Inactive"] = DictObject({ "online_members": [], "offline_members": [] }) for char_id, org_member in self.orglist.finished_org_members.items(): if org_member.online == 2: org_ranks["Inactive"].offline_members.append(org_member) elif org_member.online == 1: org_ranks[org_member.org_rank_name].online_members.append( org_member) else: org_ranks[org_member.org_rank_name].offline_members.append( org_member) blob = "[%s] [%s] [%s]" % ( self.text.make_chatcmd( "HTML", f"/start http://people.anarchy-online.com/org/stats/d/5/name/{self.orglist.org_info.org_id}/" ), self.text.make_chatcmd( "XML", f"/start http://people.anarchy-online.com/org/stats/d/5/name/{self.orglist.org_info.org_id}/basicstats.xml" ), self.text.make_chatcmd( "JSON", f"/start http://people.anarchy-online.com/org/stats/d/5/name/{self.orglist.org_info.org_id}/basicstats.xml?data_type=json" )) if self.orglist.offline_member_display_threshold == self.DEFAULT_OFFLINE_MEMBER_DISPLAY_THRESHOLD: blob += " " + self.text.make_tellcmd( "Show all offline members", f"orglist {self.orglist.org_info.org_id} --show_all_offline") blob += "\n\n" num_online = 0 num_total = 0 for rank_name, rank_info in org_ranks.items(): rank_num_online = len(rank_info.online_members) rank_num_total = len(rank_info.offline_members) + rank_num_online blob += "<pagebreak><header2>%s (%d / %d)</header2>\n" % ( rank_name, rank_num_online, rank_num_total) num_online += rank_num_online num_total += rank_num_total for org_member in sorted(rank_info.online_members, key=lambda x: x.name): level = org_member.level if org_member.ai_level == 0 else "%d/<green>%d</green>" % ( org_member.level, org_member.ai_level) blob += "%s (Level <highlight>%s</highlight>, %s %s <highlight>%s</highlight>)\n" % ( org_member.name, level, org_member.gender, org_member.breed, org_member.profession) if rank_num_total == rank_num_online: pass elif rank_num_total < self.orglist.offline_member_display_threshold: blob += "<font color='#555555'>" + ", ".join( map( lambda x: x.name, sorted(rank_info.offline_members, key=lambda x: x.name))) + "</font>" blob += "\n" else: blob += "<font color='#555555'>Offline members omitted for brevity</font>\n" blob += "\n" return ChatBlob( "Orglist for '%s' (%d / %d)" % (self.orglist.org_info.name, num_online, num_total), blob) def iterate_org_members(self): # add org_members that we don't have online status for as buddies while self.orglist.org_members and self.buddy_list_has_available_slots( ): org_member = self.orglist.org_members.pop() char_id = org_member.char_id self.orglist.waiting_org_members[char_id] = org_member is_online = self.buddy_service.is_online(char_id) if is_online is None: if self.character_service.resolve_char_to_id(org_member.name): self.buddy_service.add_buddy(char_id, self.ORGLIST_BUDDY_TYPE) self.buddy_service.remove_buddy(char_id, self.ORGLIST_BUDDY_TYPE) else: # character is inactive, set as offline self.update_online_status(char_id, 2) else: self.update_online_status(char_id, is_online) def buddy_list_has_available_slots(self): return self.buddy_service.buddy_list_size - self.buddy_service.get_buddy_list_size( ) > 0
class BanController: def inject(self, registry): self.bot = registry.get_instance("bot") self.text = registry.get_instance("text") self.util = registry.get_instance("util") self.ban_service = registry.get_instance("ban_service") self.command_alias_service = registry.get_instance( "command_alias_service") def start(self): self.command_alias_service.add_alias("unban", "ban rem") @command(command="ban", params=[ Const("list", is_optional=True), NamedFlagParameters(["include_expired"]) ], access_level="moderator", description="Show the ban list") def ban_list_cmd(self, request, _, flag_params): t = int(time.time()) data = self.ban_service.get_ban_list(flag_params.include_expired) blob = "" for row in data: end_time = "never" if row.finished_at == -1 else self.util.format_datetime( row.finished_at) time_left = "" if row.finished_at == -1 else " (%s left)" % self.util.time_to_readable( row.finished_at - t) added_time = self.util.format_datetime(row.created_at) name = row.name if row.name else ("Unknown(%d)" % row.char_id) blob += f"<pagebreak>Name: <highlight>{name}</highlight>\n" blob += f"Added: <highlight>{added_time}</highlight>\n" blob += f"By: <highlight>{row.sender_name}</highlight>\n" blob += f"Ends: <highlight>{end_time}</highlight>{time_left}\n" blob += f"Reason: <highlight>{row.reason}</highlight>\n\n" return ChatBlob(f"Ban List ({len(data)})", blob) @command(command="ban", params=[Options(["rem", "remove"]), Character("character")], access_level="moderator", description="Remove a character from the ban list") def ban_remove_cmd(self, request, _, char): if not char.char_id: return StandardMessage.char_not_found(char.name) if not self.ban_service.get_ban(char.char_id): return f"<highlight>{char.name}</highlight> is not banned." self.ban_service.remove_ban(char.char_id, request.sender.char_id) return f"<highlight>{char.name}</highlight> has been removed from the ban list." @command(command="ban", params=[ Const("add", is_optional=True), Character("character"), Time("duration", is_optional=True), Any("reason", is_optional=True) ], access_level="moderator", description="Add a character to the ban list") def ban_add_cmd(self, request, _, char, duration, reason): if not char.char_id: return StandardMessage.char_not_found(char.name) if self.ban_service.get_ban(char.char_id): return f"<highlight>{char.name}</highlight> is already banned." if reason and len(reason) > 255: return "Ban reason cannot be more than 255 characters." self.ban_service.add_ban(char.char_id, request.sender.char_id, duration, reason) msg = f"<highlight>{char.name}</highlight> has been added to the ban list" if duration: msg += " for " msg += self.util.time_to_readable(duration) if reason: msg += " with reason: " msg += reason msg += "." return msg
class HelpController: def inject(self, registry): self.bot = registry.get_instance("bot") self.text = registry.get_instance("text") self.db = registry.get_instance("db") self.access_service = registry.get_instance("access_service") self.command_service = registry.get_instance("command_service") self.command_alias_service = registry.get_instance( "command_alias_service") def start(self): self.command_alias_service.add_alias("version", "about") @command(command="about", params=[], access_level="all", description="Show information about the development of this bot") def about_cmd(self, request): with open(os.path.dirname(os.path.realpath(__file__)) + os.sep + "about.txt", mode="r", encoding="UTF-8") as f: return ChatBlob(f"About Tyrbot {self.bot.version}", f.read()) @command(command="help", params=[], access_level="all", description="Show a list of commands to get help with") def help_list_cmd(self, request): data = self.db.query( "SELECT command, module, access_level FROM command_config " "WHERE enabled = 1 " "ORDER BY module ASC, command ASC") blob = "" current_group = "" current_module = "" current_command = "" access_level = self.access_service.get_access_level( request.sender.char_id) for row in data: if access_level[ "level"] > self.access_service.get_access_level_by_label( row.access_level)["level"]: continue parts = row.module.split(".") group = parts[0] module = parts[1] if group != current_group: current_group = group blob += "\n\n<header2>" + current_group + "</header2>" if module != current_module: current_module = module blob += "\n" + module + ":" if row.command != current_command: current_command = row.command blob += " " + self.text.make_tellcmd(row.command, "help " + row.command) return ChatBlob("Help (main)", blob) @command( command="help", params=[Any("search"), NamedFlagParameters(["show_regex"])], access_level="all", description="Show help for a specific command", extended_description= "Search param can be either a command name or a module name (eg. 'standard.online')" ) def help_detail_cmd(self, request, help_topic, named_params): help_topic = help_topic.lower() # check for alias alias = self.command_alias_service.check_for_alias(help_topic) if alias: help_topic = alias # check if help topic matches a command data = self.db.query( "SELECT command, sub_command, access_level FROM command_config " "WHERE command = ? AND channel = ? AND enabled = 1", [help_topic, request.channel]) help_text = self.command_service.format_help_text( data, request.sender.char_id, named_params.show_regex) if help_text: return self.command_service.format_help_text_blob( help_topic, help_text) # check if help topic matches a module data = self.db.query( "SELECT command, sub_command, access_level FROM command_config " "WHERE module = ? AND channel = ? AND enabled = 1", [help_topic, request.channel]) help_text = self.command_service.format_help_text( data, request.sender.char_id, named_params.show_regex) if help_text: return self.command_service.format_help_text_blob( help_topic, help_text) return f"Could not find help on <highlight>{help_topic}</highlight>."
class ConfigController: def inject(self, registry): self.db: DB = registry.get_instance("db") self.text: Text = registry.get_instance("text") self.util = registry.get_instance("util") self.command_service = registry.get_instance("command_service") self.event_service = registry.get_instance("event_service") self.setting_service = registry.get_instance("setting_service") self.config_events_controller = registry.get_instance( "config_events_controller") @command(command="config", params=[], access_level="admin", description="Show configuration options for the bot") def config_list_cmd(self, request): sql = """SELECT module, SUM(CASE WHEN enabled = 1 THEN 1 ELSE 0 END) count_enabled, SUM(CASE WHEN enabled = 0 THEN 1 ELSE 0 END) count_disabled FROM (SELECT module, enabled FROM command_config UNION SELECT module, enabled FROM event_config WHERE is_hidden = 0 UNION SELECT module, 2 FROM setting) t GROUP BY module ORDER BY module ASC""" data = self.db.query(sql) count = len(data) blob = "" current_group = "" for row in data: parts = row.module.split(".") group = parts[0] module = parts[1] if group != current_group: current_group = group blob += "\n<header2>" + current_group + "</header2>\n" blob += self.text.make_tellcmd(module, "config mod " + row.module) + " " if row.count_enabled > 0 and row.count_disabled > 0: blob += "Partial" else: blob += "<green>Enabled</green>" if row.count_disabled == 0 else "<red>Disabled</red>" blob += "\n" return ChatBlob(f"Config ({count})", blob) @command(command="config", params=[ Options(["mod", "module"]), Any("module_name"), NamedFlagParameters(["include_system_events"]) ], access_level="admin", description="Show configuration options for a specific module") def config_module_list_cmd(self, request, _, module, named_params): module = module.lower() blob = "" data = self.db.query( "SELECT name FROM setting WHERE module = ? ORDER BY name ASC", [module]) if data: blob += "<header2>Settings</header2>\n" groups = self.util.group_by(data, lambda x: x.name.split("_")[0]) for group, settings in groups.items(): for row in settings: setting = self.setting_service.get(row.name) blob += "%s - %s: %s (%s)\n" % ( setting.name, setting.get_description(), setting.get_display_value(), self.text.make_tellcmd("change", "config setting " + row.name)) blob += "\n" data = self.db.query( "SELECT DISTINCT command, sub_command FROM command_config WHERE module = ? ORDER BY command ASC", [module]) if data: blob += "<header2>Commands</header2>\n" for row in data: command_key = self.command_service.get_command_key( row.command, row.sub_command) blob += self.text.make_tellcmd( command_key, "config cmd " + command_key) + "\n" blob += self.format_events(self.get_events(module, False), "Events") system_events = self.get_events(module, True) if named_params.include_system_events: blob += self.format_events(system_events, "System Events") elif system_events: blob += "\n" + self.text.make_tellcmd( "Show system events", f"config mod {module} --include_system_events") if blob: return ChatBlob(f"Module ({module})", blob) else: return "Could not find module <highlight>{module}</highlight>." @command(command="config", params=[Const("settinglist")], access_level="admin", description="List all settings") def config_settinglist_cmd(self, request, _): blob = "" data = self.db.query("SELECT * FROM setting ORDER BY module, name ASC") count = len(data) if data: blob += "<header2>Settings</header2>\n" current_module = "" for row in data: if row.module != current_module: current_module = row.module blob += "\n<pagebreak><header2>%s</header2>\n" % row.module setting = self.setting_service.get(row.name) blob += "%s - %s: %s (%s)\n" % ( setting.name, setting.get_description(), setting.get_display_value(), self.text.make_tellcmd("change", "config setting " + row.name)) return ChatBlob(f"Settings ({count})", blob) @command(command="config", params=[ Const("setting"), Any("setting_name"), Options(["set", "clear"]), Any("new_value", is_optional=True) ], access_level="admin", description="Change a setting value") def config_setting_update_cmd(self, request, _, setting_name, op, new_value): setting_name = setting_name.lower() if op == "clear": new_value = "" elif not new_value: return "Error! New value required to update setting." setting = self.setting_service.get(setting_name) if setting: setting.set_value(new_value) if op == "clear": return f"Setting <highlight>{setting_name}</highlight> has been cleared." else: return f"Setting <highlight>{setting_name}</highlight> has been set to {setting.get_display_value()}." else: return f"Could not find setting <highlight>{setting_name}</highlight>." @command(command="config", params=[Const("setting"), Any("setting_name")], access_level="admin", description="Show configuration options for a setting") def config_setting_show_cmd(self, request, _, setting_name): setting_name = setting_name.lower() blob = "" setting = self.setting_service.get(setting_name) if setting: blob += f"Current Value: <highlight>{str(setting.get_display_value())}</highlight>\n" blob += f"Description: <highlight>{setting.get_description()}</highlight>\n\n" if setting.get_extended_description(): blob += setting.get_extended_description() + "\n\n" blob += setting.get_display() return ChatBlob(f"Setting ({setting_name})", blob) else: return f"Could not find setting <highlight>{setting_name}</highlight>." def get_events(self, module, is_system): return self.db.query( "SELECT event_type, event_sub_type, handler, description, enabled, is_hidden " f"FROM event_config WHERE module = ? AND is_hidden = ? " "ORDER BY is_hidden, event_type, handler ASC", [module, 1 if is_system else 0]) def format_events(self, data, title): blob = "" if data: blob += f"\n<header2>{title}</header2>\n" for row in data: event_type_key = self.event_service.get_event_type_key( row.event_type, row.event_sub_type) enabled = "<green>Enabled</green>" if row.enabled == 1 else "<red>Disabled</red>" blob += "%s - %s [%s]" % ( self.config_events_controller.format_event_type(row), row.description, enabled) blob += " " + self.text.make_tellcmd( "On", "config event %s %s enable" % (event_type_key, row.handler)) blob += " " + self.text.make_tellcmd( "Off", "config event %s %s disable" % (event_type_key, row.handler)) if row.event_type == "timer": blob += " " + self.text.make_tellcmd( "Run Now", "config event %s %s run" % (event_type_key, row.handler)) blob += "\n" return blob