Esempio n. 1
0
    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()
Esempio n. 2
0
    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
Esempio n. 3
0
        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)
Esempio n. 4
0
    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