def net_cmd_ms(self, args: List[str]): """ IC message. Refer to the implementation for details. """ pargs = self.process_arguments('MS', args) if self.client.is_muted: # Checks to see if the client has been muted by a mod self.client.send_ooc("You have been muted by a moderator.") return if (self.client.area.ic_lock and not self.client.is_staff() and not self.client.can_bypass_iclock): self.client.send_ooc('The IC chat in this area is currently locked.') return if not self.client.area.can_send_message(): return # Trim out any leading/trailing whitespace characters up to a chain of spaces pargs['text'] = Constants.trim_extra_whitespace(pargs['text']) # Check if after all of this, the message is empty. If so, ignore if not pargs['text']: return # First, check if the player just sent the same message with the same character and did # not receive any other messages in the meantime. # This helps prevent record these messages and retransmit it to clients who may want to # filter these out if (pargs['text'] == self.client.last_ic_raw_message and self.client.last_received_ic[0] == self.client and self.client.get_char_name() == self.client.last_ic_char): return if not self.client.area.iniswap_allowed: if self.client.area.is_iniswap(self.client, pargs['pre'], pargs['anim'], pargs['folder']): self.client.send_ooc("Iniswap is blocked in this area.") return if pargs['folder'] in self.client.area.restricted_chars and not self.client.is_staff(): self.client.send_ooc('Your character is restricted in this area.') return if pargs['msg_type'] not in ('chat', '0', '1'): return if pargs['anim_type'] not in (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10): return if pargs['char_id'] != self.client.char_id: return if Constants.includes_relative_directories(pargs['sfx']): self.client.send_ooc(f'Sound effects and voicelines may not not reference parent or ' f'current directories: {pargs["sfx"]}') return if pargs['sfx_delay'] < 0: return if pargs['button'] not in (0, 1, 2, 3, 4, 5, 6, 7, 8): # Shouts return if pargs['button'] > 0 and not self.client.area.bullet and not self.client.is_staff(): self.client.send_ooc('Bullets are disabled in this area.') return if pargs['evidence'] < 0: return if pargs['ding'] not in (0, 1, 2, 3, 4, 5, 6, 7): # Effects return if pargs['color'] not in (0, 1, 2, 3, 4, 5, 6, 7, 8): return if pargs['color'] == 5 and not self.client.is_officer(): pargs['color'] = 0 if self.client.pos: pargs['pos'] = self.client.pos else: if pargs['pos'] not in ('def', 'pro', 'hld', 'hlp', 'jud', 'wit'): return # Make sure the areas are ok with this try: self.client.area.publisher.publish('area_client_inbound_ms_check', { 'client': self.client, 'contents': pargs, }) except TsuserverException as ex: self.client.send_ooc(ex) return # Make sure the clients are ok with this try: self.client.publisher.publish('client_inbound_ms_check', { 'contents': pargs, }) except TsuserverException as ex: self.client.send_ooc(ex) return # At this point, the message is guaranteed to be sent self.client.publish_inbound_command('MS', pargs) self.client.send_command_dict('ackMS', dict()) self.client.pos = pargs['pos'] # First, update last raw message sent *before* any transformations. That is so that the # server can accurately ignore client sending the same message over and over again self.client.last_ic_raw_message = pargs['text'] self.client.last_ic_char = self.client.get_char_name() # Truncate and alter message if message effect is in place raw_msg = pargs['text'][:256] msg = raw_msg if self.client.gimp: # If you are gimped, gimp message. msg = random.choice(self.server.gimp_list) if self.client.disemvowel: # If you are disemvoweled, replace string. msg = Constants.disemvowel_message(msg) if self.client.disemconsonant: # If you are disemconsonanted, replace string. msg = Constants.disemconsonant_message(msg) if self.client.remove_h: # If h is removed, replace string. msg = Constants.remove_h_message(msg) gag_replaced = False if self.client.is_gagged: allowed_starters = ('(', '*', '[') if msg != ' ' and not msg.startswith(allowed_starters): gag_replaced = True msg = Constants.gagged_message() if msg != raw_msg: self.client.send_ooc_others('(X) {} [{}] tried to say `{}` but is currently gagged.' .format(self.client.displayname, self.client.id, raw_msg), is_zstaff_flex=True, in_area=True) # Censor passwords if login command accidentally typed in IC for password in self.server.all_passwords: for login in ['login ', 'logincm ', 'loginrp ', 'logingm ']: if login + password in msg: msg = msg.replace(password, '[CENSORED]') if pargs['evidence'] and pargs['evidence'] in self.client.evi_list: evidence_position = self.client.evi_list[pargs['evidence']] - 1 if self.client.area.evi_list.evidences[evidence_position].pos != 'all': self.client.area.evi_list.evidences[evidence_position].pos = 'all' self.client.area.broadcast_evidence_list() pargs['evidence'] = self.client.evi_list[pargs['evidence']] else: pargs['evidence'] = 0 # If client has GlobalIC enabled, set area range target to intended range and remove # GlobalIC prefix if needed. if self.client.multi_ic is None or not msg.startswith(self.client.multi_ic_pre): area_range = range(self.client.area.id, self.client.area.id + 1) else: # As msg.startswith('') is True, this also accounts for having no required prefix. start, end = self.client.multi_ic[0].id, self.client.multi_ic[1].id + 1 start_area = self.server.area_manager.get_area_by_id(start) end_area = self.server.area_manager.get_area_by_id(end-1) area_range = range(start, end) truncated_msg = msg.replace(self.client.multi_ic_pre, '', 1) if start != end-1: self.client.send_ooc('Sent global IC message "{}" to areas {} through {}.' .format(truncated_msg, start_area.name, end_area.name)) else: self.client.send_ooc('Sent global IC message "{}" to area {}.' .format(truncated_msg, start_area.name)) pargs['msg'] = msg pargs['showname'] = '' # Dummy value, actual showname is computed later # Compute pairs # Based on tsuserver3.3 code # Only do this if character is paired, which would only happen for AO 2.6+ clients # Handle AO 2.8 logic # AO 2.8 sends their charid_pair in slightly longer format (\d+\^\d+) # The first bit corresponds to the proper charid_pair, the latter one to whether # the character should appear in front or behind the pair. We still want to extract # charid_pair so pre-AO 2.8 still see the pair; but make it so that AO 2.6 can send pair # messages. Thus, we 'invent' the missing arguments based on available info. if 'charid_pair_pair_order' in pargs: # AO 2.8 sender pargs['charid_pair'] = int(pargs['charid_pair_pair_order'].split('^')[0]) elif 'charid_pair' in pargs: # AO 2.6 sender pargs['charid_pair_pair_order'] = f'{pargs["charid_pair"]}^0' else: # E.g. DRO pargs['charid_pair'] = -1 pargs['charid_pair_pair_order'] = -1 self.client.charid_pair = pargs['charid_pair'] if 'charid_pair' in pargs else -1 self.client.offset_pair = pargs['offset_pair'] if 'offset_pair' in pargs else 0 self.client.flip = pargs['flip'] if not self.client.char_folder: self.client.char_folder = pargs['folder'] if pargs['anim_type'] not in (5, 6): self.client.last_sprite = pargs['anim'] pargs['other_offset'] = 0 pargs['other_emote'] = 0 pargs['other_flip'] = 0 pargs['other_folder'] = '' if 'charid_pair' not in pargs or pargs['charid_pair'] < -1: pargs['charid_pair'] = -1 pargs['charid_pair_pair_order'] = -1 if pargs['charid_pair'] > -1: for target in self.client.area.clients: if target == self.client: continue # Check pair has accepted pair if target.char_id != self.client.charid_pair: continue if target.charid_pair != self.client.char_id: continue # Check pair is in same position if target.pos != self.client.pos: continue pargs['other_offset'] = target.offset_pair pargs['other_emote'] = target.last_sprite pargs['other_flip'] = target.flip pargs['other_folder'] = target.char_folder break else: # There are no clients who want to pair with this client pargs['charid_pair'] = -1 pargs['offset_pair'] = 0 pargs['charid_pair_pair_order'] = -1 self.client.publish_inbound_command('MS_final', pargs) for area_id in area_range: target_area = self.server.area_manager.get_area_by_id(area_id) for c in target_area.clients: c.send_ic(params=pargs, sender=self.client, gag_replaced=gag_replaced) target_area.set_next_msg_delay(len(msg)) # Deal with shoutlog if pargs['button'] > 0: info = 'used shout {} with the message: {}'.format(pargs['button'], msg) target_area.add_to_shoutlog(self.client, info) self.client.area.set_next_msg_delay(len(msg)) logger.log_server('[IC][{}][{}]{}' .format(self.client.area.id, self.client.get_char_name(), msg), self.client) # Sending IC messages reveals sneaked players if not self.client.is_staff() and not self.client.is_visible: self.client.change_visibility(True) self.client.send_ooc_others('(X) {} [{}] revealed themselves by talking ({}).' .format(self.client.displayname, self.client.id, self.client.area.id), is_zstaff=True) # Restart AFK kick timer and lurk callout timers, if needed self.server.tasker.create_task(self.client, ['as_afk_kick', self.client.area.afk_delay, self.client.area.afk_sendto]) self.client.check_lurk() if self.client.area.is_recording: self.client.area.recorded_messages.append(args) self.client.last_ic_message = msg self.client.last_active = Constants.get_time()
def validate_contents(self, contents, extra_parameters=None): if extra_parameters is None: extra_parameters = dict() server_character_list = extra_parameters.get('server_character_list', None) server_default_area_description = extra_parameters.get( 'server_default_area_description', '') default_area_parameters = { 'afk_delay': 0, 'afk_sendto': 0, 'background_tod': dict(), 'bglock': False, 'bullet': True, 'cbg_allowed': False, 'change_reachability_allowed': True, 'default_description': server_default_area_description, 'evidence_mod': 'FFA', 'gm_iclock_allowed': True, 'has_lights': True, 'iniswap_allowed': False, 'global_allowed': True, 'lobby_area': False, 'locking_allowed': False, 'private_area': False, 'reachable_areas': '<ALL>', 'restricted_chars': '', 'rollp_allowed': True, 'rp_getarea_allowed': True, 'rp_getareas_allowed': True, 'scream_range': '', 'song_switch_allowed': False, } current_area_id = 0 area_parameters = list() temp_area_names = set() found_uncheckable_restricted_chars = False # Create the areas for item in contents: # Check required parameters if 'area' not in item: info = 'Area {} has no name.'.format(current_area_id) raise AreaError(info) if 'background' not in item: info = 'Area {} has no background.'.format(item['area']) raise AreaError(info) # Prevent reserved area names (it has a special meaning) reserved_names = { '<ALL>', '<REACHABLE_AREAS>', } for name in reserved_names: if item['area'] == name: info = ( 'An area in your area list is called "{name}". This is a reserved ' 'name, so it is not a valid area name. Please change its name and try ' 'again.') raise AreaError(info) # Prevent names that may be interpreted as a directory with . or .. # This prevents sending the client an entry to their music list which may be read as # including a relative directory if Constants.includes_relative_directories(item['area']): info = ( f'Area {item["area"]} could be interpreted as referencing the current or ' f'parent directories, so it is invalid. Please rename the area and try ' f'again.') raise AreaError(info) # Check unset optional parameters for param in default_area_parameters: if param not in item: item[param] = default_area_parameters[param] # Check use of backwards incompatible parameters if 'sound_proof' in item: info = ( 'The sound_proof property was defined for area {}. ' 'Support for sound_proof was removed in favor of scream_range. ' 'Please replace the sound_proof tag with scream_range in ' 'your area list and try again.'.format(item['area'])) raise AreaError(info) # Avoid having areas with the same name if item['area'] in temp_area_names: info = ( 'Two areas have the same name in area list: {}. ' 'Please rename the duplicated areas and try again.'.format( item['area'])) raise AreaError(info) # Check if any of the items were interpreted as Python Nones (maybe due to empty lines) for parameter in item: if item[parameter] is None: info = ( 'Parameter {} is manually undefined for area {}. This can be the case ' 'due to having an empty parameter line in your area list. ' 'Please fix or remove the parameter from the area definition and try ' 'again.'.format(parameter, item['area'])) raise AreaError(info) # Check and fix background tods if needed, as YAML parses this as a list of if item['background_tod'] != dict(): raw_background_tod_map = item['background_tod'] if not isinstance(raw_background_tod_map, dict): info = ( f'Expected background TOD for area {item["area"]} be ' f'one dictionary, found it was of type ' f'{type(raw_background_tod_map).__name__}: {raw_background_tod_map}' ) raise AreaError(info) new_background_tod = dict() if not isinstance(raw_background_tod_map, dict): info = ( f'Expected background TOD for area {item["area"]} be a dictionary, ' f'found it was of type {type(raw_background_tod_map).__name__}: ' f'{raw_background_tod_map}') raise AreaError(info) for (key, value) in raw_background_tod_map.items(): tod_name = str(key) tod_background = str(value) if not tod_name.strip(): info = ( f'TOD name `{tod_name}` invalid for area {item["area"]}. ' f'Make sure the TOD name has non-space characters and try ' f'again.') raise AreaError(info) if ' ' in tod_name: info = ( f'TOD name `{tod_name}` invalid for area {item["area"]}. ' f'Make sure the TOD name has no space characters and try ' f'again.') raise AreaError(info) if '|' in tod_name: info = ( f'TOD name `{tod_name}` contains invalid character |.' f'Make sure the TOD name does not have that character and ' f'try again.') raise AreaError(info) if '|' in tod_background: info = ( f'TOD background `{tod_background}` contains invalid ' f'character |. Make sure the TOD name does not have that ' f'character and try again.') raise AreaError(tod_background) new_background_tod[tod_name] = tod_background item['background_tod'] = new_background_tod area_parameters.append(item.copy()) temp_area_names.add(item['area']) current_area_id += 1 # Check if a reachable area is not an area name # Can only be done once all areas are created for area_item in area_parameters: name = area_item['area'] reachable_areas = Constants.fix_and_setify( area_item['reachable_areas']) scream_range = Constants.fix_and_setify(area_item['scream_range']) restricted_chars = Constants.fix_and_setify( area_item['restricted_chars']) if reachable_areas == {'<ALL>'}: reachable_areas = temp_area_names.copy() if scream_range == {'<ALL>'}: scream_range = temp_area_names.copy() elif scream_range == {'<REACHABLE_AREAS>'}: scream_range = reachable_areas.copy() area_item['reachable_areas'] = reachable_areas area_item['scream_range'] = scream_range area_item['restricted_chars'] = restricted_chars # Make sure no weird areas were set as reachable by players or by screams unrecognized_areas = reachable_areas - temp_area_names if unrecognized_areas: info = ( f'Area {name} has unrecognized areas {unrecognized_areas} defined as ' f'areas a player can reach to. Please rename the affected areas and try ' f'again.') raise AreaError(info) unrecognized_areas = scream_range - temp_area_names if unrecognized_areas: info = ( f'Area {name} has unrecognized areas {unrecognized_areas} defined as ' f'areas screams can reach to. Please rename the affected areas and try ' f'again.') raise AreaError(info) # Make sure only characters that exist are part of the restricted char set if server_character_list is not None: unrecognized_characters = restricted_chars - set( server_character_list) if unrecognized_characters: info = ( f'Area {name} has unrecognized characters {unrecognized_characters} ' f'defined as restricted characters. Please make sure the characters ' f'exist and try again.') raise AreaError(info) elif restricted_chars: found_uncheckable_restricted_chars = True if found_uncheckable_restricted_chars: info = ( 'WARNING: Some areas provided default restricted characters. However, no ' 'server character list was provided, so no checks whether restricted ' 'characters were in the character list of the server were performed.' ) print(info) return area_parameters
def play_track(self, name: str, client: ClientManager.Client, raise_if_not_found: bool = False, reveal_sneaked: bool = False, pargs: Dict[str, Any] = None): """ Wrapper function to play a music track in an area. Parameters ---------- name : str Name of the track to play client : ClientManager.Client Client who initiated the track change request. effect : int, optional Accompanying effect to the track (only used by AO 2.8.4+). Defaults to 0. raise_if_not_found : bool, optional If True, it will raise ServerError if the track name is not in the server's music list nor the client's music list. If False, it will not care about it. Defaults to False. reveal_sneaked : bool, optional If True, it will change the visibility status of the sender client to True (reveal them). If False, it will keep their visibility as it was. Defaults to False. pargs : dict of str to Any If given, they are arguments to an MC packet that was given when the track was requested, and will override any other arguments given. If not, this is ignored. Defaults to None (and converted to an empty dictionary). Raises ------ ServerError.FileInvalidNameError: If `name` references parent or current directories (e.g. "../hi.mp3") ServerError.MusicNotFoundError: If `name` is not a music track in the server or client's music list and `raise_if_not_found` is True. ServerError (with code 'FileInvalidName') If `name` references parent or current directories (e.g. "../hi.mp3") """ if not pargs: pargs = dict() if Constants.includes_relative_directories(name): info = f'Music names may not reference parent or current directories: {name}' raise ServerError.FileInvalidNameError(info) try: name, length, source = self.server.get_song_data(name, c=client) except ServerError.MusicNotFoundError: if raise_if_not_found: raise name, length, source = name, -1, '' if 'name' not in pargs: pargs['name'] = name if 'char_id' not in pargs: pargs['char_id'] = client.char_id pargs['showname'] = client.showname # Ignore AO shownames if 'loop' not in pargs: pargs['loop'] = -1 if 'channel' not in pargs: pargs['channel'] = 0 if 'effects' not in pargs: pargs['effects'] = 0 def loop(char_id): for client in self.clients: loop_pargs = pargs.copy() # Overwrite in case char_id changed (e.g., server looping) loop_pargs['char_id'] = char_id client.send_music(**loop_pargs) if self.music_looper: self.music_looper.cancel() if length > 0: f = lambda: loop(-1) # Server should loop now self.music_looper = asyncio.get_event_loop().call_later( length, f) loop(pargs['char_id']) # Record the character name and the track they played. self.current_music_player = client.displayname self.current_music = name self.current_music_source = source logger.log_server( '[{}][{}]Changed music to {}.'.format(self.id, client.get_char_name(), name), client) # Changing music reveals sneaked players, so do that if requested if not client.is_staff( ) and not client.is_visible and reveal_sneaked: client.change_visibility(True) client.send_ooc_others( '(X) {} [{}] revealed themselves by playing music ({}).'. format(client.displayname, client.id, client.area.id), is_zstaff=True)
def validate_contents(self, contents, extra_parameters=None) -> List[Dict[str, Any]]: # Check music list contents is indeed a list if not isinstance(contents, list): msg = (f'Expected the music list to be a list, got a ' f'{type(contents).__name__}: {contents}.') raise ServerError.FileSyntaxError(msg) # Check top level description is ok for (i, item) in enumerate(contents.copy()): if item is None: msg = ( f'Expected all music list items to be defined, but item {i} was not.' ) raise ServerError.FileSyntaxError(msg) if not isinstance(item, dict): msg = ( f'Expected all music list items to be dictionaries, but item ' f'{i}: {item} was not a dictionary.') raise ServerError.FileSyntaxError(msg) if set(item.keys()) != {'category', 'songs'}: msg = ( f'Expected all music list items to have exactly two keys: category and ' f'songs, but item {i} had keys {set(item.keys())}') raise ServerError.FileSyntaxError(msg) category, songs = item['category'], item['songs'] if category is None: msg = ( f'Expected all music list categories to be defined, but category {i} was ' f'not.') raise ServerError.FileSyntaxError(msg) if songs is None: msg = ( f'Expected all music list song descriptions to be defined, but song ' f'description {i} was not.') raise ServerError.FileSyntaxError(msg) if not isinstance(category, (str, float, int, bool, complex)): msg = ( f'Expected all music list category names to be strings or numbers, but ' f'category {i}: {category} was not a string or number.') raise ServerError.FileSyntaxError(msg) if not isinstance(songs, list): msg = ( f'Expected all music list song descriptions to be a list, but ' f'description {i}: {songs} was not a list.') raise ServerError.FileSyntaxError(msg) # Check each song description dictionary is ok for (i, item) in enumerate(contents.copy()): category = item['category'] songs = item['songs'] for (j, song) in enumerate(songs): if song is None: msg = ( f'Expected all music list song descriptions to be defined, but song ' f'description {j} in category {i}: {category} was not defined.' ) raise ServerError.FileSyntaxError(msg) if not isinstance(song, dict): msg = ( f'Expected all music list song descriptions to be dictionaries: but ' f'song description {j} in category {i}: {category} was not a ' f'dictionary: {song}.') raise ServerError.FileSyntaxError(msg) if 'name' not in song.keys(): msg = ( f'Expected all music lists song descriptions to have a name as key, but ' f'song description {j} in category {i}: {category} ' f'had keys {set(song.keys())}.') raise ServerError.FileSyntaxError(msg) if not set(song.keys()).issubset({'name', 'length', 'source'}): msg = ( f'Expected all music list song description keys be contained in the set ' f"{{'name', 'length', 'source'}} but song description {j} in category " f'{i}: {category} had keys {set(song.keys())}.') raise ServerError.FileSyntaxError(msg) name = song['name'] length = song['length'] if 'length' in song else -1 source = song['source'] if 'source' in song else '' if not isinstance(name, (str, float, int, bool, complex)): msg = ( f'Expected all music list song names to be strings or numbers, but ' f'song {j}: {name} in category {i}: {category} was not a string or ' f'number.') raise ServerError.FileSyntaxError(msg) if not isinstance(length, (int, float)): msg = ( f'Expected all music list song lengths to be numbers, but song {j}: ' f'{name} in category {i}: {category} had non-numerical length {length}.' ) raise ServerError.FileSyntaxError(msg) if not isinstance(source, (str, float, int, bool, complex)): msg = ( f'Expected all music list song sources to be strings or numbers, but ' f'song {j}: {name} in category {i}: {category} was not a string or ' f'number.') # Prevent names that may be interpreted as a directory with . or .. # This prevents sending the client an entry to their music list which may be read as # including a relative directory if Constants.includes_relative_directories(name): info = ( f'Music {name} could be interpreted as referencing current or ' f'parent directories, so it is invalid.') raise ServerError.FileSyntaxError(info) return contents