def __init__(self): self.software = 'tsuserver3' self.release = 3 self.major_version = 3 self.minor_version = 0 self.config = None self.allowed_iniswaps = [] self.char_list = None self.char_emotes = None self.char_pages_ao1 = None self.music_list = [] self.music_list_ao2 = None self.music_pages_ao1 = None self.bglock = False self.backgrounds = None self.zalgo_tolerance = None self.ipRange_bans = [] self.geoIpReader = None self.useGeoIp = False try: self.geoIpReader = geoip2.database.Reader('./storage/GeoLite2-ASN.mmdb') self.useGeoIp = True # on debian systems you can use /usr/share/GeoIP/GeoIPASNum.dat if the geoip-database-extra package is installed except FileNotFoundError: self.useGeoIp = False pass self.ms_client = None try: self.load_config() self.area_manager = AreaManager(self) self.load_iniswaps() self.load_characters() self.load_music() self.load_backgrounds() self.load_ipranges() self.load_gimps() self.load_prompts() self.load_miscdata() self.save_miscdata() except yaml.YAMLError as exc: print('There was a syntax error parsing a configuration file:') print(exc) print('Please revise your syntax and restart the server.') sys.exit(1) except OSError as exc: print('There was an error opening or writing to a file:') print(exc) sys.exit(1) except Exception as exc: print('There was a configuration error:') print(exc) print('Please check sample config files for the correct format.') sys.exit(1) self.client_manager = ClientManager(self) server.logger.setup_logger(debug=self.config['debug'])
def __init__(self): self.config = None self.allowed_iniswaps = None self.load_config() self.load_iniswaps() self.client_manager = ClientManager(self) self.area_manager = AreaManager(self) self.ban_manager = BanManager() self.software = 'tsuserver3' self.version = 'tsuserver3dev' self.release = 3 self.major_version = 1 self.minor_version = 1 self.ipid_list = {} self.hdid_list = {} self.char_list = None self.char_pages_ao1 = None self.music_list = None self.music_list_ao2 = None self.music_pages_ao1 = None self.backgrounds = None self.load_characters() self.load_music() self.load_backgrounds() self.load_ids() self.district_client = None self.ms_client = None self.rp_mode = False logger.setup_logger(debug=self.config['debug'])
def __init__(self): self.config = None self.allowed_iniswaps = None self.loaded_ips = {} self.load_config() self.load_iniswaps() self.client_manager = ClientManager(self) self.area_manager = AreaManager(self) self.serverpoll_manager = ServerpollManager(self) self.ban_manager = BanManager() self.software = 'tsuserver3' self.version = 'tsuserver3dev' self.release = 3 self.major_version = 2 self.minor_version = 0 self.char_list = None self.char_pages_ao1 = None self.music_list = None self.music_list_ao2 = None self.music_pages_ao1 = None self.backgrounds = None self.data = None self.features = set() self.load_characters() self.load_music() self.load_backgrounds() self.load_data() self.load_ids() self.enable_features() self.district_client = None self.ms_client = None self.rp_mode = False logger.setup_logger(debug=self.config['debug'], log_size=self.config['log_size'], log_backups=self.config['log_backups'])
def _listener_handler(self, data): res = None try: port, freePort = self.port_manager.request_port() clientManager = None def terminateClient(): nonlocal clientManager nonlocal freePort freePort() self._client_managers.remove(clientManager) clientManager = ClientManager(port, terminateClient, data) clientManager.start() self._client_managers.append( clientManager ) res = Response(ResponseTypes.accept, { "port": port, "data": clientManager.initialResponse }) except Exception as err: res = Response(ResponseTypes.reject, { "message": str(err) }) return res.toDict()
def __init__(self): self.software = 'tsuserver3' self.release = 3 self.major_version = 3 self.minor_version = 0 self.config = None self.allowed_iniswaps = [] self.char_list = None self.char_emotes = None self.char_pages_ao1 = None self.music_list = [] self.music_list_ao2 = None self.music_pages_ao1 = None self.bglock = False self.backgrounds = None self.zalgo_tolerance = None self.ipRange_bans = [] self.geoIpReader = None self.useGeoIp = False try: self.geoIpReader = geoip2.database.Reader( './storage/GeoLite2-ASN.mmdb') self.useGeoIp = True # on debian systems you can use /usr/share/GeoIP/GeoIPASNum.dat if the geoip-database-extra package is installed except FileNotFoundError: self.useGeoIp = False pass self.ms_client = None try: self.load_config() self.area_manager = AreaManager(self) self.load_iniswaps() self.load_characters() self.load_music() self.load_backgrounds() self.load_ipranges() except yaml.YAMLError as exc: print('There was a syntax error parsing a configuration file:') print(exc) print('Please revise your syntax and restart the server.') # Truly idiotproof if os.name == 'nt': input('(Press Enter to exit)') sys.exit(1) self.client_manager = ClientManager(self) server.logger.setup_logger(debug=self.config['debug'])
def __init__(self): self.release = 3 self.major_version = 'DR' self.minor_version = '190622b' self.software = 'tsuserver{}'.format(self.get_version_string()) self.version = 'tsuserver{}dev'.format(self.get_version_string()) logger.log_print('Launching {}...'.format(self.software)) logger.log_print('Loading server configurations...') self.config = None self.global_connection = None self.shutting_down = False self.loop = None self.allowed_iniswaps = None self.default_area = 0 self.load_config() self.load_iniswaps() self.char_list = list() self.load_characters() self.client_manager = ClientManager(self) self.area_manager = AreaManager(self) self.ban_manager = BanManager(self) self.ipid_list = {} self.hdid_list = {} self.char_pages_ao1 = None self.music_list = None self.music_list_ao2 = None self.music_pages_ao1 = None self.backgrounds = None self.load_music() self.load_backgrounds() self.load_ids() self.district_client = None self.ms_client = None self.rp_mode = False self.user_auth_req = False self.client_tasks = dict() self.active_timers = dict() self.showname_freeze = False self.commands = importlib.import_module('server.commands') logger.setup_logger(debug=self.config['debug'])
def __init__(self): self.software = 'tsuserver3' self.release = 3 self.major_version = 3 self.minor_version = 0 self.config = None self.allowed_iniswaps = [] self.char_list = None self.char_emotes = None self.char_pages_ao1 = None self.music_list = None self.music_list_ao2 = None self.music_pages_ao1 = None self.backgrounds = None self.zalgo_tolerance = None self.ms_client = None try: self.load_config() self.area_manager = AreaManager(self) self.load_iniswaps() self.load_characters() self.load_music() self.load_backgrounds() except yaml.YAMLError as exc: print('There was a syntax error parsing a configuration file:') print(exc) print('Please revise your syntax and restart the server.') # Truly idiotproof if os.name == 'nt': input('(Press Enter to exit)') sys.exit(1) self.client_manager = ClientManager(self) server.logger.setup_logger(debug=self.config['debug'])
class TsuServer3: """The main class for tsuserver3 server software.""" def __init__(self): self.software = 'tsuserver3' self.release = 3 self.major_version = 3 self.minor_version = 0 self.config = None self.allowed_iniswaps = [] self.char_list = None self.char_emotes = None self.char_pages_ao1 = None self.music_list = [] self.music_list_ao2 = None self.music_pages_ao1 = None self.bglock = False self.backgrounds = None self.zalgo_tolerance = None self.ipRange_bans = [] self.geoIpReader = None self.useGeoIp = False try: self.geoIpReader = geoip2.database.Reader( './storage/GeoLite2-ASN.mmdb') self.useGeoIp = True # on debian systems you can use /usr/share/GeoIP/GeoIPASNum.dat if the geoip-database-extra package is installed except FileNotFoundError: self.useGeoIp = False pass self.ms_client = None try: self.load_config() self.area_manager = AreaManager(self) self.load_iniswaps() self.load_characters() self.load_music() self.load_backgrounds() self.load_ipranges() except yaml.YAMLError as exc: print('There was a syntax error parsing a configuration file:') print(exc) print('Please revise your syntax and restart the server.') # Truly idiotproof if os.name == 'nt': input('(Press Enter to exit)') sys.exit(1) self.client_manager = ClientManager(self) server.logger.setup_logger(debug=self.config['debug']) def start(self): """Start the server.""" loop = asyncio.get_event_loop() bound_ip = '0.0.0.0' if self.config['local']: bound_ip = '127.0.0.1' ao_server_crt = loop.create_server(lambda: AOProtocol(self), bound_ip, self.config['port']) ao_server = loop.run_until_complete(ao_server_crt) if self.config['use_websockets']: ao_server_ws = websockets.serve(new_websocket_client(self), bound_ip, self.config['websocket_port']) asyncio.ensure_future(ao_server_ws) if self.config['use_masterserver']: self.ms_client = MasterServerClient(self) asyncio.ensure_future(self.ms_client.connect(), loop=loop) if self.config['zalgo_tolerance']: self.zalgo_tolerance = self.config['zalgo_tolerance'] asyncio.ensure_future(self.schedule_unbans()) database.log_misc('start') print('Server started and is listening on port {}'.format( self.config['port'])) try: loop.run_forever() except KeyboardInterrupt: pass database.log_misc('stop') ao_server.close() loop.run_until_complete(ao_server.wait_closed()) loop.close() async def schedule_unbans(self): while True: database.schedule_unbans() await asyncio.sleep(3600 * 12) @property def version(self): """Get the server's current version.""" return f'{self.release}.{self.major_version}.{self.minor_version}' def new_client(self, transport): """ Create a new client based on a raw transport by passing it to the client manager. :param transport: asyncio transport :returns: created client object """ peername = transport.get_extra_info('peername')[0] if self.useGeoIp: try: geoIpResponse = self.geoIpReader.asn(peername) asn = str(geoIpResponse.autonomous_system_number) except geoip2.errors.AddressNotFoundError: asn = "Loopback" pass else: asn = "Loopback" for line, rangeBan in enumerate(self.ipRange_bans): if rangeBan != "" and peername.startswith( rangeBan) or asn == rangeBan: msg = 'BD#' msg += 'Abuse\r\n' msg += f'ID: {line}\r\n' msg += 'Until: N/A' msg += '#%' transport.write(msg.encode('utf-8')) raise ClientError c = self.client_manager.new_client(transport) c.server = self c.area = self.area_manager.default_area() c.area.new_client(c) return c def remove_client(self, client): """ Remove a disconnected client. :param client: client object """ client.area.remove_client(client) self.client_manager.remove_client(client) @property def player_count(self): """Get the number of non-spectating clients.""" return len([ client for client in self.client_manager.clients if client.char_id != -1 ]) def load_config(self): """Load the main server configuration from a YAML file.""" with open('config/config.yaml', 'r', encoding='utf-8') as cfg: self.config = yaml.safe_load(cfg) self.config['motd'] = self.config['motd'].replace('\\n', ' \n') if 'music_change_floodguard' not in self.config: self.config['music_change_floodguard'] = { 'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0 } if 'wtce_floodguard' not in self.config: self.config['wtce_floodguard'] = { 'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0 } if 'zalgo_tolerance' not in self.config: self.config['zalgo_tolerance'] = 3 if isinstance(self.config['modpass'], str): self.config['modpass'] = { 'default': { 'password': self.config['modpass'] } } if 'multiclient_limit' not in self.config: self.config['multiclient_limit'] = 16 def load_characters(self): """Load the character list from a YAML file.""" with open('config/characters.yaml', 'r', encoding='utf-8') as chars: self.char_list = yaml.safe_load(chars) self.build_char_pages_ao1() self.char_emotes = {char: Emotes(char) for char in self.char_list} def load_music(self): self.build_music_list() self.music_pages_ao1 = self.build_music_pages_ao1(self.music_list) self.music_list_ao2 = self.build_music_list_ao2(self.music_list) def load_backgrounds(self): """Load the backgrounds list from a YAML file.""" with open('config/backgrounds.yaml', 'r', encoding='utf-8') as bgs: self.backgrounds = yaml.safe_load(bgs) def load_iniswaps(self): """Load a list of characters for which INI swapping is allowed.""" try: with open('config/iniswaps.yaml', 'r', encoding='utf-8') as iniswaps: self.allowed_iniswaps = yaml.safe_load(iniswaps) except: logger.debug('Cannot find iniswaps.yaml') def load_ipranges(self): """Load a list of banned IP ranges.""" try: with open('config/iprange_ban.txt', 'r', encoding='utf-8') as ipranges: self.ipRange_bans = ipranges.read().splitlines() except: logger.debug('Cannot find iprange_ban.txt') def build_char_pages_ao1(self): """ Cache a list of characters that can be used for the AO1 connection handshake. """ self.char_pages_ao1 = [ self.char_list[x:x + 10] for x in range(0, len(self.char_list), 10) ] for i in range(len(self.char_list)): self.char_pages_ao1[i // 10][i % 10] = '{}#{}&&0&&&0&'.format( i, self.char_list[i]) def build_music_list(self): with open('config/music.yaml', 'r', encoding='utf-8') as music: self.music_list = yaml.safe_load(music) def build_music_pages_ao1(self, music_list): song_list = [] index = 0 for item in music_list: if 'category' not in item: continue song_list.append('{}#{}'.format(index, item['category'])) index += 1 for song in item['songs']: song_list.append('{}#{}'.format(index, song['name'])) index += 1 song_list = [song_list[x:x + 10] for x in range(0, len(song_list), 10)] return song_list def build_music_list_ao2(self, music_list): song_list = [] for item in music_list: if 'category' not in item: #skip settings n stuff continue song_list.append(item['category']) for song in item['songs']: song_list.append(song['name']) return song_list def is_valid_char_id(self, char_id): """ Check if a character ID is a valid one. :param char_id: character ID :returns: True if within length of character list; False otherwise """ return len(self.char_list) > char_id >= 0 def get_char_id_by_name(self, name): """ Get a character ID by the name of the character. :param name: name of character :returns: Character ID """ for i, ch in enumerate(self.char_list): if ch.lower() == name.lower(): return i raise ServerError('Character not found.') def get_song_data(self, music_list, music): """ Get information about a track, if exists. :param music_list: music list to search :param music: track name :returns: tuple (name, length or -1) :raises: ServerError if track not found """ for item in music_list: if 'category' not in item: #skip settings n stuff continue if item['category'] == music: return item['category'], -1 for song in item['songs']: if song['name'] == music: try: return song['name'], song['length'] except KeyError: return song['name'], -1 raise ServerError('Music not found.') def send_all_cmd_pred(self, cmd, *args, pred=lambda x: True): """ Broadcast an AO-compatible command to all clients that satisfy a predicate. """ for client in self.client_manager.clients: if pred(client): client.send_command(cmd, *args) def broadcast_global(self, client, msg, as_mod=False): """ Broadcast an OOC message to all clients that do not have global chat muted. :param client: sender :param msg: message :param as_mod: add moderator prefix (Default value = False) """ char_name = client.char_name ooc_name = '{}[{}][{}]'.format('<dollar>G', client.area.abbreviation, char_name) if as_mod: ooc_name += '[M]' self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: not x.muted_global) def send_modchat(self, client, msg): """ Send an OOC message to all mods. :param client: sender :param msg: message """ name = client.name ooc_name = '{}[{}][{}]'.format('<dollar>M', client.area.abbreviation, name) self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: x.is_mod) def broadcast_need(self, client, msg): """ Broadcast an OOC "need" message to all clients who do not have advertisements muted. :param client: sender :param msg: message """ char_name = client.char_name area_name = client.area.name area_id = client.area.abbreviation self.send_all_cmd_pred( 'CT', '{}'.format(self.config['hostname']), '=== Advert ===\r\n{} in {} [{}] needs {}\r\n==============='. format(char_name, area_name, area_id, msg), '1', pred=lambda x: not x.muted_adverts) def send_arup(self, args): """Update the area properties for 2.6 clients. Playercount: ARUP#0#<area1_p: int>#<area2_p: int>#... Status: ARUP#1##<area1_s: string>##<area2_s: string>#... CM: ARUP#2##<area1_cm: string>##<area2_cm: string>#... Lockedness: ARUP#3##<area1_l: string>##<area2_l: string>#... :param args: """ if len(args) < 2: # An argument count smaller than 2 means we only got the identifier of ARUP. return if args[0] not in (0, 1, 2, 3): return if args[0] == 0: for part_arg in args[1:]: try: _sanitised = int(part_arg) except: return elif args[0] in (1, 2, 3): for part_arg in args[1:]: try: _sanitised = str(part_arg) except: return self.send_all_cmd_pred('ARUP', *args, pred=lambda x: True) def refresh(self): """ Refresh as many parts of the server as possible: - MOTD - Mod credentials (unmodding users if necessary) - Characters - Music - Backgrounds - Commands - Banlists """ with open('config/config.yaml', 'r') as cfg: cfg_yaml = yaml.safe_load(cfg) self.config['motd'] = cfg_yaml['motd'].replace('\\n', ' \n') # Reload moderator passwords list and unmod any moderator affected by # credential changes or removals if isinstance(self.config['modpass'], str): self.config['modpass'] = { 'default': { 'password': self.config['modpass'] } } if isinstance(cfg_yaml['modpass'], str): cfg_yaml['modpass'] = { 'default': { 'password': cfg_yaml['modpass'] } } for profile in self.config['modpass']: if profile not in cfg_yaml['modpass'] or \ self.config['modpass'][profile] != cfg_yaml['modpass'][profile]: for client in filter( lambda c: c.mod_profile_name == profile, self.client_manager.clients): client.is_mod = False client.mod_profile_name = None database.log_misc('unmod.modpass', client) client.send_ooc( 'Your moderator credentials have been revoked.') self.config['modpass'] = cfg_yaml['modpass'] self.load_characters() self.load_iniswaps() self.load_music() self.load_backgrounds() self.load_ipranges() import server.commands importlib.reload(server.commands) server.commands.reload()
def __init__(self): self.software = 'KFO-Server' self.release = 3 self.major_version = 3 self.minor_version = 0 self.config = None self.censors = None self.allowed_iniswaps = [] self.char_list = None self.char_emotes = None self.char_pages_ao1 = None self.music_list = [] self.music_list_ao2 = None self.music_pages_ao1 = None self.backgrounds = None self.zalgo_tolerance = None self.ipRange_bans = [] self.geoIpReader = None self.useGeoIp = False self.supported_features = [ 'yellowtext', 'customobjections', 'prezoom', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence', 'modcall_reason', 'cccc_ic_support', 'casing_alerts', 'arup', 'looping_sfx', 'additive', 'effects' ] try: self.geoIpReader = geoip2.database.Reader( './storage/GeoLite2-ASN.mmdb') self.useGeoIp = True # on debian systems you can use /usr/share/GeoIP/GeoIPASNum.dat if the geoip-database-extra package is installed except FileNotFoundError: self.useGeoIp = False pass self.ms_client = None try: self.load_config() self.load_censors() self.load_iniswaps() self.load_characters() self.load_music() self.load_backgrounds() self.load_ipranges() self.hub_manager = HubManager(self) except yaml.YAMLError as exc: print('There was a syntax error parsing a configuration file:') print(exc) print('Please revise your syntax and restart the server.') sys.exit(1) except OSError as exc: print('There was an error opening or writing to a file:') print(exc) sys.exit(1) except Exception as exc: print('There was a configuration error:') print(exc) print('Please check sample config files for the correct format.') sys.exit(1) self.client_manager = ClientManager(self) server.logger.setup_logger(debug=self.config['debug']) self.webhooks = Webhooks(self) self.bridgebot = None
class TsuServer3: """The main class for KFO-Server derivative of tsuserver3 server software.""" def __init__(self): self.software = 'KFO-Server' self.release = 3 self.major_version = 3 self.minor_version = 0 self.config = None self.censors = None self.allowed_iniswaps = [] self.char_list = None self.char_emotes = None self.char_pages_ao1 = None self.music_list = [] self.music_list_ao2 = None self.music_pages_ao1 = None self.backgrounds = None self.zalgo_tolerance = None self.ipRange_bans = [] self.geoIpReader = None self.useGeoIp = False self.supported_features = [ 'yellowtext', 'customobjections', 'prezoom', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence', 'modcall_reason', 'cccc_ic_support', 'casing_alerts', 'arup', 'looping_sfx', 'additive', 'effects' ] try: self.geoIpReader = geoip2.database.Reader( './storage/GeoLite2-ASN.mmdb') self.useGeoIp = True # on debian systems you can use /usr/share/GeoIP/GeoIPASNum.dat if the geoip-database-extra package is installed except FileNotFoundError: self.useGeoIp = False pass self.ms_client = None try: self.load_config() self.load_censors() self.load_iniswaps() self.load_characters() self.load_music() self.load_backgrounds() self.load_ipranges() self.hub_manager = HubManager(self) except yaml.YAMLError as exc: print('There was a syntax error parsing a configuration file:') print(exc) print('Please revise your syntax and restart the server.') sys.exit(1) except OSError as exc: print('There was an error opening or writing to a file:') print(exc) sys.exit(1) except Exception as exc: print('There was a configuration error:') print(exc) print('Please check sample config files for the correct format.') sys.exit(1) self.client_manager = ClientManager(self) server.logger.setup_logger(debug=self.config['debug']) self.webhooks = Webhooks(self) self.bridgebot = None def start(self): """Start the server.""" loop = asyncio.get_event_loop() bound_ip = '0.0.0.0' if self.config['local']: bound_ip = '127.0.0.1' ao_server_crt = loop.create_server(lambda: AOProtocol(self), bound_ip, self.config['port']) ao_server = loop.run_until_complete(ao_server_crt) if self.config['use_websockets']: ao_server_ws = websockets.serve(new_websocket_client(self), bound_ip, self.config['websocket_port']) asyncio.ensure_future(ao_server_ws) if self.config['use_masterserver']: self.ms_client = MasterServerClient(self) asyncio.ensure_future(self.ms_client.connect(), loop=loop) if self.config['zalgo_tolerance']: self.zalgo_tolerance = self.config['zalgo_tolerance'] if 'bridgebot' in self.config and self.config['bridgebot']['enabled']: self.bridgebot = Bridgebot(self, self.config['bridgebot']['channel'], self.config['bridgebot']['hub_id'], self.config['bridgebot']['area_id']) asyncio.ensure_future(self.bridgebot.init( self.config['bridgebot']['token']), loop=loop) asyncio.ensure_future(self.schedule_unbans()) database.log_misc('start') print('Server started and is listening on port {}'.format( self.config['port'])) try: loop.run_forever() except KeyboardInterrupt: pass database.log_misc('stop') ao_server.close() loop.run_until_complete(ao_server.wait_closed()) loop.close() async def schedule_unbans(self): while True: database.schedule_unbans() await asyncio.sleep(3600 * 12) @property def version(self): """Get the server's current version.""" return f'{self.release}.{self.major_version}.{self.minor_version}' def new_client(self, transport): """ Create a new client based on a raw transport by passing it to the client manager. :param transport: asyncio transport :returns: created client object """ peername = transport.get_extra_info('peername')[0] if self.useGeoIp: try: geoIpResponse = self.geoIpReader.asn(peername) asn = str(geoIpResponse.autonomous_system_number) except geoip2.errors.AddressNotFoundError: asn = "Loopback" pass else: asn = "Loopback" for line, rangeBan in enumerate(self.ipRange_bans): if rangeBan != "" and peername.startswith( rangeBan) or asn == rangeBan: msg = 'BD#' msg += 'Abuse\r\n' msg += f'ID: {line}\r\n' msg += 'Until: N/A' msg += '#%' transport.write(msg.encode('utf-8')) raise ClientError c = self.client_manager.new_client(transport) c.server = self c.area = self.hub_manager.default_hub().default_area() c.area.new_client(c) return c def remove_client(self, client): """ Remove a disconnected client. :param client: client object """ area = client.area if not client.hidden and not client.sneaking: area.broadcast_ooc( f'[{client.id}] {client.showname} has disconnected.') area.remove_client(client) self.client_manager.remove_client(client) @property def player_count(self): """Get the number of non-spectating clients.""" return len([ client for client in self.client_manager.clients if client.char_id != -1 ]) def load_config(self): """Load the main server configuration from a YAML file.""" try: with open('config/config.yaml', 'r', encoding='utf-8') as cfg: self.config = yaml.safe_load(cfg) self.config['motd'] = self.config['motd'].replace('\\n', ' \n') except OSError: print('error: config/config.yaml wasn\'t found.') print('You are either running from the wrong directory, or') print( 'you forgot to rename config_sample (read the instructions).') sys.exit(1) if 'music_change_floodguard' not in self.config: self.config['music_change_floodguard'] = { 'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0 } if 'wtce_floodguard' not in self.config: self.config['wtce_floodguard'] = { 'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0 } if 'zalgo_tolerance' not in self.config: self.config['zalgo_tolerance'] = 3 if isinstance(self.config['modpass'], str): self.config['modpass'] = { 'default': { 'password': self.config['modpass'] } } if 'multiclient_limit' not in self.config: self.config['multiclient_limit'] = 16 def load_censors(self): """Load a list of banned words to scrub from chats.""" try: with open('config/censors.yaml', 'r', encoding='utf-8') as censors: self.censors = yaml.safe_load(censors) except: logger.debug('Cannot find censors.yaml') def load_characters(self): """Load the character list from a YAML file.""" with open('config/characters.yaml', 'r', encoding='utf-8') as chars: self.char_list = yaml.safe_load(chars) self.build_char_pages_ao1() self.char_emotes = {char: Emotes(char) for char in self.char_list} def load_music(self): self.build_music_list() self.music_pages_ao1 = self.build_music_pages_ao1(self.music_list) self.music_list_ao2 = self.build_music_list_ao2(self.music_list) def load_backgrounds(self): """Load the backgrounds list from a YAML file.""" with open('config/backgrounds.yaml', 'r', encoding='utf-8') as bgs: self.backgrounds = yaml.safe_load(bgs) def load_iniswaps(self): """Load a list of characters for which INI swapping is allowed.""" try: with open('config/iniswaps.yaml', 'r', encoding='utf-8') as iniswaps: self.allowed_iniswaps = yaml.safe_load(iniswaps) except: logger.debug('Cannot find iniswaps.yaml') def load_ipranges(self): """Load a list of banned IP ranges.""" try: with open('config/iprange_ban.txt', 'r', encoding='utf-8') as ipranges: self.ipRange_bans = ipranges.read().splitlines() except: logger.debug('Cannot find iprange_ban.txt') def build_char_pages_ao1(self): """ Cache a list of characters that can be used for the AO1 connection handshake. """ self.char_pages_ao1 = [ self.char_list[x:x + 10] for x in range(0, len(self.char_list), 10) ] for i in range(len(self.char_list)): self.char_pages_ao1[i // 10][i % 10] = '{}#{}&&0&&&0&'.format( i, self.char_list[i]) def build_music_list(self): with open('config/music.yaml', 'r', encoding='utf-8') as music: self.music_list = yaml.safe_load(music) def build_music_pages_ao1(self, music_list): song_list = [] index = 0 for item in music_list: if 'category' not in item: continue song_list.append('{}#{}'.format(index, item['category'])) index += 1 for song in item['songs']: song_list.append('{}#{}'.format(index, song['name'])) index += 1 song_list = [song_list[x:x + 10] for x in range(0, len(song_list), 10)] return song_list def build_music_list_ao2(self, music_list): song_list = [] for item in music_list: if 'category' not in item: #skip settings n stuff continue song_list.append(item['category']) for song in item['songs']: song_list.append(song['name']) return song_list def is_valid_char_id(self, char_id): """ Check if a character ID is a valid one. :param char_id: character ID :returns: True if within length of character list; False otherwise """ return len(self.char_list) > char_id >= 0 def get_char_id_by_name(self, name): """ Get a character ID by the name of the character. :param name: name of character :returns: Character ID """ for i, ch in enumerate(self.char_list): if ch.lower() == name.lower(): return i raise ServerError('Character not found.') def get_song_data(self, music_list, music): """ Get information about a track, if exists. :param music_list: music list to search :param music: track name :returns: tuple (name, length or -1) :raises: ServerError if track not found """ for item in music_list: if 'category' not in item: #skip settings n stuff continue if item['category'] == music: return item['category'], 0 for song in item['songs']: if song['name'] == music: try: return song['name'], song['length'] except KeyError: return song['name'], 0 raise ServerError('Music not found.') def send_all_cmd_pred(self, cmd, *args, pred=lambda x: True): """ Broadcast an AO-compatible command to all clients that satisfy a predicate. """ for client in self.client_manager.clients: if pred(client): client.send_command(cmd, *args) def broadcast_global(self, client, msg, as_mod=False): """ Broadcast an OOC message to all clients that do not have global chat muted. :param client: sender :param msg: message :param as_mod: add moderator prefix (Default value = False) """ if as_mod: as_mod = '[M]' else: as_mod = '' ooc_name = f'<dollar>G[{client.area.area_manager.abbreviation}]|{as_mod}{client.name}' self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: not x.muted_global) def send_modchat(self, client, msg): """ Send an OOC message to all mods. :param client: sender :param msg: message """ ooc_name = '{}[{}][{}]'.format('<dollar>M', client.area.id, client.name) self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: x.is_mod) def broadcast_need(self, client, msg): """ Broadcast an OOC "need" message to all clients who do not have advertisements muted. :param client: sender :param msg: message """ self.send_all_cmd_pred( 'CT', self.config['hostname'], f'=== Advert ===\r\n{client.name} in {client.area.name} [{client.area.id}] (Hub {client.area.area_manager.id}) needs {msg}\r\n===============', '1', pred=lambda x: not x.muted_adverts) def send_arup(self, client, args): """Update the area properties for this 2.6 client. Playercount: ARUP#0#<area1_p: int>#<area2_p: int>#... Status: ARUP#1#<area1_s: string>#<area2_s: string>#... CM: ARUP#2#<area1_cm: string>#<area2_cm: string>#... Lockedness: ARUP#3#<area1_l: string>#<area2_l: string>#... :param args: """ if len(args) < 2: # An argument count smaller than 2 means we only got the identifier of ARUP. return if args[0] not in (0, 1, 2, 3): return if args[0] == 0: for part_arg in args[1:]: try: _sanitised = int(part_arg) except: return elif args[0] in (1, 2, 3): for part_arg in args[1:]: try: _sanitised = str(part_arg) except: return client.send_command('ARUP', *args) def send_discord_chat(self, name, message, hub_id=0, area_id=0): area = self.hub_manager.get_hub_by_id(hub_id).get_area_by_id(area_id) cid = self.get_char_id_by_name(self.config['bridgebot']['character']) message = dezalgo(message) message = remove_URL(message) message = message.replace('}', '\\}').replace('{', '\\{').replace( '`', '\\`').replace('|', '\\|').replace('~', '\\~').replace( 'º', '\\º').replace('№', '\\№').replace('√', '\\√').replace( '\\s', '').replace('\\f', '') message = message.replace('#', '<num>').replace('&', '<and>').replace( '%', '<percent>').replace('$', '<dollar>') message = self.config['bridgebot']['prefix'] + message if len(name) > 14: name = name[:14].rstrip() + '.' area.send_ic(None, '1', 0, self.config['bridgebot']['character'], self.config['bridgebot']['emote'], message, self.config['bridgebot']['pos'], "", 0, cid, 0, 0, [0], 0, 0, 0, name, -1, "", "", 0, 0, 0, 0, "0", 0, "", "", "", 0, "") # Please forgive my sin def send_discord_ooc(self, name, message, hub_id=0, area_id=0): area = self.hub_manager.get_hub_by_id(hub_id).get_area_by_id(area_id) cid = self.get_char_id_by_name(self.config['bridgebot']['character']) message = dezalgo(message) message = message.replace('}', '\\}').replace('{', '\\{').replace( '`', '\\`').replace('|', '\\|').replace('~', '\\~').replace( 'º', '\\º').replace('№', '\\№').replace('√', '\\√').replace( '\\s', '').replace('\\f', '') message = message.replace('#', '<num>').replace('&', '<and>').replace( '%', '<percent>').replace('$', '<dollar>') if len(name) > 30: name = name[:30].rstrip() + '.' area.send_ooc(f"<dollar>[DIS] {name}", message) # [DIS] is changable def refresh(self): """ Refresh as many parts of the server as possible: - MOTD - Mod credentials (unmodding users if necessary) - Censors - Characters - Music - Backgrounds - Commands - Banlists """ with open('config/config.yaml', 'r') as cfg: cfg_yaml = yaml.safe_load(cfg) self.config['motd'] = cfg_yaml['motd'].replace('\\n', ' \n') # Reload moderator passwords list and unmod any moderator affected by # credential changes or removals if isinstance(self.config['modpass'], str): self.config['modpass'] = { 'default': { 'password': self.config['modpass'] } } if isinstance(cfg_yaml['modpass'], str): cfg_yaml['modpass'] = { 'default': { 'password': cfg_yaml['modpass'] } } for profile in self.config['modpass']: if profile not in cfg_yaml['modpass'] or \ self.config['modpass'][profile] != cfg_yaml['modpass'][profile]: for client in filter( lambda c: c.mod_profile_name == profile, self.client_manager.clients): client.is_mod = False client.mod_profile_name = None database.log_misc('unmod.modpass', client) client.send_ooc( 'Your moderator credentials have been revoked.') self.config['modpass'] = cfg_yaml['modpass'] self.load_censors() self.load_characters() self.load_iniswaps() self.load_music() self.load_backgrounds() self.load_ipranges() import server.commands importlib.reload(server.commands) server.commands.reload()
class TsuServer3: def __init__(self): self.config = None self.allowed_iniswaps = None self.load_config() self.load_iniswaps() self.client_manager = ClientManager(self) self.area_manager = AreaManager(self) self.ban_manager = BanManager() self.software = 'tsuserver3' self.version = 'tsuserver3dev' self.release = 3 self.major_version = 1 self.minor_version = 1 self.ipid_list = {} self.hdid_list = {} self.char_list = None self.char_pages_ao1 = None self.music_list = None self.music_list_ao2 = None self.music_pages_ao1 = None self.backgrounds = None self.load_characters() self.load_music() self.load_backgrounds() self.load_ids() self.district_client = None self.ms_client = None self.rp_mode = False logger.setup_logger(debug=self.config['debug']) def start(self): loop = asyncio.get_event_loop() bound_ip = '0.0.0.0' if self.config['local']: bound_ip = '127.0.0.1' ao_server_crt = loop.create_server(lambda: AOProtocol(self), bound_ip, self.config['port']) ao_server = loop.run_until_complete(ao_server_crt) if self.config['use_district']: self.district_client = DistrictClient(self) asyncio.ensure_future(self.district_client.connect(), loop=loop) if self.config['use_masterserver']: self.ms_client = MasterServerClient(self) asyncio.ensure_future(self.ms_client.connect(), loop=loop) logger.log_debug('Server started.') try: loop.run_forever() except KeyboardInterrupt: pass logger.log_debug('Server shutting down.') ao_server.close() loop.run_until_complete(ao_server.wait_closed()) loop.close() def get_version_string(self): return str(self.release) + '.' + str(self.major_version) + '.' + str( self.minor_version) def new_client(self, transport): c = self.client_manager.new_client(transport) if self.rp_mode: c.in_rp = True c.server = self c.area = self.area_manager.default_area() c.area.new_client(c) return c def remove_client(self, client): client.area.remove_client(client) self.client_manager.remove_client(client) def get_player_count(self): return len(self.client_manager.clients) def load_config(self): with open('config/config.yaml', 'r', encoding='utf-8') as cfg: self.config = yaml.load(cfg) self.config['motd'] = self.config['motd'].replace('\\n', ' \n') if 'music_change_floodguard' not in self.config: self.config['music_change_floodguard'] = { 'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0 } if 'wtce_floodguard' not in self.config: self.config['wtce_floodguard'] = { 'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0 } def load_characters(self): with open('config/characters.yaml', 'r', encoding='utf-8') as chars: self.char_list = yaml.load(chars) self.build_char_pages_ao1() def load_music(self): with open('config/music.yaml', 'r', encoding='utf-8') as music: self.music_list = yaml.load(music) self.build_music_pages_ao1() self.build_music_list_ao2() def load_ids(self): self.ipid_list = {} self.hdid_list = {} #load ipids try: with open('storage/ip_ids.json', 'r', encoding='utf-8') as whole_list: self.ipid_list = json.loads(whole_list.read()) except: logger.log_debug( 'Failed to load ip_ids.json from ./storage. If ip_ids.json is exist then remove it.' ) #load hdids try: with open('storage/hd_ids.json', 'r', encoding='utf-8') as whole_list: self.hdid_list = json.loads(whole_list.read()) except: logger.log_debug( 'Failed to load hd_ids.json from ./storage. If hd_ids.json is exist then remove it.' ) def dump_ipids(self): with open('storage/ip_ids.json', 'w') as whole_list: json.dump(self.ipid_list, whole_list) def dump_hdids(self): with open('storage/hd_ids.json', 'w') as whole_list: json.dump(self.hdid_list, whole_list) def get_ipid(self, ip): if not (ip in self.ipid_list): self.ipid_list[ip] = len(self.ipid_list) self.dump_ipids() return self.ipid_list[ip] def load_backgrounds(self): with open('config/backgrounds.yaml', 'r', encoding='utf-8') as bgs: self.backgrounds = yaml.load(bgs) def load_iniswaps(self): try: with open('config/iniswaps.yaml', 'r', encoding='utf-8') as iniswaps: self.allowed_iniswaps = yaml.load(iniswaps) except: logger.log_debug('cannot find iniswaps.yaml') def build_char_pages_ao1(self): self.char_pages_ao1 = [ self.char_list[x:x + 10] for x in range(0, len(self.char_list), 10) ] for i in range(len(self.char_list)): self.char_pages_ao1[i // 10][i % 10] = '{}#{}&&0&&&0&'.format( i, self.char_list[i]) def build_music_pages_ao1(self): self.music_pages_ao1 = [] index = 0 # add areas first for area in self.area_manager.areas: self.music_pages_ao1.append('{}#{}'.format(index, area.name)) index += 1 # then add music for item in self.music_list: self.music_pages_ao1.append('{}#{}'.format(index, item['category'])) index += 1 for song in item['songs']: self.music_pages_ao1.append('{}#{}'.format( index, song['name'])) index += 1 self.music_pages_ao1 = [ self.music_pages_ao1[x:x + 10] for x in range(0, len(self.music_pages_ao1), 10) ] def build_music_list_ao2(self): self.music_list_ao2 = [] # add areas first for area in self.area_manager.areas: self.music_list_ao2.append(area.name) # then add music for item in self.music_list: self.music_list_ao2.append(item['category']) for song in item['songs']: self.music_list_ao2.append(song['name']) def is_valid_char_id(self, char_id): return len(self.char_list) > char_id >= 0 def get_char_id_by_name(self, name): for i, ch in enumerate(self.char_list): if ch.lower() == name.lower(): return i raise ServerError('Character not found.') def get_song_data(self, music): for item in self.music_list: if item['category'] == music: return item['category'], -1 for song in item['songs']: if song['name'] == music: try: return song['name'], song['length'] except KeyError: return song['name'], -1 raise ServerError('Music not found.') def send_all_cmd_pred(self, cmd, *args, pred=lambda x: True): for client in self.client_manager.clients: if pred(client): client.send_command(cmd, *args) def broadcast_global(self, client, msg, as_mod=False): char_name = client.get_char_name() ooc_name = '{}[{}][{}]'.format('<dollar>G', client.area.id, char_name) if as_mod: ooc_name += '[M]' self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: not x.muted_global) if self.config['use_district']: self.district_client.send_raw_message('GLOBAL#{}#{}#{}#{}'.format( int(as_mod), client.area.id, char_name, msg)) def broadcast_need(self, client, msg): char_name = client.get_char_name() area_name = client.area.name area_id = client.area.id self.send_all_cmd_pred( 'CT', '{}'.format(self.config['hostname']), '=== Advert ===\r\n{} in {} [{}] needs {}\r\n==============='. format(char_name, area_name, area_id, msg), pred=lambda x: not x.muted_adverts) if self.config['use_district']: self.district_client.send_raw_message('NEED#{}#{}#{}#{}'.format( char_name, area_name, area_id, msg)) def refresh(self): with open('config/config.yaml', 'r') as cfg: self.config['motd'] = yaml.load(cfg)['motd'].replace('\\n', ' \n') with open('config/characters.yaml', 'r') as chars: self.char_list = yaml.load(chars) with open('config/music.yaml', 'r') as music: self.music_list = yaml.load(music) self.build_music_pages_ao1() self.build_music_list_ao2() with open('config/backgrounds.yaml', 'r') as bgs: self.backgrounds = yaml.load(bgs)
def __init__(self): self.software = "KFO-Server" self.release = 3 self.major_version = 3 self.minor_version = 0 self.config = None self.censors = None self.allowed_iniswaps = [] self.char_list = None self.char_emotes = None self.music_list = [] self.backgrounds = None self.zalgo_tolerance = None self.ipRange_bans = [] self.geoIpReader = None self.useGeoIp = False self.supported_features = [ "yellowtext", "customobjections", "prezoom", "flipping", "fastloading", "noencryption", "deskmod", "evidence", "modcall_reason", "cccc_ic_support", "casing_alerts", "arup", "looping_sfx", "additive", "effects", "expanded_desk_mods", "y_offset", ] self.command_aliases = {} try: self.geoIpReader = geoip2.database.Reader( "./storage/GeoLite2-ASN.mmdb") self.useGeoIp = True # on debian systems you can use /usr/share/GeoIP/GeoIPASNum.dat if the geoip-database-extra package is installed except FileNotFoundError: self.useGeoIp = False self.ms_client = None sys.setrecursionlimit(50) try: self.load_config() self.load_command_aliases() self.load_censors() self.load_iniswaps() self.load_characters() self.load_music() self.load_backgrounds() self.load_ipranges() self.hub_manager = HubManager(self) except yaml.YAMLError as exc: print("There was a syntax error parsing a configuration file:") print(exc) print("Please revise your syntax and restart the server.") sys.exit(1) except OSError as exc: print("There was an error opening or writing to a file:") print(exc) sys.exit(1) except Exception as exc: print("There was a configuration error:") print(exc) print("Please check sample config files for the correct format.") sys.exit(1) self.client_manager = ClientManager(self) server.logger.setup_logger(debug=self.config["debug"]) self.webhooks = Webhooks(self) self.bridgebot = None
class TsuServer3: """The main class for KFO-Server derivative of tsuserver3 server software.""" def __init__(self): self.software = "KFO-Server" self.release = 3 self.major_version = 3 self.minor_version = 0 self.config = None self.censors = None self.allowed_iniswaps = [] self.char_list = None self.char_emotes = None self.music_list = [] self.backgrounds = None self.zalgo_tolerance = None self.ipRange_bans = [] self.geoIpReader = None self.useGeoIp = False self.supported_features = [ "yellowtext", "customobjections", "prezoom", "flipping", "fastloading", "noencryption", "deskmod", "evidence", "modcall_reason", "cccc_ic_support", "casing_alerts", "arup", "looping_sfx", "additive", "effects", "expanded_desk_mods", "y_offset", ] self.command_aliases = {} try: self.geoIpReader = geoip2.database.Reader( "./storage/GeoLite2-ASN.mmdb") self.useGeoIp = True # on debian systems you can use /usr/share/GeoIP/GeoIPASNum.dat if the geoip-database-extra package is installed except FileNotFoundError: self.useGeoIp = False self.ms_client = None sys.setrecursionlimit(50) try: self.load_config() self.load_command_aliases() self.load_censors() self.load_iniswaps() self.load_characters() self.load_music() self.load_backgrounds() self.load_ipranges() self.hub_manager = HubManager(self) except yaml.YAMLError as exc: print("There was a syntax error parsing a configuration file:") print(exc) print("Please revise your syntax and restart the server.") sys.exit(1) except OSError as exc: print("There was an error opening or writing to a file:") print(exc) sys.exit(1) except Exception as exc: print("There was a configuration error:") print(exc) print("Please check sample config files for the correct format.") sys.exit(1) self.client_manager = ClientManager(self) server.logger.setup_logger(debug=self.config["debug"]) self.webhooks = Webhooks(self) self.bridgebot = None def start(self): """Start the server.""" loop = asyncio.get_event_loop_policy().get_event_loop() bound_ip = "0.0.0.0" if self.config["local"]: bound_ip = "127.0.0.1" ao_server_crt = loop.create_server(lambda: AOProtocol(self), bound_ip, self.config["port"]) ao_server = loop.run_until_complete(ao_server_crt) if self.config["use_websockets"]: ao_server_ws = websockets.serve(new_websocket_client(self), bound_ip, self.config["websocket_port"]) asyncio.ensure_future(ao_server_ws) if self.config["use_masterserver"]: self.ms_client = MasterServerClient(self) asyncio.ensure_future(self.ms_client.connect(), loop=loop) if self.config["zalgo_tolerance"]: self.zalgo_tolerance = self.config["zalgo_tolerance"] if "bridgebot" in self.config and self.config["bridgebot"]["enabled"]: try: self.bridgebot = Bridgebot( self, self.config["bridgebot"]["channel"], self.config["bridgebot"]["hub_id"], self.config["bridgebot"]["area_id"], ) asyncio.ensure_future(self.bridgebot.init( self.config["bridgebot"]["token"]), loop=loop) except Exception as ex: # Don't end the whole server if bridgebot destroys itself print(ex) asyncio.ensure_future(self.schedule_unbans()) database.log_misc("start") print("Server started and is listening on port {}".format( self.config["port"])) try: loop.run_forever() except KeyboardInterrupt: print("KEYBOARD INTERRUPT") loop.stop() database.log_misc("stop") ao_server.close() loop.run_until_complete(ao_server.wait_closed()) loop.close() async def schedule_unbans(self): while True: database.schedule_unbans() await asyncio.sleep(3600 * 12) @property def version(self): """Get the server's current version.""" return f"{self.release}.{self.major_version}.{self.minor_version}" def new_client(self, transport): """ Create a new client based on a raw transport by passing it to the client manager. :param transport: asyncio transport :returns: created client object """ peername = transport.get_extra_info("peername")[0] if self.useGeoIp: try: geoIpResponse = self.geoIpReader.asn(peername) asn = str(geoIpResponse.autonomous_system_number) except geoip2.errors.AddressNotFoundError: asn = "Loopback" pass else: asn = "Loopback" for line, rangeBan in enumerate(self.ipRange_bans): if rangeBan != "" and peername.startswith( rangeBan) or asn == rangeBan: msg = "BD#" msg += "Abuse\r\n" msg += f"ID: {line}\r\n" msg += "Until: N/A" msg += "#%" transport.write(msg.encode("utf-8")) raise ClientError c = self.client_manager.new_client(transport) c.server = self c.area = self.hub_manager.default_hub().default_area() c.area.new_client(c) return c def remove_client(self, client): """ Remove a disconnected client. :param client: client object """ if client.area: area = client.area if (not area.dark and not area.force_sneak and not client.sneaking and not client.hidden): area.broadcast_ooc( f"[{client.id}] {client.showname} has disconnected.") area.remove_client(client) self.client_manager.remove_client(client) @property def player_count(self): """Get the number of non-spectating clients.""" return len([ client for client in self.client_manager.clients if client.char_id != -1 ]) def load_config(self): """Load the main server configuration from a YAML file.""" try: with open("config/config.yaml", "r", encoding="utf-8") as cfg: self.config = yaml.safe_load(cfg) self.config["motd"] = self.config["motd"].replace("\\n", " \n") except OSError: print("error: config/config.yaml wasn't found.") print("You are either running from the wrong directory, or") print( "you forgot to rename config_sample (read the instructions).") sys.exit(1) if "music_change_floodguard" not in self.config: self.config["music_change_floodguard"] = { "times_per_interval": 1, "interval_length": 0, "mute_length": 0, } if "wtce_floodguard" not in self.config: self.config["wtce_floodguard"] = { "times_per_interval": 1, "interval_length": 0, "mute_length": 0, } if "zalgo_tolerance" not in self.config: self.config["zalgo_tolerance"] = 3 if isinstance(self.config["modpass"], str): self.config["modpass"] = { "default": { "password": self.config["modpass"] } } if "multiclient_limit" not in self.config: self.config["multiclient_limit"] = 16 if "asset_url" not in self.config: self.config["asset_url"] = "" if "block_repeat" not in self.config: self.config["block_repeat"] = True def load_command_aliases(self): """Load a list of banned words to scrub from chats.""" try: with open("config/command_aliases.yaml", "r", encoding="utf-8") as command_aliases: self.command_aliases = yaml.safe_load(command_aliases) except Exception: logger.debug("Cannot find command_aliases.yaml") def load_censors(self): """Load a list of banned words to scrub from chats.""" try: with open("config/censors.yaml", "r", encoding="utf-8") as censors: self.censors = yaml.safe_load(censors) except Exception: logger.debug("Cannot find censors.yaml") def load_characters(self): """Load the character list from a YAML file.""" with open("config/characters.yaml", "r", encoding="utf-8") as chars: self.char_list = yaml.safe_load(chars) self.char_emotes = {char: Emotes(char) for char in self.char_list} def load_music(self): self.load_music_list() def load_backgrounds(self): """Load the backgrounds list from a YAML file.""" with open("config/backgrounds.yaml", "r", encoding="utf-8") as bgs: self.backgrounds = yaml.safe_load(bgs) def load_iniswaps(self): """Load a list of characters for which INI swapping is allowed.""" try: with open("config/iniswaps.yaml", "r", encoding="utf-8") as iniswaps: self.allowed_iniswaps = yaml.safe_load(iniswaps) except Exception: logger.debug("Cannot find iniswaps.yaml") def load_ipranges(self): """Load a list of banned IP ranges.""" try: with open("config/iprange_ban.txt", "r", encoding="utf-8") as ipranges: self.ipRange_bans = ipranges.read().splitlines() except Exception: logger.debug("Cannot find iprange_ban.txt") def load_music_list(self): with open("config/music.yaml", "r", encoding="utf-8") as music: self.music_list = yaml.safe_load(music) def build_music_list(self, music_list): song_list = [] for item in music_list: if "category" not in item: # skip settings n stuff continue song_list.append(item["category"]) for song in item["songs"]: song_list.append(song["name"]) return song_list def get_song_data(self, music_list, music): """ Get information about a track, if exists. :param music_list: music list to search :param music: track name :returns: tuple (name, length or -1) :raises: ServerError if track not found """ for item in music_list: if "category" not in item: # skip settings n stuff continue if item["category"] == music: return item["category"], 0 for song in item["songs"]: if song["name"] == music: try: return song["name"], song["length"] except KeyError: return song["name"], -1 raise ServerError("Music not found.") def get_song_is_category(self, music_list, music): """ Get whether a track is a category. :param music_list: music list to search :param music: track name :returns: bool """ for item in music_list: if "category" not in item: # skip settings n stuff continue if item["category"] == music: return True return False def send_all_cmd_pred(self, cmd, *args, pred=lambda x: True): """ Broadcast an AO-compatible command to all clients that satisfy a predicate. """ for client in self.client_manager.clients: if pred(client): client.send_command(cmd, *args) def broadcast_global(self, client, msg, as_mod=False): """ Broadcast an OOC message to all clients that do not have global chat muted. :param client: sender :param msg: message :param as_mod: add moderator prefix (Default value = False) """ if as_mod: as_mod = "[M]" else: as_mod = "" ooc_name = ( f"<dollar>G[{client.area.area_manager.abbreviation}]|{as_mod}{client.name}" ) self.send_all_cmd_pred("CT", ooc_name, msg, pred=lambda x: not x.muted_global) def send_modchat(self, client, msg): """ Send an OOC message to all mods. :param client: sender :param msg: message """ ooc_name = "{}[{}][{}]".format("<dollar>M", client.area.id, client.name) self.send_all_cmd_pred("CT", ooc_name, msg, pred=lambda x: x.is_mod) def broadcast_need(self, client, msg): """ Broadcast an OOC "need" message to all clients who do not have advertisements muted. :param client: sender :param msg: message """ self.send_all_cmd_pred( "CT", self.config["hostname"], f"=== Advert ===\r\n{client.name} in {client.area.name} [{client.area.id}] (Hub {client.area.area_manager.id}) needs {msg}\r\n===============", "1", pred=lambda x: not x.muted_adverts, ) def send_arup(self, client, args): """Update the area properties for this 2.6 client. Playercount: ARUP#0#<area1_p: int>#<area2_p: int>#... Status: ARUP#1#<area1_s: string>#<area2_s: string>#... CM: ARUP#2#<area1_cm: string>#<area2_cm: string>#... Lockedness: ARUP#3#<area1_l: string>#<area2_l: string>#... :param args: """ if len(args) < 2: # An argument count smaller than 2 means we only got the identifier of ARUP. return if args[0] not in (0, 1, 2, 3): return if args[0] == 0: for part_arg in args[1:]: try: int(part_arg) except Exception: return elif args[0] in (1, 2, 3): for part_arg in args[1:]: try: str(part_arg) except Exception: return client.send_command("ARUP", *args) def send_discord_chat(self, name, message, hub_id=0, area_id=0): area = self.hub_manager.get_hub_by_id(hub_id).get_area_by_id(area_id) cid = area.area_manager.get_char_id_by_name( self.config["bridgebot"]["character"]) message = dezalgo(message) message = remove_URL(message) message = (message.replace("}", "\\}").replace("{", "\\{").replace( "`", "\\`").replace("|", "\\|").replace("~", "\\~").replace( "º", "\\º").replace("№", "\\№").replace("√", "\\√").replace( "\\s", "").replace("\\f", "")) message = self.config["bridgebot"]["prefix"] + message if len(name) > 14: name = name[:14].rstrip() + "." area.send_ic( None, "1", 0, self.config["bridgebot"]["character"], self.config["bridgebot"]["emote"], message, self.config["bridgebot"]["pos"], "", 0, cid, 0, 0, [0], 0, 0, 0, name, -1, "", "", 0, 0, 0, 0, "0", 0, "", "", "", 0, "", ) def refresh(self): """ Refresh as many parts of the server as possible: - MOTD - Mod credentials (unmodding users if necessary) - Censors - Characters - Music - Backgrounds - Commands - Banlists """ with open("config/config.yaml", "r") as cfg: cfg_yaml = yaml.safe_load(cfg) self.config["motd"] = cfg_yaml["motd"].replace("\\n", " \n") # Reload moderator passwords list and unmod any moderator affected by # credential changes or removals if isinstance(self.config["modpass"], str): self.config["modpass"] = { "default": { "password": self.config["modpass"] } } if isinstance(cfg_yaml["modpass"], str): cfg_yaml["modpass"] = { "default": { "password": cfg_yaml["modpass"] } } for profile in self.config["modpass"]: if (profile not in cfg_yaml["modpass"] or self.config["modpass"][profile] != cfg_yaml["modpass"][profile]): for client in filter( lambda c: c.mod_profile_name == profile, self.client_manager.clients, ): client.is_mod = False client.mod_profile_name = None database.log_misc("unmod.modpass", client) client.send_ooc( "Your moderator credentials have been revoked.") self.config["modpass"] = cfg_yaml["modpass"] self.load_config() self.load_command_aliases() self.load_censors() self.load_iniswaps() self.load_characters() self.load_music() self.load_backgrounds() self.load_ipranges() import server.commands importlib.reload(server.commands) server.commands.reload()
class TsuServer3: def __init__(self): self.release = 3 self.major_version = 'DR' self.minor_version = '190622b' self.software = 'tsuserver{}'.format(self.get_version_string()) self.version = 'tsuserver{}dev'.format(self.get_version_string()) logger.log_print('Launching {}...'.format(self.software)) logger.log_print('Loading server configurations...') self.config = None self.global_connection = None self.shutting_down = False self.loop = None self.allowed_iniswaps = None self.default_area = 0 self.load_config() self.load_iniswaps() self.char_list = list() self.load_characters() self.client_manager = ClientManager(self) self.area_manager = AreaManager(self) self.ban_manager = BanManager(self) self.ipid_list = {} self.hdid_list = {} self.char_pages_ao1 = None self.music_list = None self.music_list_ao2 = None self.music_pages_ao1 = None self.backgrounds = None self.load_music() self.load_backgrounds() self.load_ids() self.district_client = None self.ms_client = None self.rp_mode = False self.user_auth_req = False self.client_tasks = dict() self.active_timers = dict() self.showname_freeze = False self.commands = importlib.import_module('server.commands') logger.setup_logger(debug=self.config['debug']) def start(self): self.loop = asyncio.get_event_loop() bound_ip = '0.0.0.0' if self.config['local']: bound_ip = '127.0.0.1' logger.log_print( 'Starting a local server. Ignore outbound connection attempts.' ) ao_server_crt = self.loop.create_server(lambda: AOProtocol(self), bound_ip, self.config['port']) ao_server = self.loop.run_until_complete(ao_server_crt) logger.log_pdebug('Server started successfully!\n') if self.config['use_district']: self.district_client = DistrictClient(self) self.global_connection = asyncio.ensure_future( self.district_client.connect(), loop=self.loop) logger.log_print( 'Attempting to connect to district at {}:{}.'.format( self.config['district_ip'], self.config['district_port'])) if self.config['use_masterserver']: self.ms_client = MasterServerClient(self) self.global_connection = asyncio.ensure_future( self.ms_client.connect(), loop=self.loop) logger.log_print( 'Attempting to connect to the master server at {}:{} with the following details:' .format(self.config['masterserver_ip'], self.config['masterserver_port'])) logger.log_print('*Server name: {}'.format( self.config['masterserver_name'])) logger.log_print('*Server description: {}'.format( self.config['masterserver_description'])) try: self.loop.run_forever() except KeyboardInterrupt: pass print('') # Lame logger.log_pdebug('You have initiated a server shut down.') self.shutdown() ao_server.close() self.loop.run_until_complete(ao_server.wait_closed()) self.loop.close() logger.log_print('Server has successfully shut down.') def shutdown(self): # Cleanup operations self.shutting_down = True # Cancel further polling for district/master server if self.global_connection: self.global_connection.cancel() self.loop.run_until_complete( self.await_cancellation(self.global_connection)) # Cancel pending client tasks and cleanly remove them from the areas logger.log_print('Kicking {} remaining clients.'.format( self.get_player_count())) for area in self.area_manager.areas: while area.clients: client = next(iter(area.clients)) area.remove_client(client) for task_id in self.client_tasks[client.id].keys(): task = self.get_task(client, [task_id]) self.loop.run_until_complete(self.await_cancellation(task)) def get_version_string(self): return str(self.release) + '.' + str(self.major_version) + '.' + str( self.minor_version) def reload(self): with open('config/characters.yaml', 'r') as chars: self.char_list = yaml.safe_load(chars) with open('config/music.yaml', 'r') as music: self.music_list = yaml.safe_load(music) self.build_music_pages_ao1() self.build_music_list_ao2() with open('config/backgrounds.yaml', 'r') as bgs: self.backgrounds = yaml.safe_load(bgs) def reload_commands(self): try: self.commands = importlib.reload(self.commands) except Exception as error: return error def new_client(self, transport): c = self.client_manager.new_client(transport) if self.rp_mode: c.in_rp = True c.server = self c.area = self.area_manager.default_area() c.area.new_client(c) return c def remove_client(self, client): client.area.remove_client(client) self.client_manager.remove_client(client) def get_player_count(self): # Ignore players in the server selection screen. return len([ client for client in self.client_manager.clients if client.char_id is not None ]) def load_config(self): with open('config/config.yaml', 'r', encoding='utf-8') as cfg: self.config = yaml.safe_load(cfg) self.config['motd'] = self.config['motd'].replace('\\n', ' \n') if 'music_change_floodguard' not in self.config: self.config['music_change_floodguard'] = { 'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0 } # Backwards compatibility checks if 'spectator_name' not in self.config: self.config['spectator_name'] = 'SPECTATOR' if 'showname_max_length' not in self.config: self.config['showname_max_length'] = 30 if 'sneak_handicap' not in self.config: self.config['sneak_handicap'] = 5 # Seconds if 'blackout_background' not in self.config: self.config['blackout_background'] = 'Blackout_HD' if 'discord_link' not in self.config: self.config['discord_link'] = 'None' if 'default_area_description' not in self.config: self.config['default_area_description'] = 'No description.' # Check for uniqueness of all passwords passwords = [ 'guardpass', 'modpass', 'cmpass', 'gmpass', 'gmpass1', 'gmpass2', 'gmpass3', 'gmpass4', 'gmpass5', 'gmpass6', 'gmpass7' ] for (i, password1) in enumerate(passwords): for (j, password2) in enumerate(passwords): if i != j and self.config[password1] == self.config[password2]: info = ( 'Passwords "{}" and "{}" in server/config.yaml match. ' 'Please change them so they are different.'.format( password1, password2)) raise ServerError(info) def load_characters(self): with open('config/characters.yaml', 'r', encoding='utf-8') as chars: self.char_list = yaml.safe_load(chars) self.build_char_pages_ao1() def load_music(self, music_list_file='config/music.yaml', server_music_list=True): try: with open(music_list_file, 'r', encoding='utf-8') as music: music_list = yaml.safe_load(music) except FileNotFoundError: raise ServerError( 'Could not find music list file {}'.format(music_list_file)) if server_music_list: self.music_list = music_list self.build_music_pages_ao1() self.build_music_list_ao2(music_list=music_list) return music_list def load_ids(self): self.ipid_list = {} self.hdid_list = {} #load ipids try: with open('storage/ip_ids.json', 'r', encoding='utf-8') as whole_list: self.ipid_list = json.loads(whole_list.read()) except: logger.log_debug( 'Failed to load ip_ids.json from ./storage. If ip_ids.json exists, then remove it.' ) #load hdids try: with open('storage/hd_ids.json', 'r', encoding='utf-8') as whole_list: self.hdid_list = json.loads(whole_list.read()) except: logger.log_debug( 'Failed to load hd_ids.json from ./storage. If hd_ids.json exists, then remove it.' ) def dump_ipids(self): with open('storage/ip_ids.json', 'w') as whole_list: json.dump(self.ipid_list, whole_list) def dump_hdids(self): with open('storage/hd_ids.json', 'w') as whole_list: json.dump(self.hdid_list, whole_list) def get_ipid(self, ip): if not ip in self.ipid_list: while True: ipid = random.randint(0, 10**10 - 1) if ipid not in self.ipid_list: break self.ipid_list[ip] = ipid self.dump_ipids() return self.ipid_list[ip] def load_backgrounds(self): with open('config/backgrounds.yaml', 'r', encoding='utf-8') as bgs: self.backgrounds = yaml.safe_load(bgs) def load_iniswaps(self): try: with open('config/iniswaps.yaml', 'r', encoding='utf-8') as iniswaps: self.allowed_iniswaps = yaml.safe_load(iniswaps) except: logger.log_debug('cannot find iniswaps.yaml') def build_char_pages_ao1(self): self.char_pages_ao1 = [ self.char_list[x:x + 10] for x in range(0, len(self.char_list), 10) ] for i in range(len(self.char_list)): self.char_pages_ao1[i // 10][i % 10] = '{}#{}&&0&&&0&'.format( i, self.char_list[i]) def build_music_pages_ao1(self): self.music_pages_ao1 = [] index = 0 # add areas first for area in self.area_manager.areas: self.music_pages_ao1.append('{}#{}'.format(index, area.name)) index += 1 # then add music for item in self.music_list: self.music_pages_ao1.append('{}#{}'.format(index, item['category'])) index += 1 for song in item['songs']: self.music_pages_ao1.append('{}#{}'.format( index, song['name'])) index += 1 self.music_pages_ao1 = [ self.music_pages_ao1[x:x + 10] for x in range(0, len(self.music_pages_ao1), 10) ] def build_music_list_ao2(self, from_area=None, c=None, music_list=None): # If not provided a specific music list to overwrite if music_list is None: music_list = self.music_list # Default value # But just in case, check if this came as a request of a client who had a # previous music list preference if c and c.music_list is not None: music_list = c.music_list self.music_list_ao2 = [] # Determine whether to filter the music list or not need_to_check = (from_area is None or '<ALL>' in from_area.reachable_areas or (c is not None and (c.is_staff() or c.is_transient))) # add areas first for area in self.area_manager.areas: if need_to_check or area.name in from_area.reachable_areas: self.music_list_ao2.append("{}-{}".format(area.id, area.name)) # then add music for item in music_list: self.music_list_ao2.append(item['category']) for song in item['songs']: self.music_list_ao2.append(song['name']) def is_valid_char_id(self, char_id): return len(self.char_list) > char_id >= -1 def get_char_id_by_name(self, name): if name == self.config['spectator_name']: return -1 for i, ch in enumerate(self.char_list): if ch.lower() == name.lower(): return i raise ServerError('Character not found.') def get_song_data(self, music, c=None): # The client's personal music list should also be a valid place to search # so search in there too if possible if c and c.music_list: valid_music = self.music_list + c.music_list else: valid_music = self.music_list for item in valid_music: if item['category'] == music: return item['category'], -1 for song in item['songs']: if song['name'] == music: try: return song['name'], song['length'] except KeyError: return song['name'], -1 raise ServerError('Music not found.') def send_all_cmd_pred(self, cmd, *args, pred=lambda x: True): for client in self.client_manager.clients: if pred(client): client.send_command(cmd, *args) def broadcast_global(self, client, msg, as_mod=False, mtype="<dollar>G", condition=lambda x: not x.muted_global): username = client.name ooc_name = '{}[{}][{}]'.format(mtype, client.area.id, username) if as_mod: ooc_name += '[M]' self.send_all_cmd_pred('CT', ooc_name, msg, pred=condition) if self.config['use_district']: self.district_client.send_raw_message('GLOBAL#{}#{}#{}#{}'.format( int(as_mod), client.area.id, username, msg)) def broadcast_need(self, client, msg): char_name = client.get_char_name() area_name = client.area.name area_id = client.area.id self.send_all_cmd_pred( 'CT', '{}'.format(self.config['hostname']), '=== Advert ===\r\n{} in {} [{}] needs {}\r\n==============='. format(char_name, area_name, area_id, msg), pred=lambda x: not x.muted_adverts) if self.config['use_district']: self.district_client.send_raw_message('NEED#{}#{}#{}#{}'.format( char_name, area_name, area_id, msg)) def create_task(self, client, args): # Abort old task if it exists try: old_task = self.get_task(client, args) if not old_task.done() and not old_task.cancelled(): self.cancel_task(old_task) except KeyError: pass # Start new task self.client_tasks[client.id][args[0]] = (asyncio.ensure_future( getattr(self, args[0])(client, args[1:]), loop=self.loop), args[1:]) def cancel_task(self, task): """ Cancels current task and sends order to await cancellation """ task.cancel() asyncio.ensure_future(self.await_cancellation(task)) def remove_task(self, client, args): """ Given client and task name, removes task from server.client_tasks, and cancels it """ task = self.client_tasks[client.id].pop(args[0]) self.cancel_task(task[0]) def get_task(self, client, args): """ Returns actual task instance """ return self.client_tasks[client.id][args[0]][0] def get_task_args(self, client, args): """ Returns input arguments of task """ return self.client_tasks[client.id][args[0]][1] async def await_cancellation(self, old_task): # Wait until it is able to properly retrieve the cancellation exception try: await old_task except asyncio.CancelledError: pass async def as_afk_kick(self, client, args): afk_delay, afk_sendto = args try: delay = int( afk_delay ) * 60 # afk_delay is in minutes, so convert to seconds except (TypeError, ValueError): raise ServerError( 'The area file contains an invalid AFK kick delay for area {}: {}' .format(client.area.id, afk_delay)) if delay <= 0: # Assumes 0-minute delay means that AFK kicking is disabled return try: await asyncio.sleep(delay) except asyncio.CancelledError: raise else: try: area = client.server.area_manager.get_area_by_id( int(afk_sendto)) except: raise ServerError( 'The area file contains an invalid AFK kick destination area for area {}: {}' .format(client.area.id, afk_sendto)) if client.area.id == afk_sendto: # Don't try and kick back to same area return if client.char_id < 0: # Assumes spectators are exempted from AFK kicks return if client.is_staff(): # Assumes staff are exempted from AFK kicks return try: original_area = client.area client.change_area(area, override_passages=True, override_effects=True, ignore_bleeding=True) except: pass # Server raised an error trying to perform the AFK kick, ignore AFK kick else: client.send_host_message( "You were kicked from area {} to area {} for being inactive for {} minutes." .format(original_area.id, afk_sendto, afk_delay)) if client.area.is_locked or client.area.is_modlocked: client.area.invite_list.pop(client.ipid) async def as_timer(self, client, args): _, length, name, is_public = args # Length in seconds, already converted client_name = client.name # Failsafe in case client disconnects before task is cancelled/expires try: await asyncio.sleep(length) except asyncio.CancelledError: self.send_all_cmd_pred( 'CT', '{}'.format(self.config['hostname']), 'Timer "{}" initiated by {} has been canceled.'.format( name, client_name), pred=lambda c: (c == client or c.is_staff() or (is_public and c.area == client.area))) else: self.send_all_cmd_pred( 'CT', '{}'.format(self.config['hostname']), 'Timer "{}" initiated by {} has expired.'.format( name, client_name), pred=lambda c: (c == client or c.is_staff() or (is_public and c.area == client.area))) finally: del self.active_timers[name] async def as_handicap(self, client, args): _, length, _, announce_if_over = args client.is_movement_handicapped = True try: await asyncio.sleep(length) except asyncio.CancelledError: pass # Cancellation messages via send_host_messages must be sent manually else: if announce_if_over and not client.is_staff(): client.send_host_message( 'Your movement handicap has expired. You may now move to a new area.' ) finally: client.is_movement_handicapped = False def timer_remaining(self, start, length): current = time.time() remaining = start + length - current if remaining < 10: remain_text = "{} seconds".format('{0:.1f}'.format(remaining)) elif remaining < 60: remain_text = "{} seconds".format(int(remaining)) elif remaining < 3600: remain_text = "{}:{}".format(int(remaining // 60), '{0:02d}'.format(int(remaining % 60))) else: remain_text = "{}:{}:{}".format( int(remaining // 3600), '{0:02d}'.format(int((remaining % 3600) // 60)), '{0:02d}'.format(int(remaining % 60))) return remaining, remain_text
def setUpClass(cls): cls.cm = ClientManager() cls.loop = asyncio.get_event_loop()