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()