From 4b9db9570c668ea631702c1188d9cc8796633d0d Mon Sep 17 00:00:00 2001 From: Ryan Stein Date: Tue, 3 Oct 2017 12:41:51 -0400 Subject: [PATCH 1/7] add @tel/loc functionality, fix unescaped | in @tel error message --- evennia/commands/default/building.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 9170f3c9ff..ff487626a1 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2324,6 +2324,7 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS): Note that the only way to retrieve an object from a None location is by direct #dbref reference. A puppeted object cannot be moved to None. + loc - teleport object to the target's location instead of its contents Teleports an object somewhere. If no object is given, you yourself is teleported to the target location. """ @@ -2343,6 +2344,7 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS): # setting switches tel_quietly = "quiet" in switches to_none = "tonone" in switches + to_loc = "loc" in switches if to_none: # teleporting to None @@ -2368,7 +2370,7 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS): # not teleporting to None location if not args and not to_none: - caller.msg("Usage: teleport[/switches] [ =] |home") + caller.msg("Usage: teleport[/switches] [ =] ||home") return if rhs: @@ -2384,6 +2386,11 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS): if not destination: caller.msg("Destination not found.") return + if to_loc: + destination = destination.location + if not destination: + caller.msg("Destination has no location.") + return if obj_to_teleport == destination: caller.msg("You can't teleport an object inside of itself!") return From 5ecaa9daca3473fde62aa1f272316a20cb35ae5e Mon Sep 17 00:00:00 2001 From: Nicholas Matlaga Date: Thu, 5 Oct 2017 11:45:01 -0400 Subject: [PATCH 2/7] fix Notification not being available crash --- .../static/webclient/js/webclient_gui.js | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/evennia/web/webclient/static/webclient/js/webclient_gui.js b/evennia/web/webclient/static/webclient/js/webclient_gui.js index 7f499253f9..c1af719f72 100644 --- a/evennia/web/webclient/static/webclient/js/webclient_gui.js +++ b/evennia/web/webclient/static/webclient/js/webclient_gui.js @@ -369,28 +369,29 @@ function onNewLine(text, originator) { unread++; favico.badge(unread); document.title = "(" + unread + ") " + originalTitle; + if ("Notification" in window){ + if (("notification_popup" in options) && (options["notification_popup"])) { + Notification.requestPermission().then(function(result) { + if(result === "granted") { + var title = originalTitle === "" ? "Evennia" : originalTitle; + var options = { + body: text.replace(/(<([^>]+)>)/ig,""), + icon: "/static/website/images/evennia_logo.png" + } - if (("notification_popup" in options) && (options["notification_popup"])) { - Notification.requestPermission().then(function(result) { - if(result === "granted") { - var title = originalTitle === "" ? "Evennia" : originalTitle; - var options = { - body: text.replace(/(<([^>]+)>)/ig,""), - icon: "/static/website/images/evennia_logo.png" + var n = new Notification(title, options); + n.onclick = function(e) { + e.preventDefault(); + window.focus(); + this.close(); + } } - - var n = new Notification(title, options); - n.onclick = function(e) { - e.preventDefault(); - window.focus(); - this.close(); - } - } - }); - } - if (("notification_sound" in options) && (options["notification_sound"])) { - var audio = new Audio("/static/webclient/media/notification.wav"); - audio.play(); + }); + } + if (("notification_sound" in options) && (options["notification_sound"])) { + var audio = new Audio("/static/webclient/media/notification.wav"); + audio.play(); + } } } } @@ -427,7 +428,9 @@ function doStartDragDialog(event) { // Event when client finishes loading $(document).ready(function() { - Notification.requestPermission(); + if ("Notification" in window) { + Notification.requestPermission(); + } favico = new Favico({ animation: 'none' From 2c88963e3c199d771ee97c1b5a558471a9c179cc Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 5 Oct 2017 20:18:57 +0200 Subject: [PATCH 3/7] Fix to handle changed Widget signature affecting picklefield in admin --- evennia/utils/picklefield.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/evennia/utils/picklefield.py b/evennia/utils/picklefield.py index d63b9b32e3..bd0a9b1643 100644 --- a/evennia/utils/picklefield.py +++ b/evennia/utils/picklefield.py @@ -126,7 +126,13 @@ class PickledWidget(Textarea): except ValueError: return value - final_attrs = self.build_attrs(attrs, name=name) + # fix since the signature of build_attrs changed in Django 1.11 + if attrs is not None: + attrs["name"] = name + else: + attrs = {"name": name} + + final_attrs = self.build_attrs(attrs) return format_html('\r\n{1}', flatatt(final_attrs), value) From 8eff4678b490929a0790379cee246ce48b77d3dd Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 5 Oct 2017 21:29:05 +0200 Subject: [PATCH 4/7] Cleanup and more help entries for the Admin Attr/Tag inline forms --- evennia/typeclasses/admin.py | 63 +++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/evennia/typeclasses/admin.py b/evennia/typeclasses/admin.py index 47c0b457ae..c5dd481f90 100644 --- a/evennia/typeclasses/admin.py +++ b/evennia/typeclasses/admin.py @@ -18,17 +18,28 @@ class TagAdmin(admin.ModelAdmin): class TagForm(forms.ModelForm): """ - This form overrides the base behavior of the ModelForm that would be used for a Tag-through-model. - Since the through-models only have access to the foreignkeys of the Tag and the Object that they're - attached to, we need to spoof the behavior of it being a form that would correspond to its tag, - or the creation of a tag. Instead of being saved, we'll call to the Object's handler, which will handle - the creation, change, or deletion of a tag for us, as well as updating the handler's cache so that all - changes are instantly updated in-game. + This form overrides the base behavior of the ModelForm that would be used for a + Tag-through-model. Since the through-models only have access to the foreignkeys of the Tag and + the Object that they're attached to, we need to spoof the behavior of it being a form that would + correspond to its tag, or the creation of a tag. Instead of being saved, we'll call to the + Object's handler, which will handle the creation, change, or deletion of a tag for us, as well + as updating the handler's cache so that all changes are instantly updated in-game. """ - tag_key = forms.CharField(label='Tag Name') - tag_category = forms.CharField(label="Category", required=False) - tag_type = forms.CharField(label="Type", required=False) - tag_data = forms.CharField(label="Data", required=False) + tag_key = forms.CharField(label='Tag Name', + required=True, + help_text="This is the main key identifier") + tag_category = forms.CharField(label="Category", + help_text="Used for grouping tags. Unset (default) gives a category of None", + required=False) + tag_type = forms.CharField(label="Type", + help_text="Internal use. Either unset, \"alias\" or \"permission\"", + required=False) + tag_data = forms.CharField(label="Data", + help_text="Usually unused. Intended for eventual info about the tag itself", + required=False) + + class Meta: + fields = ("tag_key", "tag_category", "tag_data", "tag_type") def __init__(self, *args, **kwargs): """ @@ -121,8 +132,8 @@ class TagInline(admin.TabularInline): form = TagForm formset = TagFormSet related_field = None # Must be 'objectdb', 'accountdb', 'msg', etc. Set when subclassing - raw_id_fields = ('tag',) - readonly_fields = ('tag',) + # raw_id_fields = ('tag',) + # readonly_fields = ('tag',) extra = 0 def get_formset(self, request, obj=None, **kwargs): @@ -150,13 +161,26 @@ class AttributeForm(forms.ModelForm): changes are instantly updated in-game. """ attr_key = forms.CharField(label='Attribute Name', required=False, initial="Enter Attribute Name Here") - attr_category = forms.CharField(label="Category", help_text="type of attribute, for sorting", required=False) + attr_category = forms.CharField(label="Category", + help_text="type of attribute, for sorting", + required=False, + max_length=4) attr_value = PickledFormField(label="Value", help_text="Value to pickle/save", required=False) - attr_type = forms.CharField(label="Type", help_text="nick for nickname, else leave blank", required=False) + attr_type = forms.CharField(label="Type", + help_text="Internal use. Either unset (normal Attribute) or \"nick\"", + required=False, + max_length=4) attr_strvalue = forms.CharField(label="String Value", - help_text="Only enter this if value is blank and you want to save as a string", - required=False) - attr_lockstring = forms.CharField(label="Locks", required=False, widget=forms.Textarea) + help_text="Only set when using the Attribute as a string-only store", + required=False, + widget=forms.Textarea(attrs={"rows": 1, "cols": 6})) + attr_lockstring = forms.CharField(label="Locks", + required=False, + help_text="Lock string on the form locktype:lockdef;lockfunc:lockdef;...", + widget=forms.Textarea(attrs={"rows": 1, "cols": 8})) + + class Meta: + fields = ("attr_key", "attr_value", "attr_category", "attr_strvalue", "attr_lockstring", "attr_type") def __init__(self, *args, **kwargs): """ @@ -164,6 +188,7 @@ class AttributeForm(forms.ModelForm): to have based on the Attribute. attr_key, attr_category, attr_value, attr_strvalue, attr_type, and attr_lockstring all refer to the corresponding Attribute fields. The initial data of the form fields will similarly be populated. + """ super(AttributeForm, self).__init__(*args, **kwargs) attr_key = None @@ -261,8 +286,8 @@ class AttributeInline(admin.TabularInline): form = AttributeForm formset = AttributeFormSet related_field = None # Must be 'objectdb', 'accountdb', 'msg', etc. Set when subclassing - raw_id_fields = ('attribute',) - readonly_fields = ('attribute',) + # raw_id_fields = ('attribute',) + # readonly_fields = ('attribute',) extra = 0 def get_formset(self, request, obj=None, **kwargs): From 2d030afb36fe5b236a6b18fb8d981eb4e8a534b0 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 5 Oct 2017 22:43:12 +0200 Subject: [PATCH 5/7] Update AJAX client with the corrected Autologin functionality --- evennia/server/portal/portal.py | 6 ++-- evennia/server/portal/webclient.py | 1 - evennia/server/portal/webclient_ajax.py | 38 ++++++++++++++++++++----- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index c5c9740ef9..e8c310cae1 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -294,9 +294,9 @@ if WEBSERVER_ENABLED: # create ajax client processes at /webclientdata from evennia.server.portal import webclient_ajax - webclient = webclient_ajax.WebClient() - webclient.sessionhandler = PORTAL_SESSIONS - web_root.putChild("webclientdata", webclient) + ajax_webclient = webclient_ajax.AjaxWebClient() + ajax_webclient.sessionhandler = PORTAL_SESSIONS + web_root.putChild("webclientdata", ajax_webclient) webclientstr = "\n + webclient (ajax only)" if WEBSOCKET_CLIENT_ENABLED and not websocket_started: diff --git a/evennia/server/portal/webclient.py b/evennia/server/portal/webclient.py index 7d769ec899..d025b85018 100644 --- a/evennia/server/portal/webclient.py +++ b/evennia/server/portal/webclient.py @@ -93,7 +93,6 @@ class WebSocketClient(Protocol, Session): csession = self.get_client_session() if csession: - print("In disconnect: csession uid=%s" % csession.get("webclient_authenticated_uid", None)) csession["webclient_authenticated_uid"] = None csession.save() self.logged_in = False diff --git a/evennia/server/portal/webclient_ajax.py b/evennia/server/portal/webclient_ajax.py index 4ec78e3eb7..c31f5d8eab 100644 --- a/evennia/server/portal/webclient_ajax.py +++ b/evennia/server/portal/webclient_ajax.py @@ -53,11 +53,11 @@ def jsonify(obj): # -# WebClient resource - this is called by the ajax client +# AjaxWebClient resource - this is called by the ajax client # using POST requests to /webclientdata. # -class WebClient(resource.Resource): +class AjaxWebClient(resource.Resource): """ An ajax/comet long-polling transport @@ -163,13 +163,13 @@ class WebClient(resource.Resource): remote_addr = request.getClientIP() host_string = "%s (%s:%s)" % (_SERVERNAME, request.getRequestHostname(), request.getHost().port) - sess = WebClientSession() + sess = AjaxWebClientSession() sess.client = self sess.init_session("ajax/comet", remote_addr, self.sessionhandler) sess.csessid = csessid csession = _CLIENT_SESSIONS(session_key=sess.csessid) - uid = csession and csession.get("logged_in", False) + uid = csession and csession.get("webclient_authenticated_uid", False) if uid: # the client session is already logged in sess.uid = uid @@ -292,14 +292,26 @@ class WebClient(resource.Resource): # web client interface. # -class WebClientSession(session.Session): +class AjaxWebClientSession(session.Session): """ - This represents a session running in a webclient. + This represents a session running in an AjaxWebclient. """ def __init__(self, *args, **kwargs): self.protocol_name = "ajax/comet" - super(WebClientSession, self).__init__(*args, **kwargs) + super(AjaxWebClientSession, self).__init__(*args, **kwargs) + + def get_client_session(self): + """ + Get the Client browser session (used for auto-login based on browser session) + + Returns: + csession (ClientSession): This is a django-specific internal representation + of the browser session. + + """ + if self.csessid: + return _CLIENT_SESSIONS(session_key=self.csessid) def disconnect(self, reason="Server disconnected."): """ @@ -308,10 +320,22 @@ class WebClientSession(session.Session): Args: reason (str): Motivation for the disconnect. """ + csession = self.get_client_session() + + if csession: + csession["webclient_authenticated_uid"] = None + csession.save() + self.logged_in = False self.client.lineSend(self.csessid, ["connection_close", [reason], {}]) self.client.client_disconnect(self.csessid) self.sessionhandler.disconnect(self) + def at_login(self): + csession = self.get_client_session() + if csession: + csession["webclient_authenticated_uid"] = self.uid + csession.save() + def data_out(self, **kwargs): """ Data Evennia -> User From 349affe3059fc22bc53130c20ca4b242e2e34ee8 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 5 Oct 2017 22:53:45 +0200 Subject: [PATCH 6/7] Minor doc string update in evennia.js --- evennia/web/webclient/static/webclient/js/evennia.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/evennia/web/webclient/static/webclient/js/evennia.js b/evennia/web/webclient/static/webclient/js/evennia.js index 328a2a1bf8..501a04021d 100644 --- a/evennia/web/webclient/static/webclient/js/evennia.js +++ b/evennia/web/webclient/static/webclient/js/evennia.js @@ -10,12 +10,12 @@ old and does not support websockets, it will instead fall back to a long-polling (AJAX/COMET) type of connection (using evennia/server/portal/webclient_ajax.py) -All messages is a valid JSON array on single form: +All messages are valid JSON arrays on this single form: ["cmdname", args, kwargs], -where args is an JSON array and kwargs is a JSON object that will be -used as argument to call the cmdname function. +where args is an JSON array and kwargs is a JSON object. These will be both +used as arguments emitted to a callback named "cmdname" as cmdname(args, kwargs). This library makes the "Evennia" object available. It has the following official functions: @@ -45,7 +45,7 @@ An "emitter" object must have a function relay the data to its correct gui element. - The default emitter also has the following methods: - on(cmdname, listener) - this ties a listener to the backend. This function - should be called as listener(kwargs) when the backend calls emit. + should be called as listener(args, kwargs) when the backend calls emit. - off(cmdname) - remove the listener for this cmdname. */ From 866e5dce4188da667a7d252d444050b6e0ed2894 Mon Sep 17 00:00:00 2001 From: Ryan Stein Date: Thu, 5 Oct 2017 12:01:53 -0400 Subject: [PATCH 7/7] make help command's matching respect CMD_IGNORE_PREFIXES --- evennia/commands/default/help.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/evennia/commands/default/help.py b/evennia/commands/default/help.py index 715f0ad17d..6f09f98068 100644 --- a/evennia/commands/default/help.py +++ b/evennia/commands/default/help.py @@ -17,6 +17,7 @@ from evennia.utils.utils import string_suggestions, class_from_module COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) HELP_MORE = settings.HELP_MORE +CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES # limit symbol import for API __all__ = ("CmdHelp", "CmdSetHelp") @@ -231,6 +232,15 @@ class CmdHelp(Command): # try an exact command auto-help match match = [cmd for cmd in all_cmds if cmd == query] + + if not match: + # try an inexact match with prefixes stripped from query and cmds + _query = query[1:] if query[0] in CMD_IGNORE_PREFIXES else query + + match = [cmd for cmd in all_cmds + for m in cmd._matchset if m == _query or + m[0] in CMD_IGNORE_PREFIXES and m[1:] == _query] + if len(match) == 1: formatted = self.format_help_entry(match[0].key, match[0].get_help(caller, cmdset),