From ede6634081a90b81c628a4b9ecb35dfd8931c1ce Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 15 Jun 2014 21:58:53 +0200 Subject: [PATCH] Converted webclient to use websockets on port 8001. Ideally one would make it so the ajax and websocket clients work under the same django wrapper, but for now this functionality is elusive. --- src/server/portal/portal.py | 7 +- src/server/portal/websocket.py | 1 + src/settings_default.py | 14 +- ...webclient.js => evennia_ajax_webclient.js} | 0 .../javascript/evennia_websocket_webclient.js | 266 ++++++++++++++++++ src/web/templates/admin/index.html | 26 +- src/web/templates/prosimii/webclient.html | 23 +- src/web/urls.py | 4 +- src/web/utils/general_context.py | 15 +- src/web/webclient/views.py | 5 +- src/web/website/views.py | 2 +- 11 files changed, 326 insertions(+), 37 deletions(-) rename src/web/media/javascript/{evennia_webclient.js => evennia_ajax_webclient.js} (100%) create mode 100644 src/web/media/javascript/evennia_websocket_webclient.js diff --git a/src/server/portal/portal.py b/src/server/portal/portal.py index c40bf5dedc..366da88009 100644 --- a/src/server/portal/portal.py +++ b/src/server/portal/portal.py @@ -262,6 +262,12 @@ if WEBSERVER_ENABLED: web_root.putChild("webclientdata", webclient) webclientstr = "/client" + #from src.server.portal import websocket + #factory = protocol.ServerFactory() + #websocketclient = websocket.WebSocketProtocol() + #websocketclient.sessionhandler = PORTAL_SESSIONS + #web_root.putChild("websocket", websocketclient) + web_root = server.Site(web_root, logPath=settings.HTTP_LOG_FILE) proxy_service = internet.TCPServer(proxyport, web_root, @@ -270,7 +276,6 @@ if WEBSERVER_ENABLED: PORTAL.services.addService(proxy_service) print " webproxy%s%s:%s (<-> %s)" % (webclientstr, ifacestr, proxyport, serverport) - if WEBSOCKET_ENABLED: # websocket support is experimental! diff --git a/src/server/portal/websocket.py b/src/server/portal/websocket.py index 0c3b3f5304..0040921993 100644 --- a/src/server/portal/websocket.py +++ b/src/server/portal/websocket.py @@ -37,6 +37,7 @@ class WebSocketProtocol(Protocol, Session): """ This is called when the connection is first established """ + def connectionMade(self): """ This is called when the connection is first established. diff --git a/src/settings_default.py b/src/settings_default.py index 68ed71c333..f03b3544a0 100644 --- a/src/settings_default.py +++ b/src/settings_default.py @@ -61,6 +61,14 @@ WEBSERVER_THREADPOOL_LIMITS = (1, 20) # Start the evennia ajax client on /webclient # (the webserver must also be running) WEBCLIENT_ENABLED = True +# Activate Websocket support. If this is on, the default webclient will use this +# before going for the ajax implementation +WEBSOCKET_ENABLED = True +# Ports to use for Websockets. If this is changed, you must also update +# src/web/media/javascript/evennia_websocket_webclient.js to match. +WEBSOCKET_PORTS = [8001] +# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6. +WEBSOCKET_INTERFACES = ['0.0.0.0'] # Activate SSH protocol (SecureShell) SSH_ENABLED = False # Ports to use for SSH @@ -73,12 +81,6 @@ SSL_ENABLED = False SSL_PORTS = [4001] # Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6. SSL_INTERFACES = ['0.0.0.0'] -# Activate Websocket support -WEBSOCKET_ENABLED = False -# Ports to use for Websockets -WEBSOCKET_PORTS = [8021] -# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6. -WEBSOCKET_INTERFACES = ['0.0.0.0'] # The path that contains this settings.py file (no trailing slash). BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Path to the src directory containing the bulk of the codebase's code. diff --git a/src/web/media/javascript/evennia_webclient.js b/src/web/media/javascript/evennia_ajax_webclient.js similarity index 100% rename from src/web/media/javascript/evennia_webclient.js rename to src/web/media/javascript/evennia_ajax_webclient.js diff --git a/src/web/media/javascript/evennia_websocket_webclient.js b/src/web/media/javascript/evennia_websocket_webclient.js new file mode 100644 index 0000000000..b58c605923 --- /dev/null +++ b/src/web/media/javascript/evennia_websocket_webclient.js @@ -0,0 +1,266 @@ +/* + +Evennia websocket webclient (javascript component) + +The client is composed of several parts: + templates/webclient.html - the main page + webclient/views.py - the django view serving the template (based on urls.py pattern) + src/server/portal/websockets.py - the server component talking to the client + this file - the javascript component handling dynamic content + +This implements an mud client for use with Evennia, using jQuery +for simplicity. + +messages sent to the client is one of three modes: + OOB(func,(args), func,(args), ...) - OOB command executions + text - any other text is considered a normal text to echo + +*/ + +// jQuery must be imported by the calling html page before this script +// There are plenty of help on using the jQuery library on http://jquery.com/ + +// Server communications +// Set this to the value matching settings.WEBSOCKET_PORTS +var wsurl = "ws://localhost:8001"; + +function webclient_init(){ + websocket = new WebSocket(wsurl); + websocket.onopen = function(evt) { onOpen(evt) }; + websocket.onclose = function(evt) { onClose(evt) }; + websocket.onmessage = function(evt) { onMessage(evt) }; + websocket.onerror = function(evt) { onError(evt) }; +} + +function onOpen(evt) { + $("#connecting").remove(); // remove the "connecting ..." message + msg_display("sys", "Using websockets - connected to " + wsurl + ".") + + setTimeout(function () { + $("#playercount").fadeOut('slow', webclient_set_sizes); + }, 10000); +} + +function onClose(evt) { + CLIENT_HASH = 0; + alert("Mud client connection was closed cleanly."); +} + +function onMessage(evt) { + var inmsg = evt.data + if (inmsg.length > 3 && inmsg.substr(0, 3) == "OOB") { + // dynamically call oob methods, if available + try {var oobtuples = JSON.parse(inmsg.slice(3));} // everything after OOB } + catch(err) { + // not JSON packed - a normal text + msg_display('out', inmsg); + return; + } + + for (var oobtuple in oobtuples) { + try { window[oobtuple[0]](oobtuple[1]) } + catch(err) { msg_display("err", "Could not execute OOB function " + oobtuple[0] + "!") } + } + } + else { + // normal message + msg_display('out', inmsg); } +} + +function onError(evt) { + msg_display('err', "Error: Server returned an error. Try reloading the page."); +} + +function doSend(){ + outmsg = $("#inputfield").val(); + history_add(outmsg); + HISTORY_POS = 0; + $('#inputform')[0].reset(); // clear input field + websocket.send(outmsg); +} + +function doOOB(ooblist){ + // Takes an array on form [funcname, [args], funcname, [args], ... ] + var oobmsg = JSON.stringify(ooblist); + websocket.send("OOB" + oobmsg); +} + +// +// Display messages + +function msg_display(type, msg){ + // Add a div to the message window. + // type gives the class of div to use. + $("#messagewindow").append( + "
"+ msg +"
"); + // scroll message window to bottom + $('#messagewindow').animate({scrollTop: $('#messagewindow')[0].scrollHeight}); +} + +// Input history mechanism + +var HISTORY_MAX_LENGTH = 21 +var HISTORY = new Array(); +HISTORY[0] = ''; +var HISTORY_POS = 0; + +function history_step_back() { + // step backwards in history stack + HISTORY_POS = Math.min(++HISTORY_POS, HISTORY.length-1); + return HISTORY[HISTORY.length-1 - HISTORY_POS]; +} +function history_step_fwd() { + // step forward in history stack + HISTORY_POS = Math.max(--HISTORY_POS, 0); + return HISTORY[HISTORY.length-1 - HISTORY_POS]; +} +function history_add(input) { + // add an entry to history + if (input != HISTORY[HISTORY.length-1]) { + if (HISTORY.length >= HISTORY_MAX_LENGTH) { + HISTORY.shift(); // kill oldest history entry + } + HISTORY[HISTORY.length-1] = input; + HISTORY[HISTORY.length] = ''; + } +} + +// Catching keyboard shortcuts + +$.fn.appendCaret = function() { + /* jQuery extension that will forward the caret to the end of the input, and + won't harm other elements (although calling this on multiple inputs might + not have the expected consequences). + + Thanks to + http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area + for the good starting point. */ + return this.each(function() { + var range, + // Index at where to place the caret. + end, + self = this; + + if (self.setSelectionRange) { + // other browsers + end = self.value.length; + self.focus(); + // NOTE: Need to delay the caret movement until after the callstack. + setTimeout(function() { + self.setSelectionRange(end, end); + }, 0); + } + else if (self.createTextRange) { + // IE + end = self.value.length - 1; + range = self.createTextRange(); + range.collapse(true); + range.moveEnd('character', end); + range.moveStart('character', end); + // NOTE: I haven't tested to see if IE has the same problem as + // W3C browsers seem to have in this context (needing to fire + // select after callstack). + range.select(); + } + }); +}; +$.fn.appendCaret = function() { + /* jQuery extension that will forward the caret to the end of the input, and + won't harm other elements (although calling this on multiple inputs might + not have the expected consequences). + + Thanks to + http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area + for the good starting point. */ + return this.each(function() { + var range, + // Index at where to place the caret. + end, + self = this; + + if (self.setSelectionRange) { + // other browsers + end = self.value.length; + self.focus(); + // NOTE: Need to delay the caret movement until after the callstack. + setTimeout(function() { + self.setSelectionRange(end, end); + }, 0); + } + else if (self.createTextRange) { + // IE + end = self.value.length - 1; + range = self.createTextRange(); + range.collapse(true); + range.moveEnd('character', end); + range.moveStart('character', end); + // NOTE: I haven't tested to see if IE has the same problem as + // W3C browsers seem to have in this context (needing to fire + // select after callstack). + range.select(); + } + }); +}; + +$(document).keydown( function(event) { + // Get the pressed key (normalized by jQuery) + var code = event.which, + inputField = $("#inputfield"); + + // always focus input field no matter which key is pressed + inputField.focus(); + + // Special keys recognized by client + + //msg_display("out", "key code pressed: " + code); // debug + + if (code == 13) { // Enter Key + doSend(); + event.preventDefault(); + } + else { + if (code == 38) { // arrow up 38 + inputField.val(history_step_back()).appendCaret(); + } + else if (code == 40) { // arrow down 40 + inputField.val(history_step_fwd()).appendCaret(); + } + } +}); + +// handler to avoid double-clicks until the ajax request finishes +//$("#inputsend").one("click", webclient_input) + +function webclient_set_sizes() { + // Sets the size of the message window + var win_h = $(document).height(); + //var win_w = $('#wrapper').width(); + var inp_h = $('#inputform').outerHeight(true); + //var inp_w = $('#inputsend').outerWidth(true); + + $("#messagewindow").css({'height': win_h - inp_h - 1}); + //$("#inputfield").css({'width': win_w - inp_w - 20}); +} + + +// Callback function - called when page has finished loading (gets things going) +$(document).ready(function(){ + // remove the "no javascript" warning, since we obviously have javascript + $('#noscript').remove(); + // set sizes of elements and reposition them + webclient_set_sizes(); + // a small timeout to stop 'loading' indicator in Chrome + setTimeout(function () { + webclient_init(); + }, 500); + // set an idle timer to avoid proxy servers to time out on us (every 3 minutes) + setInterval(function() { + webclient_input("idle", true); + }, 60000*3); +}); + +// Callback function - called when the browser window resizes +$(window).resize(webclient_set_sizes); + +// Callback function - called when page is closed or moved away from. +//$(window).bind("beforeunload", webclient_close); diff --git a/src/web/templates/admin/index.html b/src/web/templates/admin/index.html index a7487cd49d..7a4f0a5295 100644 --- a/src/web/templates/admin/index.html +++ b/src/web/templates/admin/index.html @@ -14,12 +14,12 @@ {% if app_list %} - {% for app in app_list %} + {% for app in app_list %} {% if app.name in evennia_userapps %} {% if app.name == 'Players' %} -

