import urllib import platform import django from django.conf import settings from twisted.internet import defer from twisted.internet import protocol from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks from twisted.web.client import Agent, _HTTP11ClientFactory, HTTPConnectionPool from twisted.web.http_headers import Headers from twisted.web.iweb import IBodyProducer from zope.interface import implements from evennia.players.models import PlayerDB from evennia.server.sessionhandler import SESSIONS from evennia.utils import get_evennia_version, logger class EvenniaGameDirClient(object): """ This client class is used for gathering and sending game details to the Evennia Game Directory. Since EGD is in the early goings, this isn't incredibly configurable as far as what is being sent. """ def __init__(self, on_bad_request=None): """ :param on_bad_request: Optional callable to trigger when a bad request was sent. This is almost always going to be due to bad config. """ self.report_host = 'http://evennia-game-directory.appspot.com' self.report_path = '/api/v1/game/check_in' self.report_url = self.report_host + self.report_path self.logged_first_connect = False self._on_bad_request = on_bad_request # Oh, the humanity. Silence the factory start/stop messages. self._conn_pool = HTTPConnectionPool(reactor) self._conn_pool._factory = QuietHTTP11ClientFactory @inlineCallbacks def send_game_details(self): """ This is where the magic happens. Send details about the game to the Evennia Game Directory. """ status_code, response_body = yield self._form_and_send_request() if status_code == 200: if not self.logged_first_connect: logger.log_infomsg( "Successfully sent game details to Evennia Game Directory.") self.logged_first_connect = True return # At this point, either EGD is having issues or the payload we sent # is improperly formed (probably due to mis-configuration). logger.log_errmsg( 'Failed to send game details to Evennia Game Directory. HTTP ' 'status code was %s. Message was: %s' % (status_code, response_body) ) if status_code == 400 and self._on_bad_request: # Improperly formed request. Defer to the callback as far as what # to do. Probably not a great idea to continue attempting to send # to EGD, though. self._on_bad_request() def _form_and_send_request(self): agent = Agent(reactor, pool=self._conn_pool) headers = { 'User-Agent': ['Evennia Game Directory Client'], 'Content-Type': ['application/x-www-form-urlencoded'], } gd_config = settings.GAME_DIRECTORY_LISTING # We are using `or` statements below with dict.get() to avoid sending # stringified 'None' values to the server. values = { # Game listing stuff 'game_name': settings.SERVERNAME, 'game_status': gd_config['game_status'], 'game_website': gd_config.get('game_website') or '', 'listing_contact': gd_config['listing_contact'], # How to play 'telnet_hostname': gd_config.get('telnet_hostname') or '', 'telnet_port': gd_config.get('telnet_port') or '', 'web_client_url': gd_config.get('web_client_url') or '', # Game stats 'connected_player_count': SESSIONS.player_count(), 'total_player_count': PlayerDB.objects.num_total_players() or 0, # System info 'evennia_version': get_evennia_version(), 'python_version': platform.python_version(), 'django_version': django.get_version(), 'server_platform': platform.platform(), } data = urllib.urlencode(values) d = agent.request( 'POST', self.report_url, headers=Headers(headers), bodyProducer=StringProducer(data)) d.addCallback(self.handle_egd_response) return d def handle_egd_response(self, response): if 200 <= response.code < 300: d = defer.succeed((response.code, 'OK')) else: # Go through the horrifying process of getting the response body # out of Twisted's plumbing. d = defer.Deferred() response.deliverBody(SimpleResponseReceiver(response.code, d)) return d class SimpleResponseReceiver(protocol.Protocol): """ Used for pulling the response body out of an HTTP response. """ def __init__(self, status_code, d): self.status_code = status_code self.buf = '' self.d = d def dataReceived(self, data): self.buf += data def connectionLost(self, reason=protocol.connectionDone): self.d.callback((self.status_code, self.buf)) class StringProducer(object): """ Used for feeding a request body to the tx HTTP client. """ implements(IBodyProducer) def __init__(self, body): self.body = body self.length = len(body) def startProducing(self, consumer): consumer.write(self.body) return defer.succeed(None) def pauseProducing(self): pass def stopProducing(self): pass class QuietHTTP11ClientFactory(_HTTP11ClientFactory): """ Silences the obnoxious factory start/stop messages in the default client. """ noisy = False