Move game index client into server/

This commit is contained in:
Griatch 2019-06-24 23:16:53 +02:00
parent 050a665243
commit 0006b8d40f
9 changed files with 83 additions and 44 deletions

View file

@ -50,8 +50,6 @@ def check_errors(settings):
if hasattr(settings, "ACCOUNT_TYPECLASS_PATHS"):
raise DeprecationWarning(deprstring % "ACCOUNT_TYPECLASS_PATHS")
if hasattr(settings, "CHANNEL_TYPECLASS_PATHS"):
raise DeprecationWarning(deprstring % "CHANNEL_TYPECLASS_PATHS")
if hasattr(settings, "SEARCH_MULTIMATCH_SEPARATOR"):
raise DeprecationWarning(
"settings.SEARCH_MULTIMATCH_SEPARATOR was replaced by "
"SEARCH_MULTIMATCH_REGEX and SEARCH_MULTIMATCH_TEMPLATE. "
@ -66,10 +64,16 @@ def check_errors(settings):
"and manipulate these time units, the tools from utils.gametime "
"are now found in contrib/convert_gametime.py instead.")
if any(hasattr(settings, value) for value in ("TIME_SEC_PER_MIN", "TIME_MIN_PER_HOUR",
"TIME_HOUR_PER_DAY", "TIME_DAY_PER_WEEK", "TIME_WEEK_PER_MONTH",
"TIME_HOUR_PER_DAY", "TIME_DAY_PER_WEEK",
"TIME_WEEK_PER_MONTH",
"TIME_MONTH_PER_YEAR")):
raise DeprecationWarning(gametime_deprecation)
game_directory_deprecation = ("The setting GAME_DIRECTORY_LISTING was removed. It must be "
"renamed to GAME_INDEX_LISTING instead.")
if hasattr(settings, "GAME_DIRECTORY_LISTING"):
raise DeprecationWarning(game_directory_deprecation)
def check_warnings(settings):
"""

View file

@ -1675,7 +1675,8 @@ def init_game_directory(path, check_db=True, need_gamedir=True):
Args:
path (str): Path to new game directory, including its name.
check_db (bool, optional): Check if the databae exists.
need_gamedir (bool, optional): set to False if Evennia doesn't require to be run in a valid game directory.
need_gamedir (bool, optional): set to False if Evennia doesn't require to
be run in a valid game directory.
"""
# set the GAMEDIR path
@ -2090,6 +2091,7 @@ def main():
elif option == "info":
query_info()
elif option == "start":
error_check_python_modules()
start_evennia(args.profiler, args.profiler)
elif option == "istart":
start_server_interactive()

View file

@ -0,0 +1,186 @@
# Evennia Game Index Client
Greg Taylor 2016
This contrib features a client for the [Evennia Game Index]
(http://evennia-game-index.appspot.com/), a listing of games built on
Evennia. By listing your game on the index, 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 index is a success, we'll work
on remedying this.*
## Listing your Game
To list your game, you'll need to enable the Evennia Game Index 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:
```python
"""
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 `EvenniaGameIndexService` and fire it up after the
Evennia server has finished starting:
```python
"""
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.egi_client import EvenniaGameIndexService
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.
"""
egi_service = EvenniaGameIndexService()
server.services.addService(egi_service)
```
Next, configure your game listing by opening up `server/conf/settings.py` and
using the following as a starting point:
```python
######################################################################
# Contrib config
######################################################################
GAME_INDEX_LISTING = {
'game_status': 'pre-alpha',
# Optional, comment out or remove if N/A
'game_website': 'http://my-game.com',
'short_description': 'This is my game. It is fun. You should play it.',
# Optional but highly recommended. Markdown is supported.
'long_description': (
"Hello, there. You silly person.\n\n"
"This is the start of a new paragraph. Markdown is cool. Isn't this "
"[neat](http://evennia.com)? My game is best game. Woohoo!\n\n"
"Time to wrap this up. One final paragraph for the road."
),
'listing_contact': 'me@my-game.com',
# At minimum, specify this or the web_client_url options. Both is fine, too.
'telnet_hostname': 'my-game.com',
'telnet_port': 1234,
# At minimum, specify this or the telnet_* options. Both is fine, too.
'web_client_url': 'http://my-game.com/webclient',
}
```
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 GAME_INDEX_LISTING 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.
### short_description
Required: Yes
A short (max of 255 characters) description of your game that will appear
on the main game index page.
### long_description
Required: No
A longer, full-length description or overview of the game.
[Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet)
and some of the very basic HTML tags are accepted here.
### 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: **Must specify this and telnet_port OR web_client_url**
The hostname that players can telnet into to play your game.
### telnet_port
Required: **Must specify this and telnet_hostname OR web_client_url**
The port that the players can telnet into to play your game.
### web_client_url
Required: **Must specify this OR telnet_hostname + telnet_port**
Full URL to your game's web-based client.
## 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
### My game doesn't appear on the listing!
If you don't see your game appear on the listing after reloading your server,
check the server logs. You should see some error messages describing what
went wrong.
### I changed my game name and now there are two entries
This is a side-effect of our current, naive implementation in our listing
system. Your old entry will disappear within two hours. Alternatively,
speak up on IRC and someone might be able to manually purge the old entry.

View file

@ -0,0 +1 @@
from .service import EvenniaGameIndexService

View file

@ -0,0 +1,179 @@
"""
The client for sending data to the Evennia Game Index
"""
import urllib.request, urllib.parse, urllib.error
import platform
import warnings
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 implementer
from evennia.accounts.models import AccountDB
from evennia.server.sessionhandler import SESSIONS
from evennia.utils import get_evennia_version, logger
_EGI_HOST = 'http://evennia-game-index.appspot.com'
_EGI_REPORT_PATH = '/api/v1/game/check_in'
class EvenniaGameIndexClient(object):
"""
This client class is used for gathering and sending game details to the
Evennia Game Index. Since EGI is in the early goings, this isn't
incredibly configurable as far as to 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 = _EGI_HOST
self.report_path = _EGI_REPORT_PATH
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 Index.
"""
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 Index.")
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 Index. 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):
"""
Build the request to send to the index.
"""
agent = Agent(reactor, pool=self._conn_pool)
headers = {
b'User-Agent': [b'Evennia Game Index Client'],
b'Content-Type': [b'application/x-www-form-urlencoded'],
}
egi_config = settings.GAME_INDEX_LISTING
# We are using `or` statements below with dict.get() to avoid sending
# stringified 'None' values to the server.
try:
values = {
# Game listing stuff
'game_name': settings.SERVERNAME,
'game_status': egi_config['game_status'],
'game_website': egi_config.get('game_website', ''),
'short_description': egi_config['short_description'],
'long_description': egi_config.get('long_description', ''),
'listing_contact': egi_config['listing_contact'],
# How to play
'telnet_hostname': egi_config.get('telnet_hostname', ''),
'telnet_port': egi_config.get('telnet_port', ''),
'web_client_url': egi_config.get('web_client_url', ''),
# Game stats
'connected_account_count': SESSIONS.account_count(),
'total_account_count': AccountDB.objects.num_total_accounts() or 0,
# System info
'evennia_version': get_evennia_version(),
'python_version': platform.python_version(),
'django_version': django.get_version(),
'server_platform': platform.platform(),
}
except KeyError as err:
raise KeyError(f"Error loading GAME_INDEX_LISTING: {err}")
data = urllib.parse.urlencode(values)
d = agent.request(
b'POST', bytes(self.report_url, 'utf-8'),
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))
@implementer(IBodyProducer)
class StringProducer(object):
"""
Used for feeding a request body to the tx HTTP client.
"""
def __init__(self, body):
self.body = bytes(body, 'utf-8')
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

View file

@ -0,0 +1,59 @@
"""
Service for integrating the Evennia Game Index client into Evennia.
"""
from twisted.internet import reactor
from twisted.internet.task import LoopingCall
from twisted.application.service import Service
from evennia.utils import logger
from .client import EvenniaGameIndexClient
# How many seconds to wait before triggering the first EGI check-in.
_FIRST_UPDATE_DELAY = 10
# How often to sync to the server
_CLIENT_UPDATE_RATE = 60 * 30
class EvenniaGameIndexService(Service):
"""
Twisted Service that contains a LoopingCall for regularly sending game details
to the Evennia Game Index.
"""
# We didn't stick the Evennia prefix on here because it'd get marked as
# a core system service.
name = 'GameIndexClient'
def __init__(self):
self.client = EvenniaGameIndexClient(
on_bad_request=self._die_on_bad_request)
self.loop = LoopingCall(self.client.send_game_details)
def startService(self):
super().startService()
# Check to make sure that the client is configured.
# Start the loop, but only after a short delay. This allows the
# portal and the server time to sync up as far as total player counts.
# Prevents always reporting a count of 0.
reactor.callLater(
_FIRST_UPDATE_DELAY, self.loop.start, _CLIENT_UPDATE_RATE)
def stopService(self):
if self.running == 0:
# reload errors if we've stopped this service.
return
super().stopService()
if self.loop.running:
self.loop.stop()
def _die_on_bad_request(self):
"""
If it becomes apparent that our configuration is generating improperly
formed messages to EGI, 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 Index client service due to "
"invalid configuration.")
self.stopService()

View file

@ -74,6 +74,7 @@ IRC_ENABLED = settings.IRC_ENABLED
RSS_ENABLED = settings.RSS_ENABLED
GRAPEVINE_ENABLED = settings.GRAPEVINE_ENABLED
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
GAME_INDEX_ENABLED = settings.GAME_INDEX_ENABLED
INFO_DICT = {"servername": SERVERNAME, "version": VERSION,
"amp": "", "errors": "", "info": "", "webserver": "", "irc_rss": ""}
@ -588,6 +589,11 @@ if GRAPEVINE_ENABLED:
# Grapevine channel connections
ENABLED.append('grapevine')
if GAME_INDEX_ENABLED:
from evennia.server.game_index_client.service import EvenniaGameIndexService
egi_service = EvenniaGameIndexService()
EVENNIA.services.addService(egi_service)
if ENABLED:
INFO_DICT["irc_rss"] = ", ".join(ENABLED) + " enabled."