def check(self, character: Character, target: Target) -> bool: attr = None if self.behavior['unit'] == 'Character': mapped_attr = ATTRIBUTES['Character'].get(self.behavior['attrs'], '') attr = getattr( character, mapped_attr if mapped_attr else self.behavior['attrs']) elif self.behavior['unit'] == 'Target': mapped_attr = ATTRIBUTES['Target'].get(self.behavior['attrs'], '') attr = getattr( target, mapped_attr if mapped_attr else self.behavior['attrs']) elif self.behavior['unit'] == 'Tick': attr = time.time() - self._last_tick self._last_tick = time.time() comparable_value = VALUE_CONVERTER[self.behavior['attrs']]( self.behavior['attr_value']) res = OPERATORS[self.behavior['attrs']][self.behavior['ops']]( attr, comparable_value) Logger.debug("Attribute: {} {} {} = {}".format(attr, self.behavior['ops'], comparable_value, res)) return res
def extract_data_from_screen(self, screen: Image) -> ExtractedData or None: raw_data = self._color.image_to_string(self._crop_image(screen)) data = self.parser.parse(raw_data) Logger.debug("Parsed data: {}".format(data)) return data
def turn(self, character: Character) -> (float, Direction) or None: current_trajectory = calculate_trajectory(character.position, character.facing) waypoint_trajectory = Trajectory( character.position, self.waypoints.peek(character.current_waypoint)) angle_difference, direction = current_trajectory.calculate_turn( waypoint_trajectory) # self._show_on_plot(current_trajectory, waypoint_trajectory) if angle_difference <= GlobalConfig.config.movement.turn_threshold: return None elif angle_difference >= GlobalConfig.config.movement.stop_threshold: if character.is_moving: self.controller.stop() character.is_moving = False Logger.debug('Current angle: {}'.format(character.facing)) Logger.debug('Waypoint: {} - {} rad on the {}'.format( waypoint_trajectory.end_point, angle_difference, direction.name)) if direction == Direction.left: self.controller.turn_left(transform_turn(angle_difference)) else: self.controller.turn_right(transform_turn(angle_difference)) return angle_difference, direction
class Channel(object): def __init__(self, parent, host='127.0.0.1', port=6668, name='undefined', topic='undefined'): self.notify = Logger( 'Channel') # TODO - generate unique hashes for each channel? self.parent = parent self.host = host self.port = port self.name = name self.topic = topic def setup_channel(self): self.notify.warning('attempting to establish connection to server...') with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((self.host, self.parent.port)) s.sendall(bytes([1])) try: data = s.recv(4096) self.notify.debug('received data - {}'.format(data)) self.handle_data(Packet(data)) except Exception as e: raise Exception(e) def handle_data(self, packet): if packet.data[0] == 2: self.notify.info('successfully established connection to server!')
class OakleyDbImporter(object): def __init__(self, connection_pool): self.logger = Logger(self.__class__.__name__).get() self.connection_pool = connection_pool def import_table(self, table_name, import_file): self.logger.info('importing table [{}] from file [{}]'.format( table_name, import_file)) with open(import_file, 'r') as my_file: rr = UnicodeReader(my_file) query = "INSERT INTO {} (".format(table_name) cnx = self.connection_pool.get_connection() # first line is header, column names # second line is header, column types processed_header = False processed_types = False field_types = [] for line in rr: if not processed_header: processed_header = True query += ','.join(line) query += ') VALUES (' data_str = '%s,' * len(line) query += data_str.rstrip(',') query += ')' elif processed_header and not processed_types: processed_types = True field_types = line else: cursor = cnx.cursor() for count in range(0, len(line)): if field_types[count] == 'INT' or field_types[ count] == 'BIT': line[count] = int(line[count]) elif field_types[count] == 'TIMESTAMP' and line[ count] == '0': line[count] = 0 self.logger.debug( "Updating model with query [%s] and data [%s]", query, line) cursor.execute(query, line) cnx.commit() cursor.close() self.connection_pool.release_connection(cnx) print 'Done'
def parse(self, raw: str) -> ExtractedData: raw = [r for r in raw.split('\n')] Logger.debug("Extracting raw data: {}".format(raw)) try: clean_data = self._extract_value(raw) data = ExtractedData( player_health=int(clean_data[self.ADDON_DATA_POSITION[0]]), player_resource=int(clean_data[self.ADDON_DATA_POSITION[1]]), player_position=( float(clean_data[self.ADDON_DATA_POSITION[2]]), -float(clean_data[self.ADDON_DATA_POSITION[3]])), facing=float(clean_data[self.ADDON_DATA_POSITION[4]]), combat=bool(clean_data[self.ADDON_DATA_POSITION[5][0]]), casting=CastingState( clean_data[self.ADDON_DATA_POSITION[5][1]]), last_ability=LastAbilityExecution( clean_data[self.ADDON_DATA_POSITION[5][2]]), target_health=int(clean_data[self.ADDON_DATA_POSITION[6]]), target_distance=DistanceRange( clean_data[self.ADDON_DATA_POSITION[7][0]]), target_in_combat=bool( clean_data[self.ADDON_DATA_POSITION[7][1]]), target_id=int( str(clean_data[self.ADDON_DATA_POSITION[8]])[:5], 16) if len(clean_data[self.ADDON_DATA_POSITION[8]]) > 2 else int( clean_data[self.ADDON_DATA_POSITION[8]]), target_guid=int(str(clean_data[self.ADDON_DATA_POSITION[8]]), 16) if len(clean_data[self.ADDON_DATA_POSITION[8]]) > 2 else int( clean_data[self.ADDON_DATA_POSITION[8]]), is_inventory_full=bool( clean_data[self.ADDON_DATA_POSITION[5][3]]), player_has_pet=bool( clean_data[self.ADDON_DATA_POSITION[5][4]]), player_first_resource_available=bool( clean_data[self.ADDON_DATA_POSITION[5][5]]), pet_health=int(clean_data[self.ADDON_DATA_POSITION[9]]), pet_mana=int(clean_data[self.ADDON_DATA_POSITION[10]]), ) return data except Exception as e: raise ExtractException(raw)
class AccessManager: def __init__(self): self.logger = Logger("access_manager") def inject(self, registry): self.db = registry.get_instance("db") self.character_manager = registry.get_instance("character_manager") self.alts_manager = registry.get_instance("alts_manager") def register_access_level(self, label, level, handler): self.logger.debug("Registering access level %d with label '%s'" % (level, label)) self.access_levels.append({"label": label.lower(), "level": level, "handler": handler}) self.access_levels = sorted(self.access_levels, key=lambda k: k["level"]) def get_access_levels(self): acclvls = self.db.query("SELECT * FROM access_ranks ORDER BY access_level DESC"); if len(acclvls) > 0: for row in acclvls: result.append(["name":row.name, "access_level":row.access_level, "name_short":row.name_short, "description":row.description]) return result def get_access_level(self, char): return char.access_level def get_access_level_by_level(self, level): row = self.db.query_single("SELECT * FROM access_ranks WHERE access_level == ?", [level]) if row: return row else: return None def get_access_level_by_label(self, label): row = self.db.query_single("SELECT access_level FROM access_ranks WHERE name LIKE ?", [label]) if row: return row.access_level else: return None def check_access(self, char, access_level_label): return char.highest_access <= self.get_access_level_by_label(access_level_label)["level"]
def start(self, show_window=False): self._waypoints.parse(self.config.waypoint['grind'][0]['waypoints']) self._behavior.resolve_profile(GlobalConfig.config.behavior) for screen in self._screen_interceptor.capture(): if show_window: self._show_window(screen) time_before = time.time() try: data = self._extractor.extract_data_from_screen(screen) self._data_sanitizer.sanitize_data(data) delta = time.time() - time_before Logger.debug("Elapsed time after extraction: {}".format(delta)) time.sleep(0.05) self._state_handler.update(data, screen) except ExtractException as e: # screen.save(f"errorimages\\{str(uuid.uuid4().hex)}.bmp") Logger.error( "Error while extracting data from addon. Data extracted: {}", e.partial) if self._extract_error_count <= GlobalConfig.config.core.extract_error_threshold: self._extract_error_count += 1 continue else: self._recover_error_count += 1 self._state_handler = StateHandler(self._controller, self._behavior, self._waypoints) except RecoverableException as e: if self._recover_error_count <= GlobalConfig.config.core.recoverable_error_threshold: self._recover_error_count += 1 self._state_handler = StateHandler(self._controller, self._behavior, self._waypoints) continue else: raise UnrecoverableException(str(e))
def move(self, character: Character): Logger.debug( "Following waypoint {} out of {}. Character is currently moving: {}" .format(character.current_waypoint, len(self.waypoints.waypoints) - 1, character.is_moving)) if character.position.is_close_to( self.waypoints.waypoints[character.current_waypoint], GlobalConfig.config.movement.waypoint_difference_threshold): Logger.debug("Close to waypoint") if character.current_waypoint == len(self.waypoints.waypoints) - 1: character.current_waypoint = 0 self.waypoints.reverse() else: character.current_waypoint += 1 if not self.turn(character): if not character.is_moving: Logger.debug('Moving') self.controller.move_forward() character.is_moving = True
def _check_through_screen(self, gen): Logger.debug("Entered loot state - Checking through screen") max_step_x = 20 max_step_y = 15 iter_x = int( (self.screen_res[2] - self.screen_res[0]) / max_step_x) + 1 iter_y = int( (self.screen_res[3] - self.screen_res[1]) / max_step_y) + 1 base_x = self.screen_res[0] base_y = self.screen_res[1] safe_zone = 3 max_step_x -= safe_zone max_step_y -= safe_zone Logger.debug( "Checking through screen: x-stepsize: {} y-stepsize: {}".format( iter_x, iter_y)) found = False while True: for i in range(safe_zone, max_step_x): x = base_x + iter_x * i for j in range(safe_zone * 2, max_step_y): # start from more downwards y = base_y + iter_y * j Logger.debug("Current x: {} current y: {}".format(x, y)) self.controller.move_mouse(x, y) current_screen = next(gen) found = self.scuttler.try_find( current_screen, ScreenObjects.LOOT_ICON.value) if found: self.controller.right_click() if not found: break if not found: self.finished_looting = True
class SettingService: def __init__(self): self.logger = Logger(__name__) self.settings = {} self.db_cache = {} self.change_listeners = {} def inject(self, registry): self.db = registry.get_instance("db") self.util = registry.get_instance("util") def start(self): # process decorators for _, inst in Registry.get_all_instances().items(): for name, method in get_attrs(inst).items(): if hasattr(method, "setting"): setting_name, value, description, extended_description, obj = getattr( method, "setting") self.register(inst.module_name, setting_name, value, obj, description, extended_description) def register(self, module, name, value, setting: SettingType, description, extended_description=None): """Call during start""" name = name.lower() module = module.lower() setting.set_name(name) setting.set_description(description) setting.set_extended_description(extended_description) if not description: self.logger.warning("No description specified for setting '%s'" % name) if " " in name: raise Exception( "One or more spaces found in setting name '%s' for module '%s'" % (name, module)) row = self.db.query_single( "SELECT name, value, description FROM setting WHERE name = ?", [name]) if row is None: self.logger.debug("Adding setting '%s'" % name) self.db.exec( "INSERT INTO setting (name, value, description, module, verified) VALUES (?, ?, ?, ?, ?)", [name, "", description, module, 1]) # verify default value is a valid value, and is formatted appropriately setting.set_value(value) else: self.logger.debug("Updating setting '%s'" % name) self.db.exec( "UPDATE setting SET description = ?, verified = ?, module = ? WHERE name = ?", [description, 1, module, name]) self.settings[name] = setting def register_change_listener(self, setting_name, handler): """ Call during start Args: setting_name: str handler: (name: string, old_value, new_value) -> void """ if len(inspect.signature(handler).parameters) != 3: raise Exception( "Incorrect number of arguments for handler '%s.%s()'" % (handler.__module__, handler.__name__)) if setting_name in self.settings: if setting_name not in self.change_listeners: self.change_listeners[setting_name] = [] self.change_listeners[setting_name].append(handler) else: raise Exception( "Could not register change_listener for setting '%s' since it does not exist" % setting_name) def get_value(self, name): # check cache first result = self.db_cache.get(name, None) if result: return result.value else: row = self.db.query_single( "SELECT value FROM setting WHERE name = ?", [name]) # store result in cache self.db_cache[name] = row return row.value if row else None def set_value(self, name, value): old_value = self.get_value(name) # clear cache self.db_cache[name] = None self.db.exec("UPDATE setting SET value = ? WHERE name = ?", [value, name]) if name in self.change_listeners: for change_listener in self.change_listeners[name]: change_listener(name, old_value, value) def get(self, name): name = name.lower() setting = self.settings.get(name, None) if setting: return setting else: return None
def main(argv=None): parser = argparse.ArgumentParser( description="Client tool for changing boot order via Redfish API.") parser.add_argument("-H", help="iDRAC host address") parser.add_argument("-u", help="iDRAC username", required=True) parser.add_argument("-p", help="iDRAC password", required=True) parser.add_argument("-i", help="Path to iDRAC interfaces yaml", default=None) parser.add_argument("-t", help="Type of host. Accepts: foreman, director") parser.add_argument("-l", "--log", help="Optional argument for logging results to a file") parser.add_argument("-f", "--force", dest='force', action='store_true', help="Optional argument for forced clear-jobs") parser.add_argument("--host-list", help="Path to a plain text file with a list of hosts.", default=None) parser.add_argument("--pxe", help="Set next boot to one-shot boot PXE", action="store_true") parser.add_argument( "--boot-to", help="Set next boot to one-shot boot to a specific device") parser.add_argument( "--boot-to-type", help="Set next boot to one-shot boot to either director or foreman") parser.add_argument( "--boot-to-mac", help= "Set next boot to one-shot boot to a specific MAC address on the target" ) parser.add_argument("--reboot-only", help="Flag for only rebooting the host", action="store_true") parser.add_argument( "--power-cycle", help="Flag for sending ForceOff instruction to the host", action="store_true") parser.add_argument("--racreset", help="Flag for iDRAC reset", action="store_true") parser.add_argument("--check-boot", help="Flag for checking the host boot order", action="store_true") parser.add_argument("--firmware-inventory", help="Get firmware inventory", action="store_true") parser.add_argument("--export-configuration", help="Export system configuration to XML", action="store_true") parser.add_argument("--clear-jobs", help="Clear any schedule jobs from the queue", action="store_true") parser.add_argument("-v", "--verbose", help="Verbose output", action="store_true") parser.add_argument("-r", "--retries", help="Number of retries for executing actions.", default=RETRIES) args = vars(parser.parse_args(argv)) log_level = DEBUG if args["verbose"] else INFO logger = Logger() logger.start(level=log_level) if args["log"]: file_handler = FileHandler(args["log"]) file_handler.setFormatter(Formatter(logger.LOGFMT)) file_handler.setLevel(DEBUG) logger.addHandler(file_handler) host_list = args["host_list"] host = args["H"] if host_list: try: with open(host_list, "r") as _file: for _host in _file.readlines(): try: execute_badfish(_host.strip(), args, logger) except SystemExit: continue except IOError as ex: logger.debug(ex) logger.error("There was something wrong reading from %s" % host_list) elif not host: logger.error( "You must specify at least either a host (-H) or a host list (--host-list)." ) else: execute_badfish(host, args, logger) return 0
class PorkService: def __init__(self): self.logger = Logger(__name__) def inject(self, registry): self.bot = registry.get_instance("bot") self.db = registry.get_instance("db") self.character_service = registry.get_instance("character_service") def pre_start(self): self.bot.add_packet_handler(server_packets.CharacterLookup.id, self.update) self.bot.add_packet_handler(server_packets.CharacterName.id, self.update) def request_char_info(self, char_name, server_num): url = self.get_pork_url(server_num, char_name) try: r = requests.get(url, timeout=5) result = r.json() except ReadTimeout: self.logger.warning("Timeout while requesting '%s'" % url) result = None except ValueError as e: self.logger.debug( "Error marshalling value as json for url '%s': %s" % (url, r.text), e) result = None char_info = None if result: char_info_json = result[0] org_info_json = result[1] if result[1] else {} char_info = DictObject({ "name": char_info_json["NAME"], "char_id": char_info_json["CHAR_INSTANCE"], "first_name": char_info_json["FIRSTNAME"], "last_name": char_info_json["LASTNAME"], "level": char_info_json["LEVELX"], "breed": char_info_json["BREED"], "dimension": char_info_json["CHAR_DIMENSION"], "gender": char_info_json["SEX"], "faction": char_info_json["SIDE"], "profession": char_info_json["PROF"], "profession_title": char_info_json["PROFNAME"], "ai_rank": char_info_json["RANK_name"], "ai_level": char_info_json["ALIENLEVEL"], "pvp_rating": char_info_json["PVPRATING"], "pvp_title": char_info_json["PVPTITLE"] or "", "head_id": char_info_json["HEADID"], "org_id": org_info_json.get("ORG_INSTANCE", 0), "org_name": org_info_json.get("NAME", ""), "org_rank_name": org_info_json.get("RANK_TITLE", ""), "org_rank_id": org_info_json.get("RANK", 0), "source": "people.anarchy-online.com", "cache_age": 0 }) return char_info def get_character_info(self, char, max_cache_age=86400): char_id = self.character_service.resolve_char_to_id(char) char_name = self.character_service.resolve_char_to_name(char) t = int(time.time()) # if there is an entry in database and it is within the cache time, use that db_char_info = self.get_from_database(char_id=char_id, char_name=char_name) if db_char_info: db_char_info.cache_age = t - db_char_info.last_updated if db_char_info.cache_age < max_cache_age and db_char_info.source != "chat_server": return db_char_info # if we can't resolve to a char_name, we can't make a call to pork if not char_name: return db_char_info char_info = self.request_char_info(char_name, self.bot.dimension) if char_info and char_info.char_id == char_id: self.save_character_info(char_info) return char_info else: # return cached info from database, even tho it's old, and set cache_age (if it exists) if db_char_info: db_char_info.cache_age = t - db_char_info.last_updated return db_char_info def load_character_info(self, char_id): char_info = self.get_character_info(char_id) if not char_info: char_info = DictObject({ "name": "Unknown:" + str(char_id), "char_id": char_id, "first_name": "", "last_name": "", "level": 0, "breed": "", "dimension": self.bot.dimension, "gender": "", "faction": "", "profession": "", "profession_title": "", "ai_rank": "", "ai_level": 0, "pvp_rating": 0, "pvp_title": "", "head_id": 0, "org_id": 0, "org_name": "", "org_rank_name": "", "org_rank_id": 6, "source": "stub" }) self.save_character_info(char_info) def save_character_info(self, char_info): if char_info["dimension"] != self.bot.dimension: return self.db.exec("DELETE FROM player WHERE char_id = ?", [char_info["char_id"]]) insert_sql = """ INSERT INTO player ( char_id, name, first_name, last_name, level, breed, gender, faction, profession, profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, dimension, head_id, pvp_rating, pvp_title, source, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ self.db.exec(insert_sql, [ char_info["char_id"], char_info["name"], char_info["first_name"], char_info["last_name"], char_info["level"], char_info["breed"], char_info["gender"], char_info["faction"], char_info["profession"], char_info["profession_title"], char_info["ai_rank"], char_info["ai_level"], char_info["org_id"], char_info["org_name"], char_info["org_rank_name"], char_info["org_rank_id"], char_info["dimension"], char_info["head_id"], char_info["pvp_rating"], char_info["pvp_title"], char_info["source"], int(time.time()) ]) def get_from_database(self, char_id=None, char_name=None): if char_id: return self.db.query_single( "SELECT char_id, name, first_name, last_name, level, breed, gender, faction, profession, " "profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, " "dimension, head_id, pvp_rating, pvp_title, source, last_updated " "FROM player WHERE char_id = ?", [char_id]) elif char_name: return self.db.query_single( "SELECT char_id, name, first_name, last_name, level, breed, gender, faction, profession, " "profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, " "dimension, head_id, pvp_rating, pvp_title, source, last_updated " "FROM player WHERE name = ?", [char_name]) else: return None def update(self, packet): # don't update if we didn't get a valid response if packet.char_id == 4294967295: return character = self.get_from_database(char_id=packet.char_id) if character: if character.name != packet.name: self.db.exec("UPDATE player SET name = ? WHERE char_id = ?", [packet.name, packet.char_id]) else: insert_sql = """ INSERT INTO player ( char_id, name, first_name, last_name, level, breed, gender, faction, profession, profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, dimension, head_id, pvp_rating, pvp_title, source, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""" self.db.exec(insert_sql, [ packet.char_id, packet.name, "", "", 0, "", "", "", "", "", "", 0, 0, "", "", 6, self.bot.dimension, 0, 0, "", "chat_server", int(time.time()) ]) def find_orgs(self, search): return self.db.query( "SELECT DISTINCT org_name, org_id FROM player WHERE org_name <EXTENDED_LIKE=0> ?", [search], extended_like=True) def get_pork_url(self, dimension, char_name): return "http://people.anarchy-online.com/character/bio/d/%d/name/%s/bio.xml?data_type=json" % ( dimension, char_name)
class DB: SQLITE = "sqlite" MYSQL = "mysql" def __init__(self): self.conn = None self.enhanced_like_regex = re.compile( r"(\s+)(\S+)\s+<EXTENDED_LIKE=(\d+)>\s+\?(\s*)", re.IGNORECASE) self.lastrowid = None self.logger = Logger(__name__) self.type = None self.transaction_level = 0 def sqlite_row_factory(self, cursor: sqlite3.Cursor, row): d = {} for idx, col in enumerate(cursor.description): d[col[0]] = row[idx] return d def connect_mysql(self, host, port, username, password, database_name): self.type = self.MYSQL self.conn = mysql.connector.connect(user=username, password=password, host=host, port=port, database=database_name, charset="utf8", autocommit=True) self.exec("SET collation_connection = 'utf8_general_ci'") self.exec("SET sql_mode = 'TRADITIONAL,ANSI'") self.create_db_version_table() def connect_sqlite(self, filename): self.type = self.SQLITE self.conn = sqlite3.connect(filename, isolation_level=None, check_same_thread=False) self.conn.row_factory = self.sqlite_row_factory self.create_db_version_table() def create_db_version_table(self): self.exec( "CREATE TABLE IF NOT EXISTS db_version (file VARCHAR(255) NOT NULL, version VARCHAR(255) NOT NULL, verified SMALLINT NOT NULL)" ) def _execute_wrapper(self, sql, params, callback): if self.type == self.MYSQL: # buffered=True - https://stackoverflow.com/a/33632767/280574 cur = self.conn.cursor(dictionary=True, buffered=True) else: cur = self.conn.cursor() start_time = time.time() try: cur.execute( sql if self.type == self.SQLITE else sql.replace("?", "%s"), params) except Exception as e: raise SqlException( "SQL Error: '%s' for '%s' [%s]" % (str(e), sql, ", ".join(map(lambda x: str(x), params)))) from e elapsed = time.time() - start_time if elapsed > 0.5: self.logger.warning("slow query (%fs) '%s' for params: %s" % (elapsed, sql, str(params))) result = callback(cur) cur.close() return result def query_single(self, sql, params=None, extended_like=False): if params is None: params = [] if extended_like: sql, params = self.handle_extended_like(sql, params) sql, params = self.format_sql(sql, params) def map_result(cur): row = cur.fetchone() return DictObject(row) if row else None return self._execute_wrapper(sql, params, map_result) def query(self, sql, params=None, extended_like=False): if params is None: params = [] if extended_like: sql, params = self.handle_extended_like(sql, params) sql, params = self.format_sql(sql, params) def map_result(cur): return list(map(lambda row: DictObject(row), cur.fetchall())) return self._execute_wrapper(sql, params, map_result) def exec(self, sql, params=None, extended_like=False): if params is None: params = [] if extended_like: sql, params = self.handle_extended_like(sql, params) sql, params = self.format_sql(sql, params) def map_result(cur): return [cur.rowcount, cur.lastrowid] row_count, lastrowid = self._execute_wrapper(sql, params, map_result) self.lastrowid = lastrowid return row_count def last_insert_id(self): return self.lastrowid def format_sql(self, sql, params=None): # TODO check for AUTOINCREMENT in sql and log warning if self.type == self.SQLITE: sql = sql.replace("AUTO_INCREMENT", "AUTOINCREMENT") sql = sql.replace(" INT ", " INTEGER ") sql = sql.replace("INSERT IGNORE", "INSERT OR IGNORE") return sql, params def handle_extended_like(self, sql, params): original_params = params.copy() params = list(map(lambda x: [x], params)) for match in self.enhanced_like_regex.finditer(sql): field = match.group(2) index = int(match.group(3)) extra_sql, vals = self._get_extended_params( field, original_params[index].split(" ")) sql = self.enhanced_like_regex.sub( match.group(1) + "(" + " AND ".join(extra_sql) + ")" + match.group(4), sql, 1) # remove current param and add generated params in its place del params[index] params.insert(index, vals) return sql, [item for sublist in params for item in sublist] def _get_extended_params(self, field, params): extra_sql = [] vals = [] for p in params: if p.startswith("-") and p != "-": vals.append("%" + p[1:] + "%") extra_sql.append(field + " NOT LIKE ?") else: vals.append("%" + p + "%") extra_sql.append(field + " LIKE ?") return extra_sql, vals def get_connection(self): return self.conn def load_sql_file(self, sqlfile, base_path): filename = base_path + os.sep + sqlfile db_version = self.get_db_version(filename) file_version = self.get_file_version(filename) if db_version: if parse_version(file_version) > parse_version(db_version): self.logger.debug("loading sql file '%s'" % sqlfile) self._load_file(filename) self.exec( "UPDATE db_version SET version = ?, verified = 1 WHERE file = ?", [int(file_version), filename]) else: self.logger.debug("loading sql file '%s'" % sqlfile) self._load_file(filename) self.exec( "INSERT INTO db_version (file, version, verified) VALUES (?, ?, 1)", [filename, int(file_version)]) def get_file_version(self, filename): return str(int(os.path.getmtime(filename))) def get_db_version(self, filename): row = self.query_single( "SELECT version FROM db_version WHERE file = ?", [filename]) if row: return row.version else: return None def _load_file(self, filename): with open(filename, mode="r", encoding="UTF-8") as f: with self.transaction(): cur = self.conn.cursor() line_num = 1 for line in f.readlines(): try: sql, _ = self.format_sql(line) sql = sql.strip() if sql and not sql.startswith("--"): cur.execute(sql) except Exception as e: raise Exception( "sql error in file '%s' on line %d: %s" % (filename, line_num, str(e))) line_num += 1 cur.close() def get_type(self): return self.type # transaction support def transaction(self): return self def __enter__(self): # called when entering `with` code block self.begin_transaction() def __exit__(self, exc_type, exc_val, exc_tb): # called when exiting `with` code block # if exc_type, exc_val or exc_tb is not None, there was an exception # otherwise the code block exited normally if exc_type is None: self.commit_transaction() else: self.rollback_transaction() # False here indicates that if there was an exception, it should not be suppressed but instead propagated return False def begin_transaction(self): if self.transaction_level == 0: self.exec("BEGIN;") self.transaction_level += 1 def commit_transaction(self): if self.transaction_level == 1: self.exec("COMMIT;") self.transaction_level -= 1 def rollback_transaction(self): if self.transaction_level == 1: self.exec("ROLLBACK;") self.transaction_level -= 1
class AccessService: def __init__(self): self.access_levels = [{ "label": "none", "level": 0, "handler": self.no_access }, { "label": "all", "level": 100, "handler": self.all_access }] self.logger = Logger(__name__) def inject(self, registry): self.character_service = registry.get_instance("character_service") self.alts_service = registry.get_instance("alts_service") def register_access_level(self, label, level, handler): self.logger.debug("Registering access level %d with label '%s'" % (level, label)) self.access_levels.append({ "label": label.lower(), "level": level, "handler": handler }) self.access_levels = sorted(self.access_levels, key=lambda k: k["level"]) def get_access_levels(self): return self.access_levels def get_access_level(self, char_id): access_level1 = self.get_single_access_level(char_id) alts = self.alts_service.get_alts(char_id) if not alts: return access_level1 main = alts[0] if main.char_id == char_id: return access_level1 else: access_level2 = self.get_single_access_level(main.char_id) if access_level1["level"] < access_level2["level"]: return access_level1 else: return access_level2 def compare_access_levels(self, access_level1, access_level2): """ Returns a positive number if the access_level1 is greater than access_level2, a negative number if access_level1 is less than access_level2, and 0 if the access levels are equal. :param access_level1: :param access_level2: :return: int """ a1 = self.get_access_level_by_label(access_level1) a2 = self.get_access_level_by_label(access_level2) return a2["level"] - a1["level"] def has_sufficient_access_level(self, char_id1, char_id2): """ Returns True if char1 has a higher access level than char2 or if char1 is a verified alt of char2, and False otherwise. :param char_id1: :param char_id2: :return: """ # return True if char_ids are the same if char_id1 == char_id2: return True # return True if both chars have the same main if self.alts_service.get_main( char_id1).char_id == self.alts_service.get_main( char_id2).char_id: return True a1 = self.get_access_level(char_id1) a2 = self.get_access_level(char_id2) return a2["level"] - a1["level"] > 0 def get_single_access_level(self, char): char_id = self.character_service.resolve_char_to_id(char) for access_level in self.access_levels: if access_level["handler"](char_id): return access_level def get_access_level_by_level(self, level): for access_level in self.access_levels: if access_level["level"] == level: return access_level return None def get_access_level_by_label(self, label): label = label.lower() for access_level in self.access_levels: if access_level["label"] == label: return access_level return None def check_access(self, char, access_level_label): char_id = self.character_service.resolve_char_to_id(char) if not char_id: return None return self.get_access_level( char)["level"] <= self.get_access_level_by_label( access_level_label)["level"] def no_access(self, char_id): return False def all_access(self, char_id): return True
class BuddyController: def __init__(self): self.logger = Logger(__name__) def inject(self, registry): self.bot = registry.get_instance("bot") self.character_service = registry.get_instance("character_service") self.buddy_service = registry.get_instance("buddy_service") self.ts: TranslationService = registry.get_instance("translation_service") self.getresp = self.ts.get_response @command(command="buddylist", params=[], access_level="admin", description="Show characters on the buddy list") def buddylist_cmd(self, request): buddy_list = [] for char_id, buddy in self.buddy_service.get_all_buddies().items(): char_name = self.character_service.resolve_char_to_name(char_id, "Unknown(%d)" % char_id) buddy_list.append([char_name, buddy]) blob = self.format_buddies(buddy_list) return ChatBlob(f"Buddy list ({len(buddy_list)})", blob) @command(command="buddylist", params=[Const("add"), Character("character"), Any("type")], access_level="admin", description="Add a character to the buddy list") def buddylist_add_cmd(self, request, _, char, buddy_type): buddy_type = buddy_type.lower() if char.char_id: self.buddy_service.add_buddy(char.char_id, buddy_type) return f"Character <highlight>{char.name}</highlight> has been added to the buddy list for type <highlight>{buddy_type}</highlight>." else: return self.getresp("global", "char_not_found", {"char": char.name}) @command(command="buddylist", params=[Options(["rem", "remove"]), Const("all")], access_level="admin", description="Remove all characters from the buddy list") def buddylist_remove_all_cmd(self, request, _1, _2): buddies = self.buddy_service.get_all_buddies().items() for char_id, buddy in buddies: self.buddy_service.remove_buddy(char_id, None, True) return f"Removed all <highlight>{len(buddies)}</highlight> buddies from the buddy list." @command(command="buddylist", params=[Options(["rem", "remove"]), Character("character"), Any("type")], access_level="admin", description="Remove a character from the buddy list by type") def buddylist_remove_cmd(self, request, _, char, buddy_type): buddy_type = buddy_type.lower() if char.char_id: self.buddy_service.remove_buddy(char.char_id, buddy_type) return f"Character <highlight>{char.name}</highlight> has been removed from the buddy list for type <highlight>{buddy_type}</highlight>." else: return self.getresp("global", "char_not_found", {"char": char.name}) @command(command="buddylist", params=[Options(["rem", "remove"]), Character("character")], access_level="admin", description="Remove a character from the buddy list forcefully") def buddylist_remove_force_cmd(self, request, _, char): if char.char_id: self.buddy_service.remove_buddy(char.char_id, None, force_remove=True) return f"Character <highlight>{char.name}</highlight> has been removed from the buddy list forcefully." else: return self.getresp("global", "char_not_found", {"char": char.name}) @command(command="buddylist", params=[Const("clean")], access_level="admin", description="Remove all orphaned buddies from the buddy list") def buddylist_clean_cmd(self, request, _): num_removed = self.remove_orphaned_buddies() return f"Removed <highlight>{num_removed}</highlight> orphaned buddies from the buddy list." @command(command="buddylist", params=[Const("search"), Any("character")], access_level="admin", description="Search for characters on the buddy list") def buddylist_search_cmd(self, request, _, search): search = search.lower() buddy_list = [] for char_id, buddy in self.buddy_service.get_all_buddies().items(): char_name = self.character_service.resolve_char_to_name(char_id, "Unknown(%d)" % char_id) if search in char_name.lower(): buddy_list.append([char_name, buddy["online"], ",".join(buddy["types"])]) blob = self.format_buddies(buddy_list) return ChatBlob(f"Buddy List Search Results ({len(buddy_list)})", blob) @timerevent(budatime="24h", description="Remove orphaned buddies", is_hidden=True) def remove_orphaned_buddies_event(self, event_type, event_data): if self.bot.is_ready(): self.logger.debug("Removing %d orphaned buddies" % self.remove_orphaned_buddies()) def remove_orphaned_buddies(self): count = 0 for char_id, buddy in self.buddy_service.get_all_buddies().items(): if len(buddy["types"]) == 0: self.buddy_service.remove_buddy(char_id, None, True) count += 1 return count def format_buddies(self, buddy_list): buddy_list = sorted(buddy_list, key=lambda x: x[0]) blob = "" for name, buddy in buddy_list: blob += "%s(%s) - %s\n" % (name, buddy["conn_id"], ",".join(buddy["types"])) return blob
class Server(object): def __init__(self, host='127.0.0.1', port=6667, backlog=10000): self.notify = Logger('Server') self.host = host self.port = port self.backlog = backlog self.channels = {} # store connected channels to the server self.nicknames = [ ] # registered nicknames across all channels on the server def start_server(self): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_addr = (self.host, self.port) self.notify.info('starting server at {0}:{1}'.format(*server_addr)) s.bind(server_addr) s.listen(self.backlog) while True: conn, client_addr = s.accept() try: self.notify.debug( 'incoming connection from {0}:{1}'.format(*client_addr)) # TODO - determine whether new suggestion is a channel or a client while True: data = conn.recv(1024) self.notify.debug( 'incoming data received from {0}:{1} - {2}'.format( client_addr[0], client_addr[1], data)) if data: # TODO - handle packet data self.handle_data(Packet(data), conn) else: self.notify.warning( 'invalid data received from {0}:{1} - {2}'.format( client_addr[0], client_addr[1], data)) break finally: conn.close() def handle_data(self, packet, conn): if packet.data[0] == 1: try: self.register_channel(conn) self.notify.info( 'successfully registered new channel to server!') self.notify.debug(self.channels) except: raise Exception( 'unable to register new channel - is the client still connected to the server?' ) def register_channel(self, conn): try: channel = self.channels[len(self.channels)] + 1 self.channels[channel] = conn conn.sendall([2]) except KeyError: channel = 1000000 self.channels[channel] = conn conn.sendall([2])
class PorkService: def __init__(self): self.logger = Logger(__name__) def inject(self, registry): self.bot = registry.get_instance("bot") self.db = registry.get_instance("db") self.character_service = registry.get_instance("character_service") def pre_start(self): self.bot.register_packet_handler(server_packets.CharacterLookup.id, self.update) self.bot.register_packet_handler(server_packets.CharacterName.id, self.update) def start(self): self.db.exec( "CREATE TABLE IF NOT EXISTS player ( char_id BIGINT PRIMARY KEY, first_name VARCHAR(30) NOT NULL, name VARCHAR(20) NOT NULL, last_name VARCHAR(30) NOT NULL, " "level SMALLINT NOT NULL, breed VARCHAR(20) NOT NULL, gender VARCHAR(20) NOT NULL, faction VARCHAR(20) NOT NULL, profession VARCHAR(20) NOT NULL, " "profession_title VARCHAR(50) NOT NULL, ai_rank VARCHAR(20) NOT NULL, ai_level SMALLINT, org_id INT DEFAULT NULL, org_name VARCHAR(255) NOT NULL, " "org_rank_name VARCHAR(20) NOT NULL, org_rank_id SMALLINT NOT NULL, dimension SMALLINT NOT NULL, head_id INT NOT NULL, pvp_rating SMALLINT NOT NULL, " "pvp_title VARCHAR(20) NOT NULL, source VARCHAR(50) NOT NULL, last_updated INT NOT NULL )" ) # forces a lookup from remote PoRK server # this should not be called directly unless you are requesting info for a char on a different server # since cache will not be used and the result will not update the cache automatically def request_char_info(self, char_name, server_num): url = self.get_pork_url(server_num, char_name) try: r = requests.get(url, timeout=5) result = r.json() except ReadTimeout: self.logger.warning("Timeout while requesting '%s'" % url) result = None except ValueError as e: self.logger.debug( "Error marshalling value as json for url '%s': %s" % (url, r.text), e) result = None char_info = None if result: char_info_json = result[0] org_info_json = result[1] if result[1] else {} char_info = DictObject({ "name": char_info_json["NAME"], "char_id": char_info_json["CHAR_INSTANCE"], "first_name": char_info_json["FIRSTNAME"], "last_name": char_info_json["LASTNAME"], "level": char_info_json["LEVELX"], "breed": char_info_json["BREED"], "dimension": char_info_json["CHAR_DIMENSION"], "gender": char_info_json["SEX"], "faction": char_info_json["SIDE"], "profession": char_info_json["PROF"], "profession_title": char_info_json["PROFNAME"], "ai_rank": char_info_json["RANK_name"], "ai_level": char_info_json["ALIENLEVEL"], "pvp_rating": char_info_json["PVPRATING"], "pvp_title": char_info_json["PVPTITLE"] or "", "head_id": char_info_json["HEADID"], "org_id": org_info_json.get("ORG_INSTANCE", 0), "org_name": org_info_json.get("NAME", ""), "org_rank_name": org_info_json.get("RANK_TITLE", ""), "org_rank_id": org_info_json.get("RANK", 0), "source": "people.anarchy-online.com", "cache_age": 0 }) return char_info # standard method to get character pork data when character is on the same server def get_character_info(self, char_name_or_id, max_cache_age=86400): char_id = self.character_service.resolve_char_to_id(char_name_or_id) char_name = self.character_service.resolve_char_to_name( char_name_or_id) t = int(time.time()) # if there is an entry in database and it is within the cache time, use that db_char_info = self.get_from_database(char_id=char_id, char_name=char_name) if db_char_info: db_char_info.cache_age = t - db_char_info.last_updated if db_char_info.cache_age < max_cache_age and db_char_info.source != "chat_server": return db_char_info # if we can't resolve to a char_name, we can't make a call to pork if not char_name: return db_char_info char_info = self.request_char_info(char_name, self.bot.dimension) if char_info and (char_id is None or char_info.char_id == char_id): self.save_character_info(char_info) return char_info else: # return cached info from database, even tho it's old, and set cache_age (if it exists) if db_char_info: db_char_info.cache_age = t - db_char_info.last_updated return db_char_info # forces a skeleton object into the player table in the case that PoRK does not return any data # call this method if you don't need the data now but want to ensure there is a record in the database def load_character_info(self, char_id, char_name=None): char_info = self.get_character_info(char_id) if not char_info and char_name: char_info = self.get_character_info(char_name) if not char_info: char_info = DictObject({ "name": "Unknown:" + str(char_id), "char_id": char_id, "first_name": "", "last_name": "", "level": 0, "breed": "", "dimension": self.bot.dimension, "gender": "", "faction": "", "profession": "", "profession_title": "", "ai_rank": "", "ai_level": 0, "pvp_rating": 0, "pvp_title": "", "head_id": 0, "org_id": 0, "org_name": "", "org_rank_name": "", "org_rank_id": 6, "source": "stub" }) self.save_character_info(char_info) def save_character_info(self, char_info): if char_info["dimension"] != self.bot.dimension: return self.db.exec("DELETE FROM player WHERE char_id = ?", [char_info["char_id"]]) insert_sql = """ INSERT IGNORE INTO player ( char_id, name, first_name, last_name, level, breed, gender, faction, profession, profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, dimension, head_id, pvp_rating, pvp_title, source, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """ self.db.exec(insert_sql, [ char_info["char_id"], char_info["name"], char_info["first_name"], char_info["last_name"], char_info["level"], char_info["breed"], char_info["gender"], char_info["faction"], char_info["profession"], char_info["profession_title"], char_info["ai_rank"], char_info["ai_level"], char_info["org_id"], char_info["org_name"], char_info["org_rank_name"], char_info["org_rank_id"], char_info["dimension"], char_info["head_id"], char_info["pvp_rating"], char_info["pvp_title"], char_info["source"], int(time.time()) ]) def get_from_database(self, char_id=None, char_name=None): if char_id: return self.db.query_single( "SELECT char_id, name, first_name, last_name, level, breed, gender, faction, profession, " "profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, " "dimension, head_id, pvp_rating, pvp_title, source, last_updated " "FROM player WHERE char_id = ?", [char_id]) elif char_name: return self.db.query_single( "SELECT char_id, name, first_name, last_name, level, breed, gender, faction, profession, " "profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, " "dimension, head_id, pvp_rating, pvp_title, source, last_updated " "FROM player WHERE name = ?", [char_name]) else: return None def update(self, conn, packet): # don't update if we didn't get a valid response if packet.char_id == 4294967295: return character = self.get_from_database(char_id=packet.char_id) if character: if character.name != packet.name: self.db.exec("UPDATE player SET name = ? WHERE char_id = ?", [packet.name, packet.char_id]) else: insert_sql = """ INSERT IGNORE INTO player ( char_id, name, first_name, last_name, level, breed, gender, faction, profession, profession_title, ai_rank, ai_level, org_id, org_name, org_rank_name, org_rank_id, dimension, head_id, pvp_rating, pvp_title, source, last_updated) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""" self.db.exec(insert_sql, [ packet.char_id, packet.name, "", "", 0, "", "", "", "", "", "", 0, 0, "", "", 6, self.bot.dimension, 0, 0, "", "chat_server", int(time.time()) ]) def find_orgs(self, search): return self.db.query( "SELECT DISTINCT org_name, org_id FROM player WHERE org_name <EXTENDED_LIKE=0> ?", [search], extended_like=True) def get_pork_url(self, dimension, char_name): return "http://people.anarchy-online.com/character/bio/d/%d/name/%s/bio.xml?data_type=json" % ( dimension, char_name)
class EventService: def __init__(self): self.handlers = {} self.logger = Logger(__name__) self.event_types = [] self.db_cache = {} def inject(self, registry): self.db = registry.get_instance("db") self.util = registry.get_instance("util") def pre_start(self): self.register_event_type("timer") def start(self): # process decorators for _, inst in Registry.get_all_instances().items(): for name, method in get_attrs(inst).items(): if hasattr(method, "event"): attrs = getattr(method, "event") handler = getattr(inst, name) self.register(handler, attrs.event_type, attrs.description, inst.module_name, attrs.is_hidden, attrs.is_enabled) def register_event_type(self, event_type): """ Call during pre_start Args: event_type (str) """ event_type = event_type.lower() if event_type in self.event_types: self.logger.error( "Could not register event type '%s': event type already registered" % event_type) return self.logger.debug("Registering event type '%s'" % event_type) self.event_types.append(event_type) def is_event_type(self, event_base_type): return event_base_type in self.event_types def register(self, handler, event_type, description, module, is_hidden, is_enabled): """ Call during pre_start Args: handler: (event_type, event_data) -> void event_type: str description: str module: str is_hidden: bool is_enabled: bool """ if len(inspect.signature(handler).parameters) != 2: raise Exception( "Incorrect number of arguments for handler '%s.%s()'" % (handler.__module__, handler.__name__)) event_base_type, event_sub_type = self.get_event_type_parts(event_type) module = module.lower() handler_name = self.util.get_handler_name(handler) is_hidden = 1 if is_hidden else 0 is_enabled = 1 if is_enabled else 0 if event_base_type not in self.event_types: self.logger.error( "Could not register handler '%s' for event type '%s': event type does not exist" % (handler_name, event_type)) return if not description: self.logger.warning( "No description for event_type '%s' and handler '%s'" % (event_type, handler_name)) row = self.db.query_single( "SELECT 1 FROM event_config WHERE event_type = ? AND handler = ?", [event_base_type, handler_name]) if row is None: # add new event commands self.db.exec( "INSERT INTO event_config (event_type, event_sub_type, handler, description, module, enabled, verified, is_hidden) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [ event_base_type, event_sub_type, handler_name, description, module, is_enabled, 1, is_hidden ]) if event_base_type == "timer": self.db.exec( "INSERT INTO timer_event (event_type, event_sub_type, handler, next_run) VALUES (?, ?, ?, ?)", [ event_base_type, event_sub_type, handler_name, int(time.time()) ]) else: # mark command as verified self.db.exec( "UPDATE event_config SET verified = ?, module = ?, description = ?, event_sub_type = ?, is_hidden = ? WHERE event_type = ? AND handler = ?", [ 1, module, description, event_sub_type, is_hidden, event_base_type, handler_name ]) if event_base_type == "timer": self.db.exec( "UPDATE timer_event SET event_sub_type = ? WHERE event_type = ? AND handler = ?", [event_sub_type, event_base_type, handler_name]) # load command handler self.handlers[handler_name] = handler def fire_event(self, event_type, event_data=None): event_base_type, event_sub_type = self.get_event_type_parts(event_type) if event_base_type not in self.event_types: self.logger.error( "Could not fire event type '%s': event type does not exist" % event_type) return data = self.get_handlers(event_base_type, event_sub_type) for row in data: self.call_handler(row.handler, event_type, event_data) def call_handler(self, handler_method, event_type, event_data): handler = self.handlers.get(handler_method, None) if not handler: self.logger.error( "Could not find handler callback for event type '%s' and handler '%s'" % (event_type, handler_method)) return try: handler(event_type, event_data) except Exception as e: self.logger.error("error processing event '%s'" % event_type, e) def get_event_type_parts(self, event_type): parts = event_type.lower().split(":", 1) if len(parts) == 2: return parts[0], parts[1] else: return parts[0], "" def get_event_type_key(self, event_base_type, event_sub_type): return event_base_type + ":" + event_sub_type def check_for_timer_events(self, current_timestamp): data = self.db.query( "SELECT e.event_type, e.event_sub_type, e.handler, t.next_run FROM timer_event t " "JOIN event_config e ON t.event_type = e.event_type AND t.handler = e.handler " "WHERE t.next_run <= ? AND e.enabled = 1", [current_timestamp]) for row in data: self.execute_timed_event(row, current_timestamp) def execute_timed_event(self, row, current_timestamp): event_type_key = self.get_event_type_key(row.event_type, row.event_sub_type) # timer event run times should be consistent, so we base the next run time off the last run time, # instead of the current timestamp next_run = row.next_run + int(row.event_sub_type) # prevents timer events from getting too far behind, or having a large "catch-up" after # the bot has been offline for a time if next_run < current_timestamp: next_run = current_timestamp + int(row.event_sub_type) with self.db.transaction(): self.db.exec( "UPDATE timer_event SET next_run = ? WHERE event_type = ? AND handler = ?", [next_run, row.event_type, row.handler]) self.call_handler(row.handler, event_type_key, None) def update_event_status(self, event_base_type, event_sub_type, event_handler, enabled_status): # clear cache self.db_cache[event_base_type + ":" + event_sub_type] = None return self.db.exec( "UPDATE event_config SET enabled = ? WHERE event_type = ? AND event_sub_type = ? AND handler LIKE ?", [enabled_status, event_base_type, event_sub_type, event_handler]) def get_event_types(self): return self.event_types def get_handlers(self, event_base_type, event_sub_type): # check first in cache result = self.db_cache.get(event_base_type + ":" + event_sub_type, None) if result is not None: return result else: result = self.db.query( "SELECT handler FROM event_config WHERE event_type = ? AND event_sub_type = ? AND enabled = 1", [event_base_type, event_sub_type]) # store result in cache self.db_cache[event_base_type + ":" + event_sub_type] = result return result def run_timer_events_at_startup(self): t = int(time.time()) data = self.db.query( "SELECT e.event_type, e.event_sub_type, e.handler, t.next_run FROM timer_event t " "JOIN event_config e ON t.event_type = e.event_type AND t.handler = e.handler " "WHERE e.event_type = ? AND e.enabled = 1", ["timer"]) for row in data: handler = self.handlers[row.handler] attrs = getattr(handler, "event") if attrs.get("run_at_startup", False): self.execute_timed_event(row, t)
class MappingDal(object): def __init__(self, connection_pool): self.logger = Logger(self.__class__.__name__).get() self.connection_pool = connection_pool def style_family_mapping_exists(self, style_id, family_id): mapping_id = self.get_style_family_mapping_id(style_id, family_id) return mapping_id != -1 def get_style_family_mapping_id(self, style_id, family_id): query = ("SELECT id, styleid, familyid FROM familystylemap " "WHERE styleid = %s " "AND familyid = %s " "AND validfrom < %s " "AND ((validto = '0000-00-00 00:00:00') OR (validto >= %s))") now = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime()) cnx = self.connection_pool.get_connection() cursor = cnx.cursor() data = (style_id, family_id, now, now) self.logger.debug("Getting mapping with query [%s] and data [%s]", query, data) cursor.execute(query, data) mapping_id = -1 for (c_id, c_styleid, c_familyid) in cursor: if c_styleid == style_id and c_familyid == family_id: mapping_id = c_id cursor.close() self.connection_pool.release_connection(cnx) return mapping_id def get_last_style_family_mapping_id(self): style_query = "SELECT MAX(id) FROM familystylemap" cnx = self.connection_pool.get_connection() cursor = cnx.cursor() cursor.execute(style_query) ret_id = -1 for c_id in cursor: if c_id is not None and c_id[0] is not None: ret_id = int(c_id[0]) cursor.close() self.connection_pool.release_connection(cnx) return ret_id def insert_style_family_mapping(self, style_id, family_id, source_id): query = ("INSERT INTO familystylemap " "(styleid, familyid, sourceid, validfrom) " "VALUES (%s, %s, %s, %s)") now = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime()) cnx = self.connection_pool.get_connection() cursor = cnx.cursor() data = (style_id, family_id, source_id, now) cursor.execute(query, data) cnx.commit() mapping_id = int(cursor.lastrowid) cursor.close() self.connection_pool.release_connection(cnx) return mapping_id
if "features" in config: for k, v in config.features.items(): k = k.upper() logger.info("Feature %s: %s" % (k, v)) setattr(FeatureFlags, k, v) if platform.system() == "Windows": os.system("title %s.%d" % (config.bots[0].character, config.server.dimension)) # paths to search for instances: core + module_paths paths = ["core"] paths.extend(config.module_paths) # load instances logger.debug("Loading instances") Registry.load_instances(paths) Registry.inject_all() # configure database db = Registry.get_instance("db") if config.database.type == "sqlite": db.connect_sqlite("./data/" + config.database.name) elif config.database.type == "mysql": db.connect_mysql(config.database.host, config.database.port, config.database.username, config.database.password, config.database.name) else: raise Exception("Unknown database type '%s'" % config.database.type) # run db upgrade scripts
class CommandService: PRIVATE_MESSAGE_CHANNEL = "msg" def __init__(self): self.handlers = collections.defaultdict(list) self.logger = Logger(__name__) self.channels = {} self.pre_processors = [] self.ignore_regexes = [ re.compile(r" is AFK \(Away from keyboard\) since ", re.IGNORECASE), re.compile(r"I am away from my keyboard right now", re.IGNORECASE), re.compile(r"Unknown command or access denied!", re.IGNORECASE), re.compile(r"I am responding", re.IGNORECASE), re.compile(r"I only listen", re.IGNORECASE), re.compile(r"Error!", re.IGNORECASE), re.compile(r"Unknown command input", re.IGNORECASE), re.compile(r"You have been auto invited", re.IGNORECASE), re.compile(r"^<font") ] def inject(self, registry): self.db = registry.get_instance("db") self.util = registry.get_instance("util") self.access_service: AccessService = registry.get_instance( "access_service") self.bot = registry.get_instance("bot") self.character_service: CharacterService = registry.get_instance( "character_service") self.event_service = registry.get_instance("event_service") self.setting_service: SettingService = registry.get_instance( "setting_service") self.command_alias_service = registry.get_instance( "command_alias_service") self.usage_service = registry.get_instance("usage_service") self.public_channel_service = registry.get_instance( "public_channel_service") self.ban_service = registry.get_instance("ban_service") def pre_start(self): self.bot.register_packet_handler(server_packets.PrivateMessage.id, self.handle_private_message) self.register_command_channel("Private Message", self.PRIVATE_MESSAGE_CHANNEL) def start(self): access_levels = {} # process decorators for _, inst in Registry.get_all_instances().items(): for name, method in get_attrs(inst).items(): if hasattr(method, "command"): cmd_name, params, access_level, description, help_file, sub_command, extended_description = getattr( method, "command") handler = getattr(inst, name) help_text = self.get_help_file(inst.module_name, help_file) command_key = self.get_command_key( cmd_name.lower(), sub_command.lower() if sub_command else "") al = access_levels.get(command_key, None) if al is not None and al != access_level.lower(): raise Exception( "Different access levels specified for forms of command '%s'" % command_key) access_levels[command_key] = access_level self.register(handler, cmd_name, params, access_level, description, inst.module_name, help_text, sub_command, extended_description) def register(self, handler, command, params, access_level, description, module, help_text=None, sub_command=None, extended_description=None, check_access=None): """ Call during pre_start Args: handler: (request, param1, param2, ...) -> str|ChatBlob|None command: str params: [CommandParam...] access_level: str description: str module: str help_text: str sub_command: str extended_description: str check_access: (char_id, access_level_label) -> bool """ if len(inspect.signature(handler).parameters) != len(params) + 1: raise Exception( "Incorrect number of arguments for handler '%s.%s()'" % (handler.__module__, handler.__qualname__)) command = command.lower() if sub_command: sub_command = sub_command.lower() else: sub_command = "" access_level = access_level.lower() module = module.lower() command_key = self.get_command_key(command, sub_command) if help_text is None: help_text = self.generate_help(command, description, params, extended_description) if check_access is None: check_access = self.access_service.check_access if not self.access_service.get_access_level_by_label(access_level): self.logger.error( "Could not add command '%s': could not find access level '%s'" % (command, access_level)) return for channel, label in self.channels.items(): row = self.db.query_single( "SELECT access_level, module, enabled, verified " "FROM command_config " "WHERE command = ? AND sub_command = ? AND channel = ?", [command, sub_command, channel]) if row is None: # add new command self.db.exec( "INSERT INTO command_config " "(command, sub_command, access_level, channel, module, enabled, verified) " "VALUES (?, ?, ?, ?, ?, 1, 1)", [command, sub_command, access_level, channel, module]) elif row.verified: if row.module != module: self.logger.warning( "module different for different forms of command '%s' and sub_command '%s'" % (command, sub_command)) else: # mark command as verified self.db.exec( "UPDATE command_config SET verified = 1, module = ? " "WHERE command = ? AND sub_command = ? AND channel = ?", [module, command, sub_command, channel]) # save reference to command handler r = re.compile(self.get_regex_from_params(params), re.IGNORECASE | re.DOTALL) self.handlers[command_key].append({ "regex": r, "callback": handler, "help": help_text, "description": description, "params": params, "check_access": check_access }) def register_command_pre_processor(self, pre_processor): """ Call during start Args: pre_processor: (context) -> bool """ self.pre_processors.append(pre_processor) def register_command_channel(self, label, value): """ Call during pre_start Args: label: str value: str """ if value in self.channels: self.logger.error( "Could not register command channel '%s': command channel already registered" % value) return self.logger.debug("Registering command channel '%s'" % value) self.channels[value] = label def is_command_channel(self, channel): return channel in self.channels def process_command(self, message: str, channel: str, char_id, reply, conn): try: context = DictObject({ "message": message, "char_id": char_id, "channel": channel, "reply": reply }) for pre_processor in self.pre_processors: if pre_processor(context) is False: return for regex in self.ignore_regexes: if regex.search(message): return # message = html.unescape(message) command_str, command_args = self.get_command_parts(message) # check for command alias command_alias_str = self.command_alias_service.get_alias_command_str( command_str, command_args) alias_depth_count = 0 while command_alias_str: alias_depth_count += 1 command_str, command_args = self.get_command_parts( command_alias_str) command_alias_str = self.command_alias_service.get_alias_command_str( command_str, command_args) if alias_depth_count > 20: raise Exception( "Command alias infinite recursion detected for command '%s'" % message) cmd_configs = self.get_command_configs(command_str, channel, 1) access_level = self.access_service.get_access_level(char_id) sender = SenderObj( char_id, self.character_service.resolve_char_to_name( char_id, "Unknown(%d)" % char_id), access_level) if cmd_configs: # given a list of cmd_configs that are enabled, see if one has regex that matches incoming command_str cmd_config, matches, handler = self.get_matches( cmd_configs, command_args) if matches: if handler["check_access"](char_id, cmd_config.access_level): response = handler["callback"]( CommandRequest(conn, channel, sender, reply), *self.process_matches(matches, handler["params"])) if response is not None: reply(response) # record command usage self.usage_service.add_usage( command_str, self.util.get_handler_name(handler["callback"]), char_id, channel) else: self.access_denied_response(message, sender, cmd_config, reply) else: # handlers were found, but no handler regex matched data = self.db.query( "SELECT command, sub_command, access_level FROM command_config " "WHERE command = ? AND channel = ? AND enabled = 1", [command_str, channel]) help_text = self.format_help_text(data, char_id) if help_text: reply( self.format_help_text_blob(command_str, help_text)) else: # the command is known, but no help is returned, therefore character does not have access to command reply("Access denied.") else: self.handle_unknown_command(command_str, command_args, channel, sender, reply) except Exception as e: self.logger.error("error processing command: %s" % message, e) reply("There was an error processing your request.") def handle_unknown_command(self, command_str, command_args, channel, sender, reply): reply(f"Error! Unknown command <highlight>{command_str}</highlight>.") def access_denied_response(self, message, sender, cmd_config, reply): reply("Access denied.") def get_command_parts(self, message): parts = message.split(" ", 1) if len(parts) == 2: return parts[0].lower(), " " + parts[1] else: return parts[0].lower(), "" def get_command_configs(self, command, channel=None, enabled=1, sub_command=None): sql = "SELECT command, sub_command, access_level, channel, enabled FROM command_config WHERE command = ?" params = [command] if channel: sql += " AND channel = ?" params.append(channel) if enabled: sql += " AND enabled = ?" params.append(enabled) if sub_command: sql += " AND sub_command = ?" params.append(sub_command) sql += " ORDER BY sub_command, channel" return self.db.query(sql, params) def get_matches(self, cmd_configs, command_args): for row in cmd_configs: command_key = self.get_command_key(row.command, row.sub_command) handlers = self.handlers[command_key] for handler in handlers: matches = handler["regex"].search(command_args) if matches: return row, matches, handler return None, None, None def process_matches(self, matches, params): groups = list(matches.groups()) processed = [] for param in params: processed.append(param.process_matches(groups)) return processed def format_help_text(self, data, char_id, show_regex=False): # filter out commands that character does not have access level for data = filter( lambda row: self.access_service.check_access( char_id, row.access_level), data) def get_regex(params): if show_regex: return "\n" + self.get_regex_from_params(params) else: return "" def read_help_text(row): command_key = self.get_command_key(row.command, row.sub_command) return filter( lambda x: x is not None, map( lambda handler: handler["help"] + get_regex(handler[ "params"]), self.handlers[command_key])) content = "\n\n".join(flatmap(read_help_text, data)) return content if content else None def format_help_text_blob(self, topic, help_text): return ChatBlob("Help (" + topic + ")", help_text) def get_help_file(self, module, help_file): if help_file: try: help_file = "./" + module.replace(".", "/") + "/" + help_file with open(help_file, mode="r", encoding="UTF-8") as f: return f.read().strip() except FileNotFoundError as e: self.logger.error("Error reading help file", e) return None def get_command_key(self, command, sub_command): if sub_command: return command + ":" + sub_command else: return command def get_command_key_parts(self, command_str): parts = command_str.split(":", 1) if len(parts) == 2: return parts[0], parts[1] else: return parts[0], "" def get_regex_from_params(self, params): # params must be wrapped with line-beginning and line-ending anchors in order to match # when no params are specified (eg. "^$") return "^" + "".join(map(lambda x: x.get_regex(), params)) + "$" def generate_help(self, command, description, params, extended_description=None): help_text = description + ":\n" + "<tab><symbol>" + command + " " + " ".join( map(lambda x: x.get_name(), params)) if extended_description: help_text += "\n" + extended_description return help_text def get_handlers(self, command_key): return self.handlers.get(command_key, None) def handle_private_message(self, conn: Conn, packet: server_packets.PrivateMessage): if not self.setting_service.get("accept_commands_from_slave_bots" ).get_value() and not conn.is_main: return # since the command symbol is not required for private messages, # the command_str must have length of at least 1 in order to be valid, # otherwise it is ignored if len(packet.message) < 1: return # ignore leading space message = packet.message.lstrip() def reply(msg): if self.bot.mass_message_queue and FeatureFlags.FORCE_LARGE_MESSAGES_FROM_SLAVES and \ isinstance(msg, ChatBlob) and len(msg.msg) > FeatureFlags.FORCE_LARGE_MESSAGES_FROM_SLAVES_THRESHOLD: self.bot.send_mass_message(packet.char_id, msg, conn=conn) else: self.bot.send_private_message(packet.char_id, msg, conn=conn) self.process_command(self.trim_command_symbol(message), self.PRIVATE_MESSAGE_CHANNEL, packet.char_id, reply, conn) def trim_command_symbol(self, s): symbol = self.setting_service.get("symbol").get_value() if s.startswith(symbol): s = s[len(symbol):] return s
class BuddyController: def __init__(self): self.logger = Logger(__name__) def inject(self, registry): self.bot = registry.get_instance("bot") self.character_service = registry.get_instance("character_service") self.buddy_service = registry.get_instance("buddy_service") self.ts: TranslationService = registry.get_instance("translation_service") self.getresp = self.ts.get_response def start(self): self.ts.register_translation("module/buddy", self.load_buddy_msg) def load_buddy_msg(self): with open("modules/core/buddy/buddy.msg", mode="r", encoding="UTF-8") as f: return hjson.load(f) @command(command="buddylist", params=[], access_level="admin", description="Show characters on the buddy list") def buddylist_cmd(self, request): buddy_list = [] for char_id, buddy in self.buddy_service.get_all_buddies().items(): char_name = self.character_service.resolve_char_to_name(char_id, "Unknown(%d)" % char_id) buddy_list.append([char_name, buddy["online"], ",".join(buddy["types"])]) blob = self.format_buddies(buddy_list) return ChatBlob(self.getresp("module/buddy", "blob_title", {"amount": len(buddy_list)}), blob) @command(command="buddylist", params=[Const("add"), Character("character"), Any("type")], access_level="admin", description="Add a character to the buddy list") def buddylist_add_cmd(self, request, _, char, buddy_type): buddy_type = buddy_type.lower() if char.char_id: self.buddy_service.add_buddy(char.char_id, buddy_type) return self.getresp("module/buddy", "add_success", {"char": char.name, "type": buddy_type}) else: return self.getresp("global", "char_not_found", {"char": char.name}) @command(command="buddylist", params=[Options(["rem", "remove"]), Const("all")], access_level="admin", description="Remove all characters from the buddy list") def buddylist_remove_all_cmd(self, request, _1, _2): count = 0 for char_id, buddy in self.buddy_service.get_all_buddies().items(): self.buddy_service.remove_buddy(char_id, None, True) count += 1 return self.getresp("module/buddy", "rem_all", {"count": count}) @command(command="buddylist", params=[Options(["rem", "remove"]), Character("character"), Any("type")], access_level="admin", description="Remove a character from the buddy list") def buddylist_remove_cmd(self, request, _, char, buddy_type): buddy_type = buddy_type.lower() if char.char_id: self.buddy_service.remove_buddy(char.char_id, buddy_type) return self.getresp("module/buddy", "rem_single", {"char": char.name, "type": buddy_type}) else: return self.getresp("global", "char_not_found", {"char": char.name}) @command(command="buddylist", params=[Const("clean")], access_level="admin", description="Remove all orphaned buddies from the buddy list") def buddylist_clean_cmd(self, request, _): return self.getresp("module/buddy", "rem_orphaned", {"count":self.remove_orphaned_buddies()}) @command(command="buddylist", params=[Const("search"), Any("character")], access_level="admin", description="Search for characters on the buddy list") def buddylist_search_cmd(self, request, _, search): search = search.lower() buddy_list = [] for char_id, buddy in self.buddy_service.get_all_buddies().items(): char_name = self.character_service.resolve_char_to_name(char_id, "Unknown(%d)" % char_id) if search in char_name.lower(): buddy_list.append([char_name, buddy["online"], ",".join(buddy["types"])]) blob = self.format_buddies(buddy_list) return ChatBlob(self.getresp("module/buddy", "search_title", {"amount": len(buddy_list)}), blob) @timerevent(budatime="24h", description="Remove orphaned buddies") def remove_orphaned_buddies_event(self, event_type, event_data): self.logger.debug("removing %d orphaned buddies" % self.remove_orphaned_buddies()) def remove_orphaned_buddies(self): count = 0 for char_id, buddy in self.buddy_service.get_all_buddies().items(): if len(buddy["types"]) == 0: self.buddy_service.remove_buddy(char_id, None, True) count += 1 return count def format_buddies(self, buddy_list): buddy_list = sorted(buddy_list, key=lambda x: x[0]) blob = "" for name, online, types in buddy_list: blob += "%s - %s\n" % (name, types) return blob
class SettingService: def __init__(self): self.logger = Logger(__name__) self.settings = {} self.db_cache = {} def inject(self, registry): self.db = registry.get_instance("db") self.util = registry.get_instance("util") def start(self): # process decorators for _, inst in Registry.get_all_instances().items(): for name, method in get_attrs(inst).items(): if hasattr(method, "setting"): setting_name, value, description, obj = getattr(method, "setting") handler = getattr(inst, name) module = self.util.get_module_name(handler) self.register(setting_name, value, description, obj, module) def register(self, name, value, description, setting: SettingType, module): name = name.lower() module = module.lower() setting.set_name(name) setting.set_description(description) if not description: self.logger.warning("No description specified for setting '%s'" % name) if " " in name: raise Exception("One or more spaces found in setting name '%s' for module '%s'" % (name, module)) row = self.db.query_single("SELECT name, value, description FROM setting WHERE name = ?", [name]) if row is None: self.logger.debug("Adding setting '%s'" % name) self.db.exec( "INSERT INTO setting (name, value, description, module, verified) VALUES (?, ?, ?, ?, ?)", [name, "", description, module, 1]) # verify default value is a valid value, and is formatted appropriately setting.set_value(value) else: self.logger.debug("Updating setting '%s'" % name) self.db.exec( "UPDATE setting SET description = ?, verified = ?, module = ? WHERE name = ?", [description, 1, module, name]) self.settings[name] = setting def get_value(self, name): # check cache first result = self.db_cache.get(name, None) if result: return result.value else: row = self.db.query_single("SELECT value FROM setting WHERE name = ?", [name]) # store result in cache self.db_cache[name] = row return row.value if row else None def set_value(self, name, value): # clear cache self.db_cache[name] = None self.db.exec("UPDATE setting SET value = ? WHERE name = ?", [value, name]) def get(self, name): name = name.lower() setting = self.settings.get(name, None) if setting: return setting else: return None
def spawn(func, *args, **kwargs): thread_name = kwargs.pop('thread_name', None) or get_func_name(func) def wrapper(thread_name, args, kwargs): try: func(*args, **kwargs) except Exception, ex: log.error('Thread "%s" raised an exception: %s', thread_name, ex, exc_info=True) th = threading.Thread(target=wrapper, name=thread_name, args=(thread_name, args, kwargs)) try: th.start() log.debug("Spawned thread with name '%s'" % thread_name) except thread.error, ex: log.error('Unable to spawn thread: %s', ex, exc_info=True, extra={ 'data': { 'active_count': threading.active_count() } }) return None return th def schedule(func, seconds, *args, **kwargs): def schedule_sleep(): time.sleep(seconds) func(*args, **kwargs)
class Tyrbot: CONNECT_EVENT = "connect" PRIVATE_MSG_EVENT = "private_msg" def __init__(self): super().__init__() self.logger = Logger(__name__) self.ready = False self.packet_handlers = {} self.superadmin = None self.status: BotStatus = BotStatus.SHUTDOWN self.dimension = None self.last_timer_event = 0 self.start_time = int(time.time()) self.version = "0.7-beta" self.incoming_queue = FifoQueue() self.mass_message_queue = None self.conns = DictObject() self.primary_conn_id = None def inject(self, registry): self.db = registry.get_instance("db") self.character_service: CharacterService = registry.get_instance( "character_service") self.public_channel_service: PublicChannelService = registry.get_instance( "public_channel_service") self.text: Text = registry.get_instance("text") self.setting_service: SettingService = registry.get_instance( "setting_service") self.access_service: AccessService = registry.get_instance( "access_service") self.event_service = registry.get_instance("event_service") self.job_scheduler = registry.get_instance("job_scheduler") def init(self, config, registry, mmdb_parser): self.mmdb_parser = mmdb_parser self.superadmin = config.superadmin.capitalize() self.dimension = config.server.dimension self.db.exec( "CREATE TABLE IF NOT EXISTS command_config (command VARCHAR(50) NOT NULL, sub_command VARCHAR(50) NOT NULL, access_level VARCHAR(50) NOT NULL, channel VARCHAR(50) NOT NULL, " "module VARCHAR(50) NOT NULL, enabled SMALLINT NOT NULL, verified SMALLINT NOT NULL)" ) self.db.exec( "CREATE TABLE IF NOT EXISTS event_config (event_type VARCHAR(50) NOT NULL, event_sub_type VARCHAR(50) NOT NULL, handler VARCHAR(255) NOT NULL, description VARCHAR(255) NOT NULL, " "module VARCHAR(50) NOT NULL, enabled SMALLINT NOT NULL, verified SMALLINT NOT NULL, is_hidden SMALLINT NOT NULL)" ) self.db.exec( "CREATE TABLE IF NOT EXISTS timer_event (event_type VARCHAR(50) NOT NULL, event_sub_type VARCHAR(50) NOT NULL, handler VARCHAR(255) NOT NULL, next_run INT NOT NULL)" ) self.db.exec( "CREATE TABLE IF NOT EXISTS setting (name VARCHAR(50) NOT NULL, value VARCHAR(255) NOT NULL, description VARCHAR(255) NOT NULL, module VARCHAR(50) NOT NULL, verified SMALLINT NOT NULL)" ) self.db.exec( "CREATE TABLE IF NOT EXISTS command_alias (alias VARCHAR(50) NOT NULL, command VARCHAR(1024) NOT NULL, enabled SMALLINT NOT NULL)" ) self.db.exec( "CREATE TABLE IF NOT EXISTS command_usage (command VARCHAR(255) NOT NULL, handler VARCHAR(255) NOT NULL, char_id INT NOT NULL, channel VARCHAR(20) NOT NULL, created_at INT NOT NULL)" ) self.db.exec( "CREATE TABLE IF NOT EXISTS ban_list (char_id INT NOT NULL, sender_char_id INT NOT NULL, created_at INT NOT NULL, finished_at INT NOT NULL, reason VARCHAR(255) NOT NULL, ended_early SMALLINT NOT NULL)" ) self.db.exec("UPDATE db_version SET verified = 0") self.db.exec( "UPDATE db_version SET verified = 1 WHERE file = 'db_version'") # prepare commands, events, and settings self.db.exec("UPDATE command_config SET verified = 0") self.db.exec("UPDATE event_config SET verified = 0") self.db.exec("UPDATE setting SET verified = 0") with self.db.transaction(): registry.pre_start_all() registry.start_all() # remove commands, events, and settings that are no longer registered self.db.exec("DELETE FROM db_version WHERE verified = 0") self.db.exec("DELETE FROM command_config WHERE verified = 0") self.db.exec("DELETE FROM event_config WHERE verified = 0") self.db.exec( "DELETE FROM timer_event WHERE handler NOT IN (SELECT handler FROM event_config WHERE event_type = ?)", ["timer"]) self.db.exec("DELETE FROM setting WHERE verified = 0") self.status = BotStatus.RUN def pre_start(self): self.access_service.register_access_level("superadmin", 10, self.check_superadmin) self.event_service.register_event_type(self.CONNECT_EVENT) self.event_service.register_event_type(self.PRIVATE_MSG_EVENT) def start(self): self.setting_service.register( "core.system", "symbol", "!", TextSettingType(["!", "#", "*", "@", "$", "+", "-"]), "Symbol for executing bot commands") self.setting_service.register( "core.system", "org_channel_max_page_length", 7500, NumberSettingType([4500, 6000, 7500, 9000, 10500, 12000]), "Maximum size of blobs in org channel") self.setting_service.register( "core.system", "private_message_max_page_length", 7500, NumberSettingType([4500, 6000, 7500, 9000, 10500, 12000]), "Maximum size of blobs in private messages") self.setting_service.register( "core.system", "private_channel_max_page_length", 7500, NumberSettingType([4500, 6000, 7500, 9000, 10500, 12000]), "Maximum size of blobs in private channel") self.setting_service.register( "core.system", "accept_commands_from_slave_bots", False, BooleanSettingType(), "Accept and respond to commands sent to slave bots (only applies if you have added slave bots in the config)" ) self.setting_service.register("core.colors", "header_color", "#FFFF00", ColorSettingType(), "Color for headers") self.setting_service.register("core.colors", "header2_color", "#FCA712", ColorSettingType(), "Color for sub-headers") self.setting_service.register("core.colors", "highlight_color", "#00BFFF", ColorSettingType(), "Color for highlight") self.setting_service.register("core.colors", "notice_color", "#FF8C00", ColorSettingType(), "Color for important notices") self.setting_service.register("core.colors", "neutral_color", "#E6E1A6", ColorSettingType(), "Color for neutral faction") self.setting_service.register("core.colors", "omni_color", "#FA8484", ColorSettingType(), "Color for omni faction") self.setting_service.register("core.colors", "clan_color", "#F79410", ColorSettingType(), "Color for clan faction") self.setting_service.register("core.colors", "unknown_color", "#FF0000", ColorSettingType(), "Color for unknown faction") self.setting_service.register("core.colors", "org_channel_color", "#89D2E8", ColorSettingType(), "Default org channel color") self.setting_service.register("core.colors", "private_channel_color", "#89D2E8", ColorSettingType(), "Default private channel color") self.setting_service.register("core.colors", "private_message_color", "#89D2E8", ColorSettingType(), "Default private message color") self.setting_service.register("core.colors", "blob_color", "#FFFFFF", ColorSettingType(), "Default blob content color") self.register_packet_handler(server_packets.PrivateMessage.id, self.handle_private_message, priority=40) def check_superadmin(self, char_id): char_name = self.character_service.resolve_char_to_name(char_id) return char_name == self.superadmin def connect(self, config): for i, bot in enumerate(config.bots): if "id" in bot: _id = bot.id else: _id = "bot" + str(i) if i == 0: self.primary_conn_id = _id conn = self.create_conn(_id) conn.connect(config.server.host, config.server.port) # only create the mass_message_queue if there is at least 1 non-main bot if not bot.is_main and not self.mass_message_queue: self.mass_message_queue = FifoQueue() packet = conn.login(bot.username, bot.password, bot.character, is_main=bot.is_main) if not packet: self.status = BotStatus.ERROR return False else: self.incoming_queue.put((conn, packet)) self.create_conn_thread( conn, None if bot.is_main else self.mass_message_queue) return True def create_conn_thread(self, conn: Conn, mass_message_queue=None): def read_packets(): try: while self.status == BotStatus.RUN: packet = conn.read_packet(1) if packet: self.incoming_queue.put((conn, packet)) while mass_message_queue and not mass_message_queue.empty( ) and conn.packet_queue.is_empty(): packet = mass_message_queue.get_or_default(block=False) if packet: conn.add_packet_to_queue(packet) except (EOFError, OSError) as e: self.status = BotStatus.ERROR self.logger.error("", e) raise e dthread = threading.Thread(target=read_packets, daemon=True) dthread.start() def create_conn(self, _id): if _id in self.conns: raise Exception(f"A connection with id {_id} already exists") def failure_callback(): self.status = BotStatus.ERROR conn = Conn(_id, failure_callback) self.conns[_id] = conn return conn def disconnect(self): # wait for all threads to stop reading packets, then disconnect them all time.sleep(2) for _id, conn in self.get_conns(): conn.disconnect() def run(self): start = time.time() # wait for flood of packets from login to stop sending time_waited = 0 while time_waited < 2: if not self.iterate(1): time_waited += 1 self.logger.info("Login complete (%fs)" % (time.time() - start)) start = time.time() self.event_service.fire_event("connect", None) self.event_service.run_timer_events_at_startup() self.event_service.check_for_timer_events(int(start)) self.logger.info("Connect events finished (%fs)" % (time.time() - start)) time_waited = 0 while time_waited < 2: if not self.iterate(1): time_waited += 1 self.ready = True timestamp = int(time.time()) while self.status == BotStatus.RUN: try: timestamp = int(time.time()) self.check_for_timer_events(timestamp) self.iterate() except Exception as e: self.logger.error("", e) # run any pending jobs/events self.check_for_timer_events(timestamp + 1) return self.status def check_for_timer_events(self, timestamp): # timer events will execute no more often than once per second if self.last_timer_event < timestamp: self.last_timer_event = timestamp self.job_scheduler.check_for_scheduled_jobs(timestamp) self.event_service.check_for_timer_events(timestamp) def register_packet_handler(self, packet_id: int, handler, priority=50): """ Call during pre_start Args: packet_id: int handler: (conn, packet) -> void priority: int """ if len(inspect.signature(handler).parameters) != 2: raise Exception( "Incorrect number of arguments for handler '%s.%s()'" % (handler.__module__, handler.__name__)) handlers = self.packet_handlers.get(packet_id, []) handlers.append(DictObject({"priority": priority, "handler": handler})) self.packet_handlers[packet_id] = sorted(handlers, key=lambda x: x.priority) def remove_packet_handler(self, packet_id, handler): handlers = self.packet_handlers.get(packet_id, []) for h in handlers: if h.handler == handler: handlers.remove(h) def iterate(self, timeout=0.1): conn, packet = self.incoming_queue.get_or_default(block=True, timeout=timeout, default=(None, None)) if packet: if isinstance(packet, server_packets.SystemMessage): packet = self.system_message_ext_msg_handling(packet) self.logger.log_chat(conn, "SystemMessage", None, packet.extended_message.get_message()) elif isinstance(packet, server_packets.PublicChannelMessage): packet = self.public_channel_message_ext_msg_handling(packet) elif isinstance(packet, server_packets.BuddyAdded) and packet.char_id == 0: return for handler in self.packet_handlers.get(packet.id, []): handler.handler(conn, packet) return packet def public_channel_message_ext_msg_handling( self, packet: server_packets.PublicChannelMessage): msg = packet.message if msg.startswith("~&") and msg.endswith("~"): try: msg = msg[2:-1].encode("utf-8") category_id = self.mmdb_parser.read_base_85(msg[0:5]) instance_id = self.mmdb_parser.read_base_85(msg[5:10]) template = self.mmdb_parser.get_message_string( category_id, instance_id) params = self.mmdb_parser.parse_params(msg[10:]) packet.extended_message = ExtendedMessage( category_id, instance_id, template, params) except Exception as e: self.logger.error( "Error handling extended message for packet: " + str(packet), e) return packet def system_message_ext_msg_handling(self, packet: server_packets.SystemMessage): try: category_id = 20000 instance_id = packet.message_id template = self.mmdb_parser.get_message_string( category_id, instance_id) params = self.mmdb_parser.parse_params(packet.message_args) packet.extended_message = ExtendedMessage(category_id, instance_id, template, params) except Exception as e: self.logger.error( "Error handling extended message: " + str(packet), e) return packet def send_org_message(self, msg, add_color=True, conn=None): if not conn: conn = self.get_primary_conn() if not conn.org_channel_id: self.logger.debug( f"Ignoring message to org channel for {conn.id} since the org_channel_id is unknown" ) else: color = self.setting_service.get( "org_channel_color").get_font_color() if add_color else "" pages = self.get_text_pages( msg, conn, self.setting_service.get( "org_channel_max_page_length").get_value()) for page in pages: packet = client_packets.PublicChannelMessage( conn.org_channel_id, color + page, "") conn.add_packet_to_queue(packet) def send_private_message(self, char_id, msg, add_color=True, conn=None): if not conn: conn = self.get_primary_conn() if char_id is None: raise Exception("Cannot send message, char_id is empty") else: color = self.setting_service.get( "private_message_color").get_font_color() if add_color else "" pages = self.get_text_pages( msg, conn, self.setting_service.get( "private_message_max_page_length").get_value()) for page in pages: self.logger.log_tell( conn, "To", self.character_service.get_char_name(char_id), page) packet = client_packets.PrivateMessage(char_id, color + page, "\0") conn.add_packet_to_queue(packet) def send_private_channel_message(self, msg, private_channel_id=None, add_color=True, conn=None): if not conn: conn = self.get_primary_conn() if private_channel_id is None: private_channel_id = conn.get_char_id() color = self.setting_service.get( "private_channel_color").get_font_color() if add_color else "" pages = self.get_text_pages( msg, conn, self.setting_service.get( "private_channel_max_page_length").get_value()) for page in pages: packet = client_packets.PrivateChannelMessage( private_channel_id, color + page, "\0") conn.send_packet(packet) def send_mass_message(self, char_id, msg, add_color=True, conn=None): if not conn: conn = self.get_primary_conn() if not char_id: self.logger.warning("Could not send message to empty char_id") else: color = self.setting_service.get( "private_message_color").get_font_color() if add_color else "" pages = self.get_text_pages( msg, conn, self.setting_service.get( "private_message_max_page_length").get_value()) for page in pages: if self.mass_message_queue: packet = client_packets.PrivateMessage( char_id, color + page, "\0") self.mass_message_queue.put(packet) else: packet = client_packets.PrivateMessage( char_id, color + page, "spam") self.get_primary_conn().send_packet(packet) def send_message_to_other_org_channels(self, msg, from_conn: Conn): for _id, conn in self.get_conns( lambda x: x.is_main and x.org_id and x != from_conn): self.send_org_message(msg, conn=conn) def handle_private_message(self, conn: Conn, packet: server_packets.PrivateMessage): char_name = self.character_service.get_char_name(packet.char_id) self.logger.log_tell(conn, "From", char_name, packet.message) self.event_service.fire_event( self.PRIVATE_MSG_EVENT, DictObject({ "char_id": packet.char_id, "name": char_name, "message": packet.message, "conn": conn })) def get_text_pages(self, msg, conn, max_page_length): if isinstance(msg, ChatBlob): return self.text.paginate(msg, conn, max_page_length=max_page_length) else: return [self.text.format_message(msg, conn)] def is_ready(self): return self.ready def shutdown(self): self.status = BotStatus.SHUTDOWN def restart(self): self.status = BotStatus.RESTART def get_primary_conn_id(self): return self.primary_conn_id def get_primary_conn(self): return self.conns[self.get_primary_conn_id()] def get_conn_by_char_id(self, char_id): for _id, conn in self.get_conns(): if char_id == conn.get_char_id(): return conn return None def get_conn_by_org_id(self, org_id): for _id, conn in self.get_conns(): if conn.org_id == org_id: return conn return None # placeholder to keep track of things that need to be fixed/updated def get_temp_conn(self): return self.get_primary_conn() def get_conns(self, conn_filter=None): if conn_filter: return [(_id, conn) for _id, conn in self.conns.items() if conn_filter(conn)] else: return self.conns.items()
class Tyrbot: CONNECT_EVENT = "connect" PACKET_EVENT = "packet" PRIVATE_MSG_EVENT = "private_msg" OUTGOING_ORG_MESSAGE_EVENT = "outgoing_org_message" OUTGOING_PRIVATE_MESSAGE_EVENT = "outgoing_private_message" OUTGOING_PRIVATE_CHANNEL_MESSAGE_EVENT = "outgoing_private_channel_message" def __init__(self): super().__init__() self.logger = Logger(__name__) self.ready = False self.packet_handlers = {} self.superadmin = None self.status: BotStatus = BotStatus.SHUTDOWN self.dimension = None self.last_timer_event = 0 self.start_time = int(time.time()) self.version = "0.5-beta" self.incoming_queue = FifoQueue() self.mass_message_queue = None self.conns = DictObject() def inject(self, registry): self.db = registry.get_instance("db") self.character_service: CharacterService = registry.get_instance("character_service") self.public_channel_service: PublicChannelService = registry.get_instance("public_channel_service") self.text: Text = registry.get_instance("text") self.setting_service: SettingService = registry.get_instance("setting_service") self.access_service: AccessService = registry.get_instance("access_service") self.event_service = registry.get_instance("event_service") self.job_scheduler = registry.get_instance("job_scheduler") def init(self, config, registry, paths, mmdb_parser): self.mmdb_parser = mmdb_parser self.superadmin = config.superadmin.capitalize() self.dimension = config.server.dimension self.db.exec("UPDATE db_version SET verified = 0") self.db.exec("UPDATE db_version SET verified = 1 WHERE file = 'db_version'") self.load_sql_files(paths) # prepare commands, events, and settings self.db.exec("UPDATE command_config SET verified = 0") self.db.exec("UPDATE event_config SET verified = 0") self.db.exec("UPDATE setting SET verified = 0") with self.db.transaction(): registry.pre_start_all() registry.start_all() # remove commands, events, and settings that are no longer registered self.db.exec("DELETE FROM db_version WHERE verified = 0") self.db.exec("DELETE FROM command_config WHERE verified = 0") self.db.exec("DELETE FROM event_config WHERE verified = 0") self.db.exec("DELETE FROM timer_event WHERE handler NOT IN (SELECT handler FROM event_config WHERE event_type = ?)", ["timer"]) self.db.exec("DELETE FROM setting WHERE verified = 0") self.status = BotStatus.RUN def pre_start(self): self.access_service.register_access_level("superadmin", 10, self.check_superadmin) self.event_service.register_event_type(self.CONNECT_EVENT) self.event_service.register_event_type(self.PACKET_EVENT) self.event_service.register_event_type(self.PRIVATE_MSG_EVENT) self.event_service.register_event_type(self.OUTGOING_ORG_MESSAGE_EVENT) self.event_service.register_event_type(self.OUTGOING_PRIVATE_MESSAGE_EVENT) self.event_service.register_event_type(self.OUTGOING_PRIVATE_CHANNEL_MESSAGE_EVENT) def start(self): self.setting_service.register_new("core.system", "symbol", "!", TextSettingType(["!", "#", "*", "@", "$", "+", "-"]), "Symbol for executing bot commands") self.setting_service.register_new("core.system", "org_channel_max_page_length", 7500, NumberSettingType([4500, 6000, 7500, 9000, 10500, 12000]), "Maximum size of blobs in org channel") self.setting_service.register_new("core.system", "private_message_max_page_length", 7500, NumberSettingType([4500, 6000, 7500, 9000, 10500, 12000]), "Maximum size of blobs in private messages") self.setting_service.register_new("core.system", "private_channel_max_page_length", 7500, NumberSettingType([4500, 6000, 7500, 9000, 10500, 12000]), "Maximum size of blobs in private channel") self.setting_service.register_new("core.system", "org_id", "", NumberSettingType(allow_empty=True), "Override the default org id", "This setting is is for development/debug purposes and should not be changed unless you understand the implications") self.setting_service.register_new("core.system", "org_name", "", TextSettingType(allow_empty=True), "The exact org name of the bot", "This setting is automatically set by the bot and should not be changed manually") self.setting_service.register_new("core.colors", "header_color", "#FFFF00", ColorSettingType(), "Color for headers") self.setting_service.register_new("core.colors", "header2_color", "#FCA712", ColorSettingType(), "Color for sub-headers") self.setting_service.register_new("core.colors", "highlight_color", "#00BFFF", ColorSettingType(), "Color for highlight") self.setting_service.register_new("core.colors", "notice_color", "#FF8C00", ColorSettingType(), "Color for important notices") self.setting_service.register_new("core.colors", "neutral_color", "#E6E1A6", ColorSettingType(), "Color for neutral faction") self.setting_service.register_new("core.colors", "omni_color", "#FA8484", ColorSettingType(), "Color for omni faction") self.setting_service.register_new("core.colors", "clan_color", "#F79410", ColorSettingType(), "Color for clan faction") self.setting_service.register_new("core.colors", "unknown_color", "#FF0000", ColorSettingType(), "Color for unknown faction") self.setting_service.register_new("core.colors", "org_channel_color", "#89D2E8", ColorSettingType(), "Default org channel color") self.setting_service.register_new("core.colors", "private_channel_color", "#89D2E8", ColorSettingType(), "Default private channel color") self.setting_service.register_new("core.colors", "private_message_color", "#89D2E8", ColorSettingType(), "Default private message color") self.setting_service.register_new("core.colors", "blob_color", "#FFFFFF", ColorSettingType(), "Default blob content color") self.register_packet_handler(server_packets.PrivateMessage.id, self.handle_private_message, priority=40) def check_superadmin(self, char_id): char_name = self.character_service.resolve_char_to_name(char_id) return char_name == self.superadmin def connect(self, config): conn = self.create_conn("main") conn.connect(config.server.host, config.server.port) packet = conn.login(config.username, config.password, config.character) if not packet: self.status = BotStatus.ERROR return False else: self.incoming_queue.put((conn, packet)) self.mass_message_queue = FifoQueue() self.create_conn_thread(conn, self.mass_message_queue) if "slaves" in config: for i, slave in enumerate(config.slaves): conn = self.create_conn("slave" + str(i)) conn.connect(config.server.host, config.server.port) packet = conn.login(slave.username, slave.password, slave.character) if not packet: self.status = BotStatus.ERROR return False else: self.incoming_queue.put((conn, packet)) self.create_conn_thread(conn, self.mass_message_queue) return True def create_conn_thread(self, conn: Conn, mass_message_queue=None): def read_packets(): try: while self.status == BotStatus.RUN: packet = conn.read_packet(1) if packet: self.incoming_queue.put((conn, packet)) while mass_message_queue and not mass_message_queue.empty() and conn.packet_queue.is_empty(): packet = mass_message_queue.get_or_default(block=False) if packet: conn.add_packet_to_queue(packet) except (EOFError, OSError) as e: self.status = BotStatus.ERROR self.logger.error("", e) raise e dthread = threading.Thread(target=read_packets, daemon=True) dthread.start() def create_conn(self, _id): if _id in self.conns: raise Exception(f"A connection with id {_id} already exists") conn = Conn(_id, self.disconnect) self.conns[_id] = conn return conn # passthrough def send_packet(self, packet): self.conns["main"].send_packet(packet) def disconnect(self): # wait for all threads to stop reading packets, then disconnect them all time.sleep(2) for _id, conn in self.conns.items(): conn.disconnect() def run(self): start = time.time() # wait for flood of packets from login to stop sending time_waited = 0 while time_waited < 5: if not self.iterate(1): time_waited += 1 self.logger.info("Login complete (%fs)" % (time.time() - start)) start = time.time() self.event_service.fire_event("connect", None) self.event_service.run_timer_events_at_startup() self.logger.info("Connect events finished (%fs)" % (time.time() - start)) self.ready = True timestamp = int(time.time()) while self.status == BotStatus.RUN: try: timestamp = int(time.time()) self.check_for_timer_events(timestamp) self.iterate() except (EOFError, OSError) as e: raise e except Exception as e: self.logger.error("", e) # run any pending jobs/events self.check_for_timer_events(timestamp + 1) return self.status def check_for_timer_events(self, timestamp): # timer events will execute no more often than once per second if self.last_timer_event < timestamp: self.last_timer_event = timestamp self.job_scheduler.check_for_scheduled_jobs(timestamp) self.event_service.check_for_timer_events(timestamp) def register_packet_handler(self, packet_id: int, handler, priority=50): """ Call during pre_start Args: packet_id: int handler: (conn, packet) -> void priority: int """ if len(inspect.signature(handler).parameters) != 2: raise Exception("Incorrect number of arguments for handler '%s.%s()'" % (handler.__module__, handler.__name__)) handlers = self.packet_handlers.get(packet_id, []) handlers.append(DictObject({"priority": priority, "handler": handler})) self.packet_handlers[packet_id] = sorted(handlers, key=lambda x: x.priority) def remove_packet_handler(self, packet_id, handler): handlers = self.packet_handlers.get(packet_id, []) for h in handlers: if h.handler == handler: handlers.remove(h) def iterate(self, timeout=0.1): conn, packet = self.incoming_queue.get_or_default(block=True, timeout=timeout, default=(None, None)) if packet: if isinstance(packet, server_packets.SystemMessage): packet = self.system_message_ext_msg_handling(packet) elif isinstance(packet, server_packets.PublicChannelMessage): packet = self.public_channel_message_ext_msg_handling(packet) if isinstance(packet, server_packets.BuddyAdded): if packet.char_id == 0: return for handler in self.packet_handlers.get(packet.id, []): handler.handler(conn, packet) self.event_service.fire_event("packet:" + str(packet.id), packet) return packet def public_channel_message_ext_msg_handling(self, packet: server_packets.PublicChannelMessage): msg = packet.message if msg.startswith("~&") and msg.endswith("~"): try: msg = msg[2:-1].encode("utf-8") category_id = self.mmdb_parser.read_base_85(msg[0:5]) instance_id = self.mmdb_parser.read_base_85(msg[5: 10]) template = self.mmdb_parser.get_message_string(category_id, instance_id) params = self.mmdb_parser.parse_params(msg[10:]) packet.extended_message = ExtendedMessage(category_id, instance_id, template, params) except Exception as e: self.logger.error("Error handling extended message for packet: " + str(packet), e) return packet def system_message_ext_msg_handling(self, packet: server_packets.SystemMessage): try: category_id = 20000 instance_id = packet.message_id template = self.mmdb_parser.get_message_string(category_id, instance_id) params = self.mmdb_parser.parse_params(packet.message_args) packet.extended_message = ExtendedMessage(category_id, instance_id, template, params) self.logger.log_chat("SystemMessage", None, packet.extended_message.get_message()) except Exception as e: self.logger.error("Error handling extended message: " + str(packet), e) return packet def send_org_message(self, msg, add_color=True, fire_outgoing_event=True, conn_id="main"): org_channel_id = self.public_channel_service.org_channel_id if org_channel_id is None: self.logger.debug("ignoring message to org channel since the org_channel_id is unknown") else: color = self.setting_service.get("org_channel_color").get_font_color() if add_color else "" pages = self.get_text_pages(msg, self.setting_service.get("org_channel_max_page_length").get_value()) for page in pages: packet = client_packets.PublicChannelMessage(org_channel_id, color + page, "") self.conns[conn_id].add_packet_to_queue(packet) if fire_outgoing_event: self.event_service.fire_event(self.OUTGOING_ORG_MESSAGE_EVENT, DictObject({"org_channel_id": org_channel_id, "message": msg})) def send_private_message(self, char, msg, add_color=True, fire_outgoing_event=True, conn_id="main"): char_id = self.character_service.resolve_char_to_id(char) if char_id is None: self.logger.warning("Could not send message to %s, could not find char id" % char) else: color = self.setting_service.get("private_message_color").get_font_color() if add_color else "" pages = self.get_text_pages(msg, self.setting_service.get("private_message_max_page_length").get_value()) for page in pages: self.logger.log_tell("To", self.character_service.get_char_name(char_id), page) packet = client_packets.PrivateMessage(char_id, color + page, "\0") self.conns[conn_id].add_packet_to_queue(packet) if fire_outgoing_event: self.event_service.fire_event(self.OUTGOING_PRIVATE_MESSAGE_EVENT, DictObject({"char_id": char_id, "message": msg})) def send_private_channel_message(self, msg, private_channel=None, add_color=True, fire_outgoing_event=True, conn_id="main"): if private_channel is None: private_channel_id = self.get_char_id() else: private_channel_id = self.character_service.resolve_char_to_id(private_channel) if private_channel_id is None: self.logger.warning("Could not send message to private channel %s, could not find private channel" % private_channel) else: color = self.setting_service.get("private_channel_color").get_font_color() if add_color else "" pages = self.get_text_pages(msg, self.setting_service.get("private_channel_max_page_length").get_value()) for page in pages: packet = client_packets.PrivateChannelMessage(private_channel_id, color + page, "\0") self.conns[conn_id].send_packet(packet) if fire_outgoing_event and private_channel_id == self.get_char_id(): self.event_service.fire_event(self.OUTGOING_PRIVATE_CHANNEL_MESSAGE_EVENT, DictObject({"private_channel_id": private_channel_id, "message": msg})) def send_mass_message(self, char_id, msg, add_color=True): if not char_id: self.logger.warning("Could not send message to empty char_id") else: color = self.setting_service.get("private_message_color").get_font_color() if add_color else "" pages = self.get_text_pages(msg, self.setting_service.get("private_message_max_page_length").get_value()) for page in pages: # self.logger.log_tell("To", self.character_service.get_char_name(char_id), page) if self.mass_message_queue: packet = client_packets.PrivateMessage(char_id, color + page, "\0") self.mass_message_queue.put(packet) else: packet = client_packets.PrivateMessage(char_id, color + page, "spam") self.conns["main"].send_packet(packet) def handle_private_message(self, conn: Conn, packet: server_packets.PrivateMessage): if conn.id != "main": return self.logger.log_tell("From", self.character_service.get_char_name(packet.char_id), packet.message) self.event_service.fire_event(self.PRIVATE_MSG_EVENT, packet) def get_text_pages(self, msg, max_page_length): if isinstance(msg, ChatBlob): return self.text.paginate(msg, max_page_length=max_page_length) else: return [self.text.format_message(msg)] def is_ready(self): return self.ready def shutdown(self): self.status = BotStatus.SHUTDOWN def restart(self): self.status = BotStatus.RESTART def load_sql_files(self, paths): dirs = flatmap(lambda x: os.walk(x), paths) dirs = filter(lambda y: not y[0].endswith("__pycache__"), dirs) def get_files(tup): return map(lambda x: os.path.join(tup[0], x), tup[2]) # get files from subdirectories files = flatmap(get_files, dirs) files = filter(lambda z: z.endswith(".sql"), files) base_path = os.getcwd() for file in files: self.db.load_sql_file(file, base_path) def get_char_name(self): return self.conns["main"].char_name def get_char_id(self): return self.conns["main"].char_id
success = False try: success = handler.run(section=section, **kwargs) except CancelException, e: handler.update_status(False) log.info('Task "%s" was cancelled', key) except Exception, ex: handler.update_status(False) log.error('Exception raised in handler for %r: %s', key, ex, exc_info=True) log.debug( 'Cache Statistics - len(matcher): %s, len(metadata): %s', len(CacheManager.get('matcher')), len(CacheManager.get('metadata')) ) # Sync "matcher" cache (back to disk) CacheManager.get('matcher').sync() # Clear memory caches CacheManager.get('matcher').cache.clear() CacheManager.get('metadata').cache.clear() # Run garbage collection log.debug('[GC] Collected %d objects', gc.collect()) log.debug('[GC] Count: %s', gc.get_count()) log.debug('[GC] Garbage: %s', len(gc.garbage))
# load logging configuration import conf.logging Registry.logger = Logger("core.registry") logger = Logger("core.bootstrap") logger.info("Starting Tyrbot...") config_file = "./conf/config.hjson" # start config wizard if config file does not exist if not os.path.exists(config_file): config_creator.create_new_cfg(config_file, "./conf/config.template.hjson") # load config logger.debug("Reading config file '%s'" % config_file) with open(config_file, "r") as cfg: config = DictObject(hjson.load(cfg)) # paths to search for instances: core + module_paths paths = ["core"] paths.extend(config.module_paths) # load instances logger.debug("Loading instances") Registry.load_instances(paths) Registry.inject_all() # configure database db = Registry.get_instance("db") if config.database.type == "sqlite":
class StyleDal(object): def __init__(self, connection_pool): self.logger = Logger(self.__class__.__name__).get() self.connection_pool = connection_pool def style_exists(self, style_name): style = self.get_style(style_name) exists = False if 'name' in style and style['name'] == style_name: exists = True return exists def get_all_styles(self): style_query = ( "SELECT s.id, s.name, s. url, f.name as familyname FROM style s " "JOIN familystylemap m on m.styleid = s.id " "JOIN family f on m.familyid = f.id " "WHERE s.validfrom < %s " "AND ((s.validto = '0000-00-00 00:00:00') OR (s.validto >= %s))") now = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime()) cnx = self.connection_pool.get_connection() cursor = cnx.cursor() style_data = (now, now) self.logger.debug("Getting style with query [%s] and data [%s]", style_query, style_data) cursor.execute(style_query, style_data) styles = [] for (c_id, c_name, c_url, c_family) in cursor: style = { 'id': c_id, 'name': c_name, 'url': c_url, 'family': c_family } styles.append(style) cursor.close() self.connection_pool.release_connection(cnx) return styles def get_styles_for_family(self, family_name): style_query = ( "SELECT s.id, s.name, s. url, f.name as familyname FROM style s " "JOIN familystylemap m on m.styleid = s.id " "JOIN family f on m.familyid = f.id " "WHERE f.name = %s " "AND s.validfrom < %s " "AND ((s.validto = '0000-00-00 00:00:00') OR (s.validto >= %s))") now = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime()) cnx = self.connection_pool.get_connection() cursor = cnx.cursor() style_data = (family_name, now, now) self.logger.debug("Getting style with query [%s] and data [%s]", style_query, style_data) cursor.execute(style_query, style_data) styles = [] for (c_id, c_name, c_url, c_family) in cursor: style = { 'id': c_id, 'name': c_name, 'url': c_url, 'family': c_family } styles.append(style) cursor.close() self.connection_pool.release_connection(cnx) return styles def get_style(self, style_name): style_query = ( "SELECT id, name, url FROM style " "WHERE name = %s " "AND validfrom < %s " "AND ((validto = '0000-00-00 00:00:00') OR (validto >= %s))") now = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime()) cnx = self.connection_pool.get_connection() cursor = cnx.cursor() style_data = (style_name, now, now) self.logger.debug("Getting style with query [%s] and data [%s]", style_query, style_data) cursor.execute(style_query, style_data) style = {} for (s_id, s_name, s_url) in cursor: if s_name == style_name: style['id'] = s_id style['name'] = s_name style['url'] = s_url cursor.close() self.connection_pool.release_connection(cnx) return style def get_style_id(self, style_name): style_query = ( "SELECT id, name FROM style " "WHERE name = %s " "AND validfrom < %s " "AND ((validto = '0000-00-00 00:00:00') OR (validto >= %s))") now = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime()) cnx = self.connection_pool.get_connection() cursor = cnx.cursor() style_data = (style_name, now, now) self.logger.debug("Getting style ID with query [%s] and data [%s]", style_query, style_data) cursor.execute(style_query, style_data) style_id = -1 for (s_id, s_name) in cursor: if s_name == style_name: style_id = s_id cursor.close() self.connection_pool.release_connection(cnx) return style_id def get_last_style_id(self): style_query = ("SELECT MAX(id) FROM style") cnx = self.connection_pool.get_connection() cursor = cnx.cursor() cursor.execute(style_query) ret_id = -1 for c_id in cursor: if c_id is not None and c_id[0] is not None: ret_id = int(c_id[0]) cursor.close() self.connection_pool.release_connection(cnx) return ret_id def insert_style(self, style_name, url, source_id): add_style = ("INSERT INTO style " "(name, sourceid, url, validfrom) " "VALUES (%s, %s, %s, %s)") now = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime()) cnx = self.connection_pool.get_connection() cursor = cnx.cursor() data_style = (style_name, source_id, url, now) cursor.execute(add_style, data_style) cnx.commit() style_id = int(cursor.lastrowid) cursor.close() self.connection_pool.release_connection(cnx) return style_id
class CommandService: PRIVATE_CHANNEL = "priv" ORG_CHANNEL = "org" PRIVATE_MESSAGE = "msg" def __init__(self): self.handlers = collections.defaultdict(list) self.logger = Logger(__name__) self.channels = {} self.ignore_regexes = [ re.compile(" is AFK \(Away from keyboard\) since ", re.IGNORECASE), re.compile("I am away from my keyboard right now", re.IGNORECASE), re.compile("Unknown command or access denied!", re.IGNORECASE), re.compile("I am responding", re.IGNORECASE), re.compile("I only listen", re.IGNORECASE), re.compile("Error!", re.IGNORECASE), re.compile("Unknown command input", re.IGNORECASE), re.compile("You have been auto invited", re.IGNORECASE), ] def inject(self, registry): self.db = registry.get_instance("db") self.util = registry.get_instance("util") self.access_service: AccessService = registry.get_instance( "access_service") self.bot: Tyrbot = registry.get_instance("bot") self.character_service: CharacterService = registry.get_instance( "character_service") self.setting_service: SettingService = registry.get_instance( "setting_service") self.command_alias_service = registry.get_instance( "command_alias_service") self.usage_service = registry.get_instance("usage_service") self.public_channel_service = registry.get_instance( "public_channel_service") self.ban_service = registry.get_instance("ban_service") def pre_start(self): self.bot.add_packet_handler(server_packets.PrivateMessage.id, self.handle_private_message) self.bot.add_packet_handler(server_packets.PrivateChannelMessage.id, self.handle_private_channel_message) self.bot.add_packet_handler(server_packets.PublicChannelMessage.id, self.handle_public_channel_message) self.register_command_channel("Private Message", self.PRIVATE_MESSAGE) self.register_command_channel("Org Channel", self.ORG_CHANNEL) self.register_command_channel("Private Channel", self.PRIVATE_CHANNEL) def start(self): # process decorators for _, inst in Registry.get_all_instances().items(): for name, method in get_attrs(inst).items(): if hasattr(method, "command"): cmd_name, params, access_level, description, help_file, sub_command, extended_description, check_access, aliases = getattr( method, "command") handler = getattr(inst, name) module = self.util.get_module_name(handler) help_text = self.get_help_file(module, help_file) self.register(handler, cmd_name, params, access_level, description, module, help_text, sub_command, extended_description, check_access) if len(inspect.signature( handler).parameters) != len(params) + 1: raise Exception( "Incorrect number of arguments for handler '%s.%s()'" % (handler.__module__, handler.__name__)) if aliases: for alias in aliases: self.command_alias_service.add_alias( alias, cmd_name) def register(self, handler, command, params, access_level, description, module, help_text=None, sub_command=None, extended_description=None, check_access=None): command = command.lower() if sub_command: sub_command = sub_command.lower() else: sub_command = "" access_level = access_level.lower() module = module.lower() command_key = self.get_command_key(command, sub_command) if help_text is None: help_text = self.generate_help(command, description, params, extended_description) if check_access is None: check_access = self.access_service.check_access if not self.access_service.get_access_level_by_label(access_level): self.logger.error( "Could not add command '%s': could not find access level '%s'" % (command, access_level)) return for channel, label in self.channels.items(): row = self.db.query_single( "SELECT access_level, module, enabled, verified " "FROM command_config " "WHERE command = ? AND sub_command = ? AND channel = ?", [command, sub_command, channel]) if row is None: # add new command commands self.db.exec( "INSERT INTO command_config " "(command, sub_command, access_level, channel, module, enabled, verified) " "VALUES (?, ?, ?, ?, ?, 1, 1)", [command, sub_command, access_level, channel, module]) elif row.verified: if row.module != module: self.logger.warning( "module different for different forms of command '%s' and sub_command '%s'" % (command, sub_command)) else: # mark command as verified self.db.exec( "UPDATE command_config SET verified = 1, module = ? " "WHERE command = ? AND sub_command = ? AND channel = ?", [module, command, sub_command, channel]) # save reference to command handler r = re.compile(self.get_regex_from_params(params), re.IGNORECASE | re.DOTALL) self.handlers[command_key].append({ "regex": r, "callback": handler, "help": help_text, "description": description, "params": params, "check_access": check_access }) def register_command_channel(self, label, value): if value in self.channels: self.logger.error( "Could not register command channel '%s': command channel already registered" % value) return self.logger.debug("Registering command channel '%s'" % value) self.channels[value] = label def is_command_channel(self, channel): return channel in self.channels def process_command(self, message: str, channel: str, char_id, reply): try: if self.ban_service.get_ban(char_id): # do nothing if character is banned self.logger.info( "ignored banned character %d for command '%s'" % (char_id, message)) return message = html.unescape(message) command_str, command_args = self.get_command_parts(message) # check for command alias command_alias = self.command_alias_service.check_for_alias( command_str) if command_alias: command_str, command_args = self.get_command_parts( command_alias + command_args) cmd_configs = self.get_command_configs(command_str, channel, 1) if cmd_configs: # given a list of cmd_configs that are enabled, see if one has regex that matches incoming command_str cmd_config, matches, handler = self.get_matches( cmd_configs, command_args) if matches: if handler["check_access"](char_id, cmd_config.access_level): sender = SenderObj( char_id, self.character_service.resolve_char_to_name( char_id, "Unknown(%d)" % char_id)) response = handler["callback"]( CommandRequest(channel, sender, reply), *self.process_matches(matches, handler["params"])) if response is not None: reply(response) # record command usage self.usage_service.add_usage( command_str, handler["callback"].__qualname__, char_id, channel) else: self.access_denied_response(char_id, cmd_config, reply) else: # handlers were found, but no handler regex matched help_text = self.get_help_text(char_id, command_str, channel) if help_text: reply(self.format_help_text(command_str, help_text)) else: reply("Error! Invalid syntax.") else: reply("Error! Unknown command <highlight>%s<end>." % command_str) except Exception as e: self.logger.error("error processing command: %s" % message, e) reply("There was an error processing your request.") def access_denied_response(self, char_id, cmd_config, reply): reply("Error! Access denied.") def get_command_parts(self, message): parts = message.split(" ", 1) if len(parts) == 2: return parts[0].lower(), " " + parts[1] else: return parts[0].lower(), "" def get_command_configs(self, command, channel=None, enabled=1, sub_command=None): sql = "SELECT command, sub_command, access_level, enabled FROM command_config WHERE command = ?" params = [command] if channel: sql += " AND channel = ?" params.append(channel) if enabled: sql += " AND enabled = ?" params.append(enabled) if sub_command: sql += " AND sub_command = ?" params.append(sub_command) sql += " ORDER BY sub_command, channel" return self.db.query(sql, params) def get_matches(self, cmd_configs, command_args): for row in cmd_configs: command_key = self.get_command_key(row.command, row.sub_command) handlers = self.handlers[command_key] for handler in handlers: # add leading space to search string to normalize input for command params matches = handler["regex"].search(command_args) if matches: return row, matches, handler return None, None, None def process_matches(self, matches, params): groups = list(matches.groups()) processed = [] for param in params: processed.append(param.process_matches(groups)) return processed def get_help_text(self, char, command_str, channel): data = self.db.query( "SELECT command, sub_command, access_level FROM command_config " "WHERE command = ? AND channel = ? AND enabled = 1", [command_str, channel]) # filter out commands that character does not have access level for data = filter( lambda row: self.access_service.check_access( char, row.access_level), data) def read_help_text(row): command_key = self.get_command_key(row.command, row.sub_command) return filter( lambda x: x is not None, map(lambda handler: handler["help"], self.handlers[command_key])) content = "\n\n".join(flatmap(read_help_text, data)) return content if content else None def format_help_text(self, topic, help_text): return ChatBlob("Help (" + topic + ")", help_text) def get_help_file(self, module, help_file): if help_file: try: help_file = "./" + module.replace(".", "/") + "/" + help_file with open(help_file) as f: return f.read().strip() except FileNotFoundError as e: self.logger.error("Error reading help file", e) return None def get_command_key(self, command, sub_command): if sub_command: return command + ":" + sub_command else: return command def get_command_key_parts(self, command_str): parts = command_str.split(":", 1) if len(parts) == 2: return parts[0], parts[1] else: return parts[0], "" def get_regex_from_params(self, params): # params must be wrapped with line-beginning and line-ending anchors in order to match # when no params are specified (eg. "^$") return "^" + "".join(map(lambda x: x.get_regex(), params)) + "$" def generate_help(self, command, description, params, extended_description=None): help_text = description + ":\n" + "<tab><symbol>" + command + " " + " ".join( map(lambda x: x.get_name(), params)) if extended_description: help_text += "\n" + extended_description return help_text def get_handlers(self, command_key): return self.handlers.get(command_key, None) def handle_private_message(self, packet: server_packets.PrivateMessage): # since the command symbol is not required for private messages, # the command_str must have length of at least 1 in order to be valid, # otherwise it is ignored if len(packet.message) < 1: return for regex in self.ignore_regexes: if regex.search(packet.message): return if packet.message[:1] == self.setting_service.get( "symbol").get_value(): command_str = packet.message[1:] else: command_str = packet.message self.process_command( command_str, self.PRIVATE_MESSAGE, packet.char_id, lambda msg: self.bot.send_private_message(packet.char_id, msg)) def handle_private_channel_message( self, packet: server_packets.PrivateChannelMessage): # since the command symbol is required in the private channel, # the command_str must have length of at least 2 in order to be valid, # otherwise it is ignored if len(packet.message) < 2: return symbol = packet.message[:1] command_str = packet.message[1:] if symbol == self.setting_service.get("symbol").get_value( ) and packet.private_channel_id == self.bot.char_id: self.process_command( command_str, self.PRIVATE_CHANNEL, packet.char_id, lambda msg: self.bot.send_private_channel_message(msg)) def handle_public_channel_message( self, packet: server_packets.PublicChannelMessage): # since the command symbol is required in the org channel, # the command_str must have length of at least 2 in order to be valid, # otherwise it is ignored if len(packet.message) < 2: return symbol = packet.message[:1] command_str = packet.message[1:] if symbol == self.setting_service.get("symbol").get_value( ) and self.public_channel_service.is_org_channel_id(packet.channel_id): self.process_command(command_str, self.ORG_CHANNEL, packet.char_id, lambda msg: self.bot.send_org_message(msg))
class ModelDal(object): def __init__(self, connection_pool): self.logger = Logger(self.__class__.__name__).get() self.connection_pool = connection_pool def model_exists(self, model): ret_model = self.get_model(model['style'], model['name'], model['sku']) exists = False if ret_model is not None and 'name' in ret_model and model[ 'name'] == ret_model['name']: exists = True return exists def get_model(self, style_name, model_name, sku): query = ( "SELECT m.id, m.name, m.sku, m.listprice, m.url FROM model m JOIN style s on m.styleid = s.id " "WHERE s.name = %s " "AND m.name = %s " "AND m.sku = %s " "AND m.validfrom < %s " "AND ((m.validto = 0) OR (m.validto >= %s))") now = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime()) cnx = self.connection_pool.get_connection() cursor = cnx.cursor() data = (style_name, model_name, sku, now, now) self.logger.debug("Getting model with query [%s] and data [%s]", query, data) cursor.execute(query, data) model = None for (m_id, m_name, m_sku, m_listprice, m_url) in cursor: if m_name == model_name and sku == m_sku: model = { 'id': m_id, 'name': m_name, 'sku': m_sku, 'listprice': m_listprice, 'url': m_url } cursor.close() self.connection_pool.release_connection(cnx) return model def get_model_id(self, style_id, model_name, sku): query = ("SELECT id, name, sku FROM model " "WHERE name = %s " "AND styleid = %s " "AND sku = %s " "AND validfrom < %s " "AND ((validto = 0) OR (validto >= %s))") now = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime()) cnx = self.connection_pool.get_connection() cursor = cnx.cursor() data = (model_name, style_id, sku, now, now) self.logger.debug("Getting model id with query [%s] and data [%s]", query, data) cursor.execute(query, data) model_id = -1 for (m_id, m_name, m_sku) in cursor: if m_name == model_name and sku == m_sku: model_id = m_id cursor.close() self.connection_pool.release_connection(cnx) return model_id def get_last_model_id(self): model_query = ("SELECT MAX(id) FROM model") cnx = self.connection_pool.get_connection() cursor = cnx.cursor() cursor.execute(model_query) ret_id = -1 for c_id in cursor: if c_id is not None and c_id[0] is not None: ret_id = int(c_id[0]) cursor.close() self.connection_pool.release_connection(cnx) return ret_id def get_fit_id(self, fit): query = "SELECT id, name FROM fit WHERE name = %s" cnx = self.connection_pool.get_connection() cursor = cnx.cursor() data = (fit, ) cursor.execute(query, data) ret_id = -1 for c_id, c_name in cursor: if c_name == fit: ret_id = c_id cursor.close() self.connection_pool.release_connection(cnx) return ret_id #style_id, model_name, model_sku, model_framecolour, model_lens, fit_id, model_listprice, model_url def insert_model(self, model, style_id, lens_id, fit_id, source_id): query = ( "INSERT INTO model " "(name, styleid, sku, listprice, url, framecolour, lensid, fitid, sourceid, validfrom) " "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)") now = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime()) cnx = self.connection_pool.get_connection() cursor = cnx.cursor() data = (model['name'], style_id, model['sku'], model['listprice'], model['url'], model['frame'], lens_id, fit_id, source_id, now) self.logger.debug("Inserting model with query [%s] and data [%s]", query, data) cursor.execute(query, data) cnx.commit() model_id = int(cursor.lastrowid) cursor.close() self.connection_pool.release_connection(cnx) # model_id = self.get_model_id(style_id, model['name'], model['sku']) return model_id def update_model(self, model, style_id, source_id): query = "UPDATE model SET " data = [] continu = False if 'releasedate' in model and model['releasedate'] is not None: query += "releasedate=%s, " data.append(model['releasedate']) continu = True if 'retiredate' in model and model['retiredate'] is not None: query += "retiredate=%s, " data.append(model['retiredate']) continu = True if 'image' in model and model['image'] is not None: query += "image=%s, " data.append(model['image']) continu = True if 'imagesmall' in model and model['imagesmall'] is not None: query += "imagesmall=%s, " data.append(model['imagesmall']) continu = True if 'note' in model and model['note'] is not None: query += "note=%s, " data.append(model['note']) continu = True if 'signature' in model and model['signature'] is not None: query += "signature=%s, " data.append(model['signature']) continu = True if 'exclusive' in model and model['exclusive'] is not None: query += "exclusive=%s, " data.append(model['exclusive']) continu = True if 'upc' in model and model['upc'] is not None: query += "upc=%s " data.append(model['upc']) continu = True # no count? just return if not continu: return query = query.rstrip(' ,') # TODO: deal with valid froms etc query += " WHERE name=%s AND sku=%s AND styleid=%s" data.append(model['name']) data.append(model['sku']) data.append(style_id) cnx = self.connection_pool.get_connection() cursor = cnx.cursor() self.logger.info("Updating model with query [%s] and data [%s]", query, data) cursor.execute(query, data) cnx.commit() cursor.close() self.connection_pool.release_connection(cnx) return