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'])
Пример #2
0
 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'])
Пример #3
0
 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'])
Пример #4
0
    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()
Пример #5
0
    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'])
Пример #6
0
    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'])
Пример #8
0
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()
Пример #9
0
    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
Пример #10
0
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()
Пример #11
0
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)
Пример #12
0
    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
Пример #13
0
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()
Пример #14
0
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
Пример #15
0
 def setUpClass(cls):
     cls.cm = ClientManager()
     cls.loop = asyncio.get_event_loop()