Admin

+

Admin

Players are the out-of-character representation of a game account. A Player can potentially control any number of in-game character Objects (depending on game).

@@ -49,7 +49,7 @@   {% endif %} - {% endif %} + {% endif %} {% endfor %} @@ -58,15 +58,15 @@ {% endif %} {% endfor %} -

Game entities

+

Game entities

- {% for app in app_list %} + {% for app in app_list %} {% if app.name in evennia_entityapps %} - {% if app.name == 'Comms' %} + {% if app.name == 'Comms' %}

This defines entities that has an in-game precense or - effect of some kind.

+ effect of some kind.

{% endif %}
@@ -99,18 +99,18 @@ {% endif %} {% endfor %} - - -

Website

- {% for app in app_list %} +

Website

+ + + {% for app in app_list %} {% if app.name in evennia_websiteapps %} - {% if app.name == 'Flatpages' %} + {% if app.name == 'Flatpages' %}

Miscellaneous objects related to the running and - managing of the Web presence.

+ managing of the Web presence.

{% endif %}
diff --git a/src/web/templates/prosimii/webclient.html b/src/web/templates/prosimii/webclient.html index 313736329e..b0d815170f 100644 --- a/src/web/templates/prosimii/webclient.html +++ b/src/web/templates/prosimii/webclient.html @@ -9,13 +9,26 @@ - + + - + + + {% if websocket_enabled %} + + {% else %} + + + {% endif %} - - @@ -38,7 +51,7 @@
Logged in Players: {{num_players_connected}}
-
+
diff --git a/src/web/urls.py b/src/web/urls.py index 5ca3bf8a41..9d9d1844c3 100755 --- a/src/web/urls.py +++ b/src/web/urls.py @@ -44,8 +44,8 @@ urlpatterns = patterns('', # favicon url(r'^favicon\.ico$', RedirectView.as_view(url='/media/images/favicon.ico')), - # ajax stuff - url(r'^webclient/',include('src.web.webclient.urls')), + # webclient stuff + url(r'^webclient/', include('src.web.webclient.urls')), ) # This sets up the server if the user want to run the Django diff --git a/src/web/utils/general_context.py b/src/web/utils/general_context.py index 2b9304e94a..421bf9587a 100644 --- a/src/web/utils/general_context.py +++ b/src/web/utils/general_context.py @@ -1,9 +1,9 @@ # -# This file defines global variables that will always be +# This file defines global variables that will always be # available in a view context without having to repeatedly -# include it. For this to work, this file is included in -# the settings file, in the TEMPLATE_CONTEXT_PROCESSORS -# tuple. +# include it. For this to work, this file is included in +# the settings file, in the TEMPLATE_CONTEXT_PROCESSORS +# tuple. # from django.db import models @@ -19,8 +19,8 @@ except AttributeError: SERVER_VERSION = get_evennia_version() -# Setup lists of the most relevant apps so -# the adminsite becomes more readable. +# Setup lists of the most relevant apps so +# the adminsite becomes more readable. PLAYER_RELATED = ['Players'] GAME_ENTITIES = ['Objects', 'Scripts', 'Comms', 'Help'] @@ -44,5 +44,6 @@ def general_context(request): 'evennia_setupapps': GAME_SETUP, 'evennia_connectapps': CONNECTIONS, 'evennia_websiteapps':WEBSITE, - "webclient_enabled" : settings.WEBCLIENT_ENABLED + "webclient_enabled" : settings.WEBCLIENT_ENABLED, + "websocket_enabled" : settings.WEBSOCKET_ENABLED } diff --git a/src/web/webclient/views.py b/src/web/webclient/views.py index 3d097b6935..d74ff4f5e2 100644 --- a/src/web/webclient/views.py +++ b/src/web/webclient/views.py @@ -8,7 +8,7 @@ page and serve it eventual static content. from django.shortcuts import render_to_response, redirect from django.template import RequestContext from django.conf import settings -from src.server.sessionhandler import SESSIONS +from src.players.models import PlayerDB def webclient(request): """ @@ -21,8 +21,9 @@ def webclient(request): print "Called from port 8000!" #return redirect("http://localhost:8001/webclient/", permanent=True) + nsess = len(PlayerDB.objects.get_connected_players()) or "none" # as an example we send the number of connected players to the template - pagevars = {'num_players_connected': SESSIONS.player_count()} + pagevars = {'num_players_connected': nsess} context_instance = RequestContext(request) return render_to_response('webclient.html', pagevars, context_instance) diff --git a/src/web/website/views.py b/src/web/website/views.py index a0af399ddc..2ab73c3d19 100644 --- a/src/web/website/views.py +++ b/src/web/website/views.py @@ -33,7 +33,7 @@ def page_index(request): nplyrs_conn_recent = len(recent_users) or "none" nplyrs = PlayerDB.objects.num_total_players() or "none" nplyrs_reg_recent = len(PlayerDB.objects.get_recently_created_players()) or "none" - nsess = len(PlayerDB.objects.get_connected_players()) or "noone" + nsess = len(PlayerDB.objects.get_connected_players()) or "none" nobjs = ObjectDB.objects.all().count() nrooms = ObjectDB.objects.filter(db_location__isnull=True).exclude(db_typeclass_path=_BASE_CHAR_TYPECLASS).count()