def add_poll(self, value): test = time.strftime('%y-%m-%d %H%M-%S') if not ([item for item in self.poll_list if item[0] == value]): if len(self.poll_list) < self.server.config['poll_slots']: self.poll_list.append([value, test]) tmp = time.strftime('%y-%m-%d %H:%M:%S') newfile = { 'name': value, 'polldetail': None, 'multivote': False, 'choices': ["Yes", "No"], 'votes': {"yes": 0, "no": 0}, 'created': tmp, 'log': [], } with open('storage/poll/{} \'{}\'.yaml'.format(test, value), 'w') as file: yaml.dump(newfile, file, default_flow_style=False) logger.log_serverpoll('Poll \'{}\' added successfully.'.format(value)) else: logger.log_serverpoll('Failed to add poll. Reason: The poll queue is full.') raise ServerError('The Poll Queue is full!') else: logger.log_serverpoll('Failed to add poll. Reason: This poll already exists.') raise ServerError('This poll already exists.') self.write_poll_list()
def prepare_music_list(self, c=None, specific_music_list=None): """ If `specific_music_list` is not None, return a client-ready version of that music list. Else, if `c` is a client with a custom chosen music list, return their latest music list. Otherwise, return a client-ready version of the server music list. Parameters ---------- c: ClientManager.Client Client whose current music list if it exists will be considered if `specific_music_list` is None specific_music_list: list of dictionaries with key sets {'category', 'songs'} Music list to use if given Returns ------- list of str Music list ready to be sent to clients """ # If not provided a specific music list to overwrite if specific_music_list is None: specific_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: specific_music_list = c.music_list prepared_music_list = list() try: for item in specific_music_list: prepared_music_list.append(item['category']) for song in item['songs']: if 'length' not in song: name, length = song['name'], -1 else: name, length = song['name'], song['length'] # Check that length is a number, and if not, abort. if not isinstance(length, (int, float)): msg = ( "The music list expected a numerical length for track '{}', but " "found it had length '{}'.").format( song['name'], song['length']) raise ServerError.MusicInvalidError(msg) prepared_music_list.append(name) except KeyError as err: msg = ( "The music list expected key '{}' for item {}, but could not find it." .format(err.args[0], item)) raise ServerError.MusicInvalid(msg) except TypeError: msg = ( "The music list expected songs to be listed for item {}, but could not find any." .format(item)) raise ServerError.MusicInvalid(msg) return prepared_music_list
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 try: 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 except KeyError as err: msg = ("The music list expected key '{}' for item {}, but could not find it." .format(err.args[0], item)) raise ServerError.MusicInvalid(msg) except TypeError: msg = ("The music list expected songs to be listed for item {}, but could not find any." .format(item)) raise ServerError.MusicInvalid(msg) self.music_pages_ao1 = [self.music_pages_ao1[x:x + 10] for x in range(0, len(self.music_pages_ao1), 10)]
def load_config(self): with Constants.fopen('config/config.yaml', 'r', encoding='utf-8') as cfg: self.config = Constants.yaml_load(cfg) self.config['motd'] = self.config['motd'].replace('\\n', ' \n') for i in range(1, 8): daily_gmpass = '******'.format(i) if daily_gmpass not in self.config or not self.config[daily_gmpass]: self.config[daily_gmpass] = None # Default values to fill in config.yaml if not present defaults_for_tags = { 'discord_link': None, 'max_numdice': 20, 'max_numfaces': 11037, 'max_modifier_length': 12, 'max_acceptable_term': 22074, 'def_numdice': 1, 'def_numfaces': 6, 'def_modifier': '', 'blackout_background': 'Blackout_HD', 'default_area_description': 'No description.', 'party_lights_timeout': 10, 'showname_max_length': 30, 'sneak_handicap': 5, 'spectator_name': 'SPECTATOR', 'music_change_floodguard': { 'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0 } } for (tag, value) in defaults_for_tags.items(): if tag not in self.config: self.config[tag] = value # Check that all passwords were generated and that they are unique passwords = [ 'guardpass', 'modpass', 'cmpass', 'gmpass', 'gmpass1', 'gmpass2', 'gmpass3', 'gmpass4', 'gmpass5', 'gmpass6', 'gmpass7' ] for password_type in passwords: if password_type not in self.config: info = ( 'Password "{}" not defined in server/config.yaml. Please make sure it is ' 'set and try again.'.format(password_type)) raise ServerError(info) for (i, password1) in enumerate(passwords): for (j, password2) in enumerate(passwords): if i != j and self.config[password1] == self.config[ password2] != None: info = ( 'Passwords "{}" and "{}" in server/config.yaml match. ' 'Please change them so they are different and try again.' .format(password1, password2)) raise ServerError(info)
def fopen(file, *args, **kwargs): try: f = open(file, *args, **kwargs) return f except FileNotFoundError: info = 'File not found: {}'.format(file) raise ServerError(info, code="FileNotFound") except OSError as ex: raise ServerError(str(ex))
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): info = ('The area file contains an invalid AFK kick delay for area {}: {}'. format(client.area.id, afk_delay)) raise ServerError(info) 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 Exception: info = ('The area file contains an invalid AFK kick destination area for area {}: ' '{}'.format(client.area.id, afk_sendto)) raise ServerError(info) 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 original_name = client.displayname client.change_area(area, override_passages=True, override_effects=True, ignore_bleeding=True) except Exception: pass # Server raised an error trying to perform the AFK kick, ignore AFK kick else: client.send_ooc('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: try: # Try and remove the IPID from the area's invite list client.area.invite_list.pop(client.ipid) except KeyError: pass # Would only happen if they joined the locked area through mod powers if client.party: p = client.party client.party.remove_member(client) client.send_ooc('You were also kicked off from your party.') for c in p.get_members(): c.send_ooc('{} was AFK kicked from your party.'.format(original_name))
def remove_ban(self, ip): try: try: int(ip) except ValueError: ipaddress.ip_address(ip) ip = self.server.get_ipid(ip) except ValueError: raise ServerError('Argument must be an IP address or IPID.') if ip in self.bans: self.bans.remove(ip) else: raise ServerError('User is already not banned.') self.write_banlist()
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)
def add_ban(self, ip): try: try: int(ip) except ValueError: ipaddress.ip_address(ip) ip = self.server.get_ipid(ip) except ValueError: raise ServerError( 'Argument must be an IP address or 10-digit number.') if ip not in self.bans: self.bans.append(ip) else: raise ServerError('User is already banned.') self.write_banlist()
def remove_ban(self, ip): try: try: int(ip) except ValueError: ipaddress.ip_address(ip) ip = self.server.get_ipid(ip) except ValueError: raise ServerError( 'Argument must be an IP address or 10-digit number.') if ip in self.bans: self.bans.remove(ip) else: raise ServerError('This IPID is not banned.') self.write_banlist()
def get_votelist(self, value): try: if [i for i in self.poll_list if i[0] == "{}".format(value)]: poll_selected = [i[1] for i in self.poll_list if i[0] == "{}".format(value)] output = ('{} \'{}\''.format("".join(poll_selected), value)) stream = open('storage/poll/{}.yaml'.format(output), 'r') stream2 = yaml.load(stream) log = stream2['log'] return log else: return None except FileNotFoundError: raise ServerError('The specified poll has no file associated with it.') except IndexError: raise ServerError('The poll list is currently empty.')
def remove_ban(self, ipid): ipid = str(ipid) if self.is_banned(ipid): del self.bans[ipid] else: raise ServerError('This IPID is not banned.') self.write_banlist()
def remove_poll_choice(self, client, value, remove): try: if [i for i in self.poll_list if i[0] == "{}".format(value)]: poll_selected = [ i[1] for i in self.poll_list if i[0] == "{}".format(value) ] output = ('{} \'{}\''.format("".join(poll_selected), value)) stream = open('storage/poll/{}.yaml'.format(output), 'r') stream2 = yaml.load(stream) choices = stream2['choices'] if not remove in choices: client.send_host_message('Item is not a choice.') return stream2['choices'] = [x for x in choices if not x == remove] stream2['votes'].pop(remove.lower()) with open('storage/poll/{}.yaml'.format(output), 'w') as votelist_file: yaml.dump(stream2, votelist_file, default_flow_style=False) return stream2['choices'] else: return None except FileNotFoundError: raise ServerError( 'The specified poll has no file associated with it.') except IndexError: return
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 add_poll_choice(self, client, value, add): try: if [i for i in self.poll_list if i[0] == "{}".format(value)]: poll_selected = [ i[1] for i in self.poll_list if i[0] == "{}".format(value) ] output = ('{} \'{}\''.format("".join(poll_selected), value)) stream = open('storage/poll/{}.yaml'.format(output), 'r') stream2 = yaml.load(stream) if add.lower() in [x.lower() for x in stream2['choices']]: client.send_host_message('Item already a choice.') return stream2['choices'].append(str(add)) stream2['votes'][add.lower()] = 0 with open('storage/poll/{}.yaml'.format(output), 'w') as votelist_file: yaml.dump(stream2, votelist_file, default_flow_style=False) return stream2['choices'] else: return None except FileNotFoundError: raise ServerError( 'The specified poll has no file associated with it.') except IndexError: return
def add_ban(self, ipid, reason): ipid = str(ipid) if not self.is_banned(ipid): self.bans[ipid] = {'Reason': reason} else: raise ServerError('This IPID is already banned.') self.write_banlist()
def ooc_cmd_playrandom(client, arg): """ Plays a random track. Usage: /playrandom """ if len(arg) > 0: raise ArgumentError('This command takes no arguments.') index = 0 for item in client.server.music_list: for song in item['songs']: index += 1 if index == 0: raise ServerError( 'No music found.') else: music_set = set(range(index)) trackid = random.choice(tuple(music_set)) index = 1 for item in client.server.music_list: for song in item['songs']: if index == trackid: client.area.play_music(song['name'], client.char_id, song['length']) client.area.add_music_playing(client, song['name']) database.log_room('play', client, client.area, message=song['name']) return else: index += 1
def get_song_data(self, music, area): """ Get information about a track, if exists. :param music: track name :returns: tuple (name, length or -1) :raises: ServerError if track not found """ for item in self.music_list: if item['category'] == music: return '~stop.mp3', 0, -1, False for song in item['songs']: if song['name'] == music: try: return song['name'], song['length'], song['mod'] except KeyError: try: return song['name'], song['length'], -1, False except KeyError: return song['name'], 0, -1, False if len(area.cmusic_list) != 0: for item in area.cmusic_list: if item['category'] == music: return '~stop.mp3', 0, -1, True if len(item['songs']) != 0: for song in item['songs']: if song['name'] == music: try: return song['name'], song['length'], song[ 'mod'], True except KeyError: return song['name'], song['length'], -1, True raise ServerError('Music not found.')
def add_ban(self, ip): try: x = len(ip) except AttributeError: raise ServerError('Argument must be an 12-digit number.') if x == 12: self.bans[ip] = True self.write_banlist()
def rolla_reload(area): try: import yaml with open('config/dice.yaml', 'r') as dice: area.ability_dice = yaml.safe_load(dice) except: raise ServerError( 'There was an error parsing the ability dice configuration. Check your syntax.' )
def remove_poll(self, value): if ([i for i in self.poll_list if i[0] == "{}".format(value)]): self.poll_list = [i for i in self.poll_list if i[0] != "{}".format(value)] logger.log_serverpoll('Poll \'{}\' removed.'.format(value)) elif value == "all": self.poll_list = [] logger.log_serverpoll('All polls removed.') else: logger.log_serverpoll('Poll removal failed. Reason: The specified poll does not exist.') raise ServerError('The specified poll does not exist.') self.write_poll_list()
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): 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 yaml_load(file): try: return yaml.safe_load(file) except yaml.YAMLError as exc: # Extract the name of the yaml separator = max(file.name.rfind('\\'), file.name.rfind('/')) file_name = file.name[separator+1:] # Then raise the exception msg = ('File {} returned the following YAML error when loading: `{}`.' .format(file_name, exc)) raise ServerError.YAMLInvalidError(msg)
def validate_contents(self, contents, extra_parameters=None) -> List[str]: # Check characters contents is indeed a list of strings if not isinstance(contents, list): msg = (f'Expected the characters list to be a list, got a ' f'{type(contents).__name__}: {contents}.') raise ServerError.FileSyntaxError(msg) for (i, character) in enumerate(contents.copy()): if character is None: msg = (f'Expected all character names to be defined, but character {i} was not.') raise ServerError.FileSyntaxError(msg) if not isinstance(character, (str, float, int, bool, complex)): msg = (f'Expected all character names to be strings or numbers, but character ' f'{i}: {character} was not a string or number.') raise ServerError.FileSyntaxError(msg) # Otherwise, character i is valid. Cast it as string to deal with YAML doing # potential casting of its own contents[i] = str(character) return contents
def remove_ban(self, client, ip): try: try: int(ip) except ValueError: ipaddress.ip_address(ip) ip = client.server.get_ipid(ip) except ValueError: if not len(ip) == 12: raise ServerError( 'Argument must be an IP address or 10-digit number.') del self.bans[ip] self.write_banlist()
def ooc_cmd_judgelog(client, arg): if not client.is_mod: raise ClientError('You must be authorized to do that.') if len(arg) != 0: raise ArgumentError('This command does not take any arguments.') jlog = client.area.judgelog if len(jlog) > 0: jlog_msg = '== Judge Log ==' for x in jlog: jlog_msg += '\r\n{}'.format(x) client.send_host_message(jlog_msg) else: raise ServerError( 'There have been no judge actions in this area since start of session.' )
def reload(self): # Keep backups in case of failure backup = [ self.char_list.copy(), self.music_list.copy(), self.backgrounds.copy() ] # Do a dummy YAML load to see if the files can be loaded and parsed at all first. reloaded_assets = [ self.load_characters, self.load_backgrounds, self.load_music ] for reloaded_asset in reloaded_assets: try: reloaded_asset() except ServerError.YAMLInvalidError as exc: # The YAML exception already provides a full description. Just add the fact the # reload was undone to ease the person who ran the command's nerves. msg = (f'{exc} Reload was undone.') raise ServerError.YAMLInvalidError(msg) except ServerError.FileSyntaxError as exc: msg = f'{exc} Reload was undone.' raise ServerError(msg) # Only on success reload self.load_characters() self.load_backgrounds() try: self.load_music() except ServerError as exc: self.char_list, self.music_list, self.backgrounds = backup msg = ( 'The new music list returned the following error when loading: `{}`. Fix the ' 'error and try again. Reload was undone.'.format(exc)) raise ServerError.FileSyntaxError(msg)
def validate_contents(self, contents, extra_parameters=None) -> List[str]: # Check background contents is indeed a list of strings if not isinstance(contents, list): msg = (f'Expected the background list to be a list, got a ' f'{type(contents).__name__}: {contents}.') raise ServerError.FileSyntaxError(msg) for (i, background) in enumerate(contents.copy()): if background is None: msg = (f'Expected all background names to be defined, ' f'found background {i} was not.') raise ServerError.FileSyntaxError(msg) if not isinstance(background, (str, float, int, bool, complex)): msg = ( f'Expected all background names to be strings or numbers, ' f'found background {i}: {background} was not a string or number.' ) raise ServerError.FileSyntaxError(msg) # Otherwise, background i is valid. Cast it as string to deal with YAML doing # potential casting of its own contents[i] = str(background) return contents
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