From 0b3314fa20cee874b1b06501e52f97abad076ad3 Mon Sep 17 00:00:00 2001 From: Greg Taylor Date: Sun, 3 Apr 2016 19:05:37 -0700 Subject: [PATCH] Introducing the Evennia Game Directory service contrib. --- evennia/contrib/gamedir_client/README.md | 139 ++++++++++++++++++++ evennia/contrib/gamedir_client/__init__.py | 1 + evennia/contrib/gamedir_client/client.py | 141 +++++++++++++++++++++ evennia/contrib/gamedir_client/service.py | 41 ++++++ 4 files changed, 322 insertions(+) create mode 100644 evennia/contrib/gamedir_client/README.md create mode 100644 evennia/contrib/gamedir_client/__init__.py create mode 100644 evennia/contrib/gamedir_client/client.py create mode 100644 evennia/contrib/gamedir_client/service.py diff --git a/evennia/contrib/gamedir_client/README.md b/evennia/contrib/gamedir_client/README.md new file mode 100644 index 000000000..e0187608d --- /dev/null +++ b/evennia/contrib/gamedir_client/README.md @@ -0,0 +1,139 @@ +# Evennia Game Directory Client + +Greg Taylor 2016 + +This contrib features a client for the [Evennia Game Directory] +(http://evennia-game-directory.appspot.com/), a listing of games built on +Evennia. By listing your game on the directory, you make it easy for other +people in the community to discover your creation. + +*Note: Since this is still an early experiment, there is no notion of +ownership for a game listing. As a consequence, we rely on the good behavior +of our users in the early goings. If the directory is a success, we'll work +on remedying this.* + +## Listing your Game + +To list your game, you'll need to enable the Evennia Game Directory client. +Start by `cd`'ing to your game directory. From there, open up +`server/conf/server_services_plugins.py`. It might look something like this +if you don't have any other optional add-ons enabled: + + """ + Server plugin services + + This plugin module can define user-created services for the Server to + start. + + This module must handle all imports and setups required to start a + twisted service (see examples in evennia.server.server). It must also + contain a function start_plugin_services(application). Evennia will + call this function with the main Server application (so your services + can be added to it). The function should not return anything. Plugin + services are started last in the Server startup process. + """ + + + def start_plugin_services(server): + """ + This hook is called by Evennia, last in the Server startup process. + + server - a reference to the main server application. + """ + pass + + +To enable the client, import `EvenniaGameDirService` and fire it up after the +Evennia server has finished starting: + + """ + Server plugin services + + This plugin module can define user-created services for the Server to + start. + + This module must handle all imports and setups required to start a + twisted service (see examples in evennia.server.server). It must also + contain a function start_plugin_services(application). Evennia will + call this function with the main Server application (so your services + can be added to it). The function should not return anything. Plugin + services are started last in the Server startup process. + """ + from evennia.contrib.gamedir_client import EvenniaGameDirService + + + def start_plugin_services(server): + """ + This hook is called by Evennia, last in the Server startup process. + + server - a reference to the main server application. + """ + gamedir_service = EvenniaGameDirService() + server.services.addService(gamedir_service) + + +Next, configure your game listing by opening up `server/conf/settings.py` and + using the following as a starting point: + + ###################################################################### + # Contrib config + ###################################################################### + + GAMEDIR_CLIENT = { + 'game_status': 'pre-alpha', + 'listing_contact': 'me@my-game.com', + 'telnet_hostname': 'my-game.com', + 'telnet_port': 1234, + } + +The following section in this README.md will go over all possible values. + +At this point, you should be all set! Simply restart your game and check the +server logs for errors. Your listing and some game state will be sent every +half hour. + +## Possible GAMEDIR_CLIENT settings + +### game_status + +Required: **Yes** +Must be one of: 'pre-alpha', 'alpha', 'beta', 'launched' + +Describes the current state of your game. + +### game_website + +Required: No + +The URL to your game's website, if you have one. + +### listing_contact + +Required: **Yes** + +An email address for us to get in touch with in the event of a listing issue +or backwards-incompatible change. + +### telnet_hostname + +Required: **Yes** + +The hostname that players can telnet into to play your game. + +### telnet_port + +Required: **Yes** + +The port that the players can telnet into to play your game. + +## What information is being reported? + +In addition the the details listed in the previous section, we send some +simple usage stats that don't currently get displayed. These will help the +Evennia maintainers get a feel for some technical specifics for games out in +the wild. + +## Troubleshooting + +If you don't see your game appear on the listing, check your server logs. You +should see some error messages. diff --git a/evennia/contrib/gamedir_client/__init__.py b/evennia/contrib/gamedir_client/__init__.py new file mode 100644 index 000000000..9f53bd5d8 --- /dev/null +++ b/evennia/contrib/gamedir_client/__init__.py @@ -0,0 +1 @@ +from evennia.contrib.gamedir_client.service import EvenniaGameDirService diff --git a/evennia/contrib/gamedir_client/client.py b/evennia/contrib/gamedir_client/client.py new file mode 100644 index 000000000..194609c2e --- /dev/null +++ b/evennia/contrib/gamedir_client/client.py @@ -0,0 +1,141 @@ +import urllib + +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._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: + logger.log_infomsg( + "Successfully sent game details to Evennia Game Directory.") + 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.GAMEDIR_CLIENT + values = { + 'game_name': settings.SERVERNAME, + 'game_status': gd_config['game_status'], + 'game_website': gd_config.get('game_website'), + 'listing_contact': gd_config['listing_contact'], + 'evennia_version': get_evennia_version(), + 'telnet_hostname': gd_config['telnet_hostname'], + 'telnet_port': gd_config['telnet_port'], + 'connected_player_count': SESSIONS.player_count(), + 'total_player_count': PlayerDB.objects.num_total_players() or 0, + } + 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 diff --git a/evennia/contrib/gamedir_client/service.py b/evennia/contrib/gamedir_client/service.py new file mode 100644 index 000000000..31a0f9104 --- /dev/null +++ b/evennia/contrib/gamedir_client/service.py @@ -0,0 +1,41 @@ +from twisted.application.service import Service +from twisted.internet.task import LoopingCall + +from evennia.contrib.gamedir_client.client import EvenniaGameDirClient +from evennia.utils import logger + + +class EvenniaGameDirService(Service): + """ + Twisted Service that contains a LoopingCall for sending details on a + game to the Evennia Game Directory. + """ + name = 'GameDirectoryClient' + + def __init__(self): + self.client = EvenniaGameDirClient( + on_bad_request=self._die_on_bad_request) + self.loop = LoopingCall(self.client.send_game_details) + + def startService(self): + super(EvenniaGameDirService, self).startService() + # TODO: Check to make sure that the client is configured. + self.loop.start(10) + + def stopService(self): + if self.running == 0: + # @reload errors if we've stopped this service. + return + super(EvenniaGameDirService, self).stopService() + self.loop.stop() + + def _die_on_bad_request(self): + """ + If it becomes apparent that our configuration is generating improperly + formed messages to EGD, we don't want to keep sending bad messages. + Stop the service so we're not wasting resources. + """ + logger.log_infomsg( + "Shutting down Evennia Game Directory client service due to " + "invalid configuration.") + self.stopService()