mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
This commit is contained in:
parent
993113b2b7
commit
005b3f4530
13 changed files with 114 additions and 64 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -2,10 +2,12 @@
|
|||
|
||||
## Evennia 0.9 (2018-2019)
|
||||
|
||||
Update to Python 3
|
||||
### Distribution
|
||||
|
||||
- Use `python3 -m venv <myenvname>`
|
||||
- Use `python3 -m pdb <script>` for debugging
|
||||
- New requirement: Python 3.7 (py2.7 support removed)
|
||||
- Django 2.1
|
||||
- Twisted 19.2.1
|
||||
- Autobahn websockets (remove old tmwx)
|
||||
- Docker image updated
|
||||
|
||||
### Commands
|
||||
|
|
@ -20,6 +22,7 @@ Update to Python 3
|
|||
that are calculated on the fly.
|
||||
- `@py` command now defaults to escaping html tags in its output when viewing in the webclient.
|
||||
Use new `/clientraw` switch to get old behavior (issue #1369).
|
||||
- Shorter and more informative, dynamic, listing of on-command vars if not setting func() in child command class.
|
||||
|
||||
### Web
|
||||
|
||||
|
|
@ -73,7 +76,7 @@ Update to Python 3
|
|||
- Prettifies Django 'change password' workflow
|
||||
- Bugfixes
|
||||
- Fixes bug on login page where error messages were not being displayed
|
||||
- Remove strvalue field from admin; it made no sense to have here, being an optimization field
|
||||
- Remove strvalue field from admin; it made no sense to have here, being an optimization field
|
||||
for internal use.
|
||||
|
||||
### Prototypes
|
||||
|
|
@ -115,6 +118,8 @@ Update to Python 3
|
|||
- Added `evennia.ANSIString` to flat API.
|
||||
- Server/Portal log files now cycle to names on the form `server_.log_19_03_08_` instead of `server.log___19.3.8`, retaining
|
||||
unix file sorting order.
|
||||
- Django signals fire for important events: Puppet/Unpuppet, Object create/rename, Login,
|
||||
Logout, Login fail Disconnect, Account create/rename
|
||||
|
||||
### Utils
|
||||
|
||||
|
|
|
|||
|
|
@ -420,7 +420,9 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
|||
return False
|
||||
|
||||
@classmethod
|
||||
def get_username_validators(cls, validator_config=getattr(settings, 'AUTH_USERNAME_VALIDATORS', [])):
|
||||
def get_username_validators(
|
||||
cls, validator_config=getattr(
|
||||
settings, 'AUTH_USERNAME_VALIDATORS', [])):
|
||||
"""
|
||||
Retrieves and instantiates validators for usernames.
|
||||
|
||||
|
|
@ -437,7 +439,8 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
|||
try:
|
||||
klass = import_string(validator['NAME'])
|
||||
except ImportError:
|
||||
msg = "The module in NAME could not be imported: %s. Check your AUTH_USERNAME_VALIDATORS setting."
|
||||
msg = ("The module in NAME could not be imported: %s. "
|
||||
"Check your AUTH_USERNAME_VALIDATORS setting.")
|
||||
raise ImproperlyConfigured(msg % validator['NAME'])
|
||||
objs.append(klass(**validator.get('OPTIONS', {})))
|
||||
return objs
|
||||
|
|
@ -473,7 +476,8 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
|||
|
||||
"""
|
||||
errors = []
|
||||
if ip: ip = str(ip)
|
||||
if ip:
|
||||
ip = str(ip)
|
||||
|
||||
# See if authentication is currently being throttled
|
||||
if ip and LOGIN_THROTTLE.check(ip):
|
||||
|
|
@ -488,8 +492,8 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
|||
banned = cls.is_banned(username=username, ip=ip)
|
||||
if banned:
|
||||
# this is a banned IP or name!
|
||||
errors.append("|rYou have been banned and cannot continue from here." \
|
||||
"\nIf you feel this ban is in error, please email an admin.|x")
|
||||
errors.append("|rYou have been banned and cannot continue from here."
|
||||
"\nIf you feel this ban is in error, please email an admin.|x")
|
||||
logger.log_sec('Authentication Denied (Banned): %s (IP: %s).' % (username, ip))
|
||||
LOGIN_THROTTLE.update(ip, 'Too many sightings of banned artifact.')
|
||||
return None, errors
|
||||
|
|
@ -504,7 +508,8 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
|||
logger.log_sec('Authentication Failure: %s (IP: %s).' % (username, ip))
|
||||
|
||||
# Update throttle
|
||||
if ip: LOGIN_THROTTLE.update(ip, 'Too many authentication failures.')
|
||||
if ip:
|
||||
LOGIN_THROTTLE.update(ip, 'Too many authentication failures.')
|
||||
|
||||
# Try to call post-failure hook
|
||||
session = kwargs.get('session', None)
|
||||
|
|
@ -573,7 +578,8 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
|||
# Disqualify if any check failed
|
||||
if False in valid:
|
||||
valid = False
|
||||
else: valid = True
|
||||
else:
|
||||
valid = True
|
||||
|
||||
return valid, errors
|
||||
|
||||
|
|
@ -713,7 +719,8 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
|||
account.db.FIRST_LOGIN = True
|
||||
|
||||
# Record IP address of creation, if available
|
||||
if ip: account.db.creator_ip = ip
|
||||
if ip:
|
||||
account.db.creator_ip = ip
|
||||
|
||||
# join the new account to the public channel
|
||||
pchannel = ChannelDB.objects.get_channel(settings.DEFAULT_CHANNELS[0]["key"])
|
||||
|
|
@ -933,7 +940,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
|||
|
||||
"""
|
||||
result = super().access(accessing_obj, access_type=access_type,
|
||||
default=default, no_superuser_bypass=no_superuser_bypass)
|
||||
default=default, no_superuser_bypass=no_superuser_bypass)
|
||||
self.at_access(result, accessing_obj, access_type, **kwargs)
|
||||
return result
|
||||
|
||||
|
|
@ -1447,7 +1454,8 @@ class DefaultGuest(DefaultAccount):
|
|||
break
|
||||
if not username:
|
||||
errors.append("All guest accounts are in use. Please try again later.")
|
||||
if ip: LOGIN_THROTTLE.update(ip, 'Too many requests for Guest access.')
|
||||
if ip:
|
||||
LOGIN_THROTTLE.update(ip, 'Too many requests for Guest access.')
|
||||
return None, errors
|
||||
else:
|
||||
# build a new account with the found guest username
|
||||
|
|
|
|||
|
|
@ -414,6 +414,14 @@ class Command(with_metaclass(CommandMeta, object)):
|
|||
set in self.parse())
|
||||
|
||||
"""
|
||||
variables = '\n'.join(" |w{}|n ({}): {}".format(key, type(val), val) for key, val in self.__dict__.items())
|
||||
string = f"""
|
||||
Command {self} has no defined `func()` - showing on-command variables:
|
||||
{variables}
|
||||
"""
|
||||
self.caller.msg(string)
|
||||
return
|
||||
|
||||
# a simple test command to show the available properties
|
||||
string = "-" * 50
|
||||
string += "\n|w%s|n - Command variables from evennia:\n" % self.key
|
||||
|
|
|
|||
|
|
@ -200,6 +200,13 @@ class MuxCommand(Command):
|
|||
by the cmdhandler right after self.parser() finishes, and so has access
|
||||
to all the variables defined therein.
|
||||
"""
|
||||
variables = '\n'.join(" |w{}|n ({}): {}".format(key, type(val), val) for key, val in self.__dict__.items())
|
||||
string = f"""
|
||||
Command {self} has no defined `func()` - showing on-command variables: No child func() defined for {self} - available variables:
|
||||
{variables}
|
||||
"""
|
||||
self.caller.msg(string)
|
||||
return
|
||||
# a simple test command to show the available properties
|
||||
string = "-" * 50
|
||||
string += "\n|w%s|n - Command variables from evennia:\n" % self.key
|
||||
|
|
|
|||
|
|
@ -446,7 +446,7 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
|
|||
nobjs = nobjs or 1 # fix zero-div error with empty database
|
||||
|
||||
# total object sum table
|
||||
totaltable = self.style_table("|wtype|n", "|wcomment|n", "|wcount|n", "|w%%|n",
|
||||
totaltable = self.style_table("|wtype|n", "|wcomment|n", "|wcount|n", "|w%|n",
|
||||
border="table", align="l")
|
||||
totaltable.align = 'l'
|
||||
totaltable.add_row("Characters", "(BASE_CHARACTER_TYPECLASS + children)",
|
||||
|
|
@ -458,7 +458,8 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
|
|||
totaltable.add_row("Other", "", nother, "%.2f" % ((float(nother) / nobjs) * 100))
|
||||
|
||||
# typeclass table
|
||||
typetable = self.style_table("|wtypeclass|n", "|wcount|n", "|w%%|n", border="table", align="l")
|
||||
typetable = self.style_table("|wtypeclass|n", "|wcount|n", "|w%|n",
|
||||
border="table", align="l")
|
||||
typetable.align = 'l'
|
||||
dbtotals = ObjectDB.objects.object_totals()
|
||||
for path, count in dbtotals.items():
|
||||
|
|
@ -466,7 +467,8 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
# last N table
|
||||
objs = ObjectDB.objects.all().order_by("db_date_created")[max(0, nobjs - nlim):]
|
||||
latesttable = self.style_table("|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", align="l", border="table")
|
||||
latesttable = self.style_table("|wcreated|n", "|wdbref|n", "|wname|n",
|
||||
"|wtypeclass|n", align="l", border="table")
|
||||
latesttable.align = 'l'
|
||||
for obj in objs:
|
||||
latesttable.add_row(utils.datetime_format(obj.date_created),
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class Command(BaseCommand):
|
|||
|
||||
Each Command implements the following methods, called
|
||||
in this order (only func() is actually required):
|
||||
- at_pre_cmd(): If this returns True, execution is aborted.
|
||||
- at_pre_cmd(): If this returns anything truthy, execution is aborted.
|
||||
- parse(): Should perform any extra parsing needed on self.args
|
||||
and store the result on self.
|
||||
- func(): Performs the actual work.
|
||||
|
|
|
|||
|
|
@ -90,8 +90,11 @@ class PortalSessionHandler(SessionHandler):
|
|||
|
||||
if session:
|
||||
# assign if we are first-connectors
|
||||
self.latest_sessid += 1
|
||||
session.sessid = self.latest_sessid
|
||||
if not session.sessid:
|
||||
# if the session already has a sessid (e.g. being inherited in the
|
||||
# case of a webclient auto-reconnect), keep it
|
||||
self.latest_sessid += 1
|
||||
session.sessid = self.latest_sessid
|
||||
session.server_connected = False
|
||||
_CONNECTION_QUEUE.appendleft(session)
|
||||
if len(_CONNECTION_QUEUE) > 1:
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ _RE_SCREENREADER_REGEX = re.compile(r"%s" % settings.SCREENREADER_REGEX_STRIP, r
|
|||
_CLIENT_SESSIONS = mod_import(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
CLOSE_NORMAL = WebSocketServerProtocol.CLOSE_STATUS_CODE_NORMAL
|
||||
|
||||
|
||||
class WebSocketClient(WebSocketServerProtocol, Session):
|
||||
"""
|
||||
Implements the server-side of the Websocket connection.
|
||||
|
|
@ -70,19 +73,21 @@ class WebSocketClient(WebSocketServerProtocol, Session):
|
|||
client_address = client_address[0] if client_address else None
|
||||
self.init_session("websocket", client_address, self.factory.sessionhandler)
|
||||
|
||||
from evennia.utils import logger
|
||||
try:
|
||||
csessid = self.http_request_uri.split("?", 1)[1]
|
||||
except Exception:
|
||||
logger.log_trace(str(self.__dict__))
|
||||
|
||||
csession = self.get_client_session()
|
||||
csession = self.get_client_session() # this sets self.csessid
|
||||
csessid = self.csessid
|
||||
uid = csession and csession.get("webclient_authenticated_uid", None)
|
||||
if uid:
|
||||
# the client session is already logged in.
|
||||
self.uid = uid
|
||||
self.logged_in = True
|
||||
|
||||
for old_session in self.sessionhandler.sessions_from_csessid(csessid):
|
||||
if (hasattr(old_session, "websocket_close_code") and
|
||||
old_session.websocket_close_code != CLOSE_NORMAL):
|
||||
# if we have old sessions with the same csession, they are remnants
|
||||
self.sessid = old_session.sessid
|
||||
self.sessionhandler.disconnect(old_session)
|
||||
|
||||
# watch for dead links
|
||||
self.transport.setTcpKeepAlive(1)
|
||||
# actually do the connection
|
||||
|
|
@ -97,11 +102,19 @@ class WebSocketClient(WebSocketServerProtocol, Session):
|
|||
reason (str or None): Motivation for the disconnection.
|
||||
|
||||
"""
|
||||
csession = self.get_client_session()
|
||||
|
||||
if csession:
|
||||
csession["webclient_authenticated_uid"] = None
|
||||
csession.save()
|
||||
self.logged_in = False
|
||||
|
||||
self.sessionhandler.disconnect(self)
|
||||
# autobahn-python: 1000 for a normal close, 3000-4999 for app. specific,
|
||||
# in case anyone wants to expose this functionality later.
|
||||
#
|
||||
# sendClose() under autobahn/websocket/interfaces.py
|
||||
self.sendClose(1000, reason)
|
||||
self.sendClose(CLOSE_NORMAL, reason)
|
||||
|
||||
def onClose(self, wasClean, code=None, reason=None):
|
||||
"""
|
||||
|
|
@ -111,19 +124,14 @@ class WebSocketClient(WebSocketServerProtocol, Session):
|
|||
|
||||
Args:
|
||||
wasClean (bool): ``True`` if the WebSocket was closed cleanly.
|
||||
reason (str): Motivation for the lost connection.
|
||||
code (int or None): Close status as sent by the WebSocket peer.
|
||||
reason (str or None): Close reason as sent by the WebSocket peer.
|
||||
|
||||
"""
|
||||
csession = self.get_client_session()
|
||||
|
||||
if csession:
|
||||
csession["webclient_authenticated_uid"] = None
|
||||
csession.save()
|
||||
self.logged_in = False
|
||||
|
||||
self.sessionhandler.disconnect(self)
|
||||
if code == CLOSE_NORMAL:
|
||||
self.disconnect(reason)
|
||||
else:
|
||||
self.websocket_close_code = code
|
||||
|
||||
def onMessage(self, payload, isBinary):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ class SessionHandler(dict):
|
|||
return newdict
|
||||
elif is_iter(data):
|
||||
return [_validate(part) for part in data]
|
||||
elif isinstance(data, (str, bytes )):
|
||||
elif isinstance(data, (str, bytes)):
|
||||
data = _utf8(data)
|
||||
|
||||
if _INLINEFUNC_ENABLED and not raw and isinstance(self, ServerSessionHandler):
|
||||
|
|
@ -257,9 +257,9 @@ class SessionHandler(dict):
|
|||
return rkwargs
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# ------------------------------------------------------------
|
||||
# Server-SessionHandler class
|
||||
#------------------------------------------------------------
|
||||
# ------------------------------------------------------------
|
||||
|
||||
class ServerSessionHandler(SessionHandler):
|
||||
"""
|
||||
|
|
@ -413,7 +413,7 @@ class ServerSessionHandler(SessionHandler):
|
|||
# set a watchdog to avoid self.disconnect from deleting
|
||||
# the session while we are looping over them
|
||||
self._disconnect_all = True
|
||||
for session in self.values:
|
||||
for session in self.values():
|
||||
session.disconnect()
|
||||
del self._disconnect_all
|
||||
|
||||
|
|
@ -443,7 +443,8 @@ class ServerSessionHandler(SessionHandler):
|
|||
|
||||
"""
|
||||
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION, operation=SCONN,
|
||||
protocol_path=protocol_path, config=configdict)
|
||||
protocol_path=protocol_path,
|
||||
config=configdict)
|
||||
|
||||
def portal_restart_server(self):
|
||||
"""
|
||||
|
|
@ -544,7 +545,8 @@ class ServerSessionHandler(SessionHandler):
|
|||
nsess = len(self.sessions_from_account(session.account)) - 1
|
||||
sreason = " ({})".format(reason) if reason else ""
|
||||
string = "Logged out: {account} {address} ({nsessions} sessions(s) remaining){reason}"
|
||||
string = string.format(reason=sreason, account=session.account, address=session.address, nsessions=nsess)
|
||||
string = string.format(reason=sreason, account=session.account,
|
||||
address=session.address, nsessions=nsess)
|
||||
session.log(string)
|
||||
|
||||
if nsess == 0:
|
||||
|
|
@ -670,7 +672,8 @@ class ServerSessionHandler(SessionHandler):
|
|||
amount of Sessions due to multi-playing).
|
||||
|
||||
"""
|
||||
return list(set(session.account for session in self.values() if session.logged_in and session.account))
|
||||
return list(set(session.account for session in self.values()
|
||||
if session.logged_in and session.account))
|
||||
|
||||
def session_from_sessid(self, sessid):
|
||||
"""
|
||||
|
|
@ -737,13 +740,17 @@ class ServerSessionHandler(SessionHandler):
|
|||
|
||||
def sessions_from_csessid(self, csessid):
|
||||
"""
|
||||
Given a cliend identification hash (for session types that offer them) return all sessions with
|
||||
a matching hash.
|
||||
Given a client identification hash (for session types that offer them)
|
||||
return all sessions with a matching hash.
|
||||
|
||||
Args
|
||||
csessid (str): The session hash
|
||||
csessid (str): The session hash.
|
||||
Returns:
|
||||
sessions (list): The sessions with matching .csessid, if any.
|
||||
|
||||
"""
|
||||
if csessid:
|
||||
return []
|
||||
return [session for session in self.values()
|
||||
if session.csessid and session.csessid == csessid]
|
||||
|
||||
|
|
|
|||
|
|
@ -68,13 +68,14 @@ def general_context(request):
|
|||
is automatically added to context of all views.
|
||||
"""
|
||||
account = None
|
||||
if request.user.is_authenticated: account = request.user
|
||||
if request.user.is_authenticated:
|
||||
account = request.user
|
||||
|
||||
puppet = None
|
||||
if account and request.session.get('puppet'):
|
||||
pk = int(request.session.get('puppet'))
|
||||
puppet = next((x for x in account.characters if x.pk == pk), None)
|
||||
|
||||
|
||||
return {
|
||||
'account': account,
|
||||
'puppet': puppet,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from django.contrib.auth import authenticate, login
|
|||
from evennia.accounts.models import AccountDB
|
||||
from evennia.utils import logger
|
||||
|
||||
|
||||
class SharedLoginMiddleware(object):
|
||||
"""
|
||||
Handle the shared login between website and webclient.
|
||||
|
|
@ -10,47 +11,47 @@ class SharedLoginMiddleware(object):
|
|||
def __init__(self, get_response):
|
||||
# One-time configuration and initialization.
|
||||
self.get_response = get_response
|
||||
|
||||
|
||||
def __call__(self, request):
|
||||
# Code to be executed for each request before
|
||||
# the view (and later middleware) are called.
|
||||
|
||||
|
||||
# Synchronize credentials between webclient and website
|
||||
# Must be performed *before* rendering the view (issue #1723)
|
||||
self.make_shared_login(request)
|
||||
|
||||
|
||||
# Process view
|
||||
response = self.get_response(request)
|
||||
|
||||
# Code to be executed for each request/response after
|
||||
# the view is called.
|
||||
|
||||
|
||||
# Return processed view
|
||||
return response
|
||||
|
||||
|
||||
@classmethod
|
||||
def make_shared_login(cls, request):
|
||||
csession = request.session
|
||||
account = request.user
|
||||
website_uid = csession.get("website_authenticated_uid", None)
|
||||
webclient_uid = csession.get("webclient_authenticated_uid", None)
|
||||
|
||||
|
||||
if not csession.session_key:
|
||||
# this is necessary to build the sessid key
|
||||
csession.save()
|
||||
|
||||
|
||||
if account.is_authenticated:
|
||||
# Logged into website
|
||||
if not website_uid:
|
||||
if website_uid is None:
|
||||
# fresh website login (just from login page)
|
||||
csession["website_authenticated_uid"] = account.id
|
||||
if webclient_uid is None:
|
||||
# auto-login web client
|
||||
csession["webclient_authenticated_uid"] = account.id
|
||||
|
||||
if webclient_uid is None:
|
||||
# auto-login web client
|
||||
csession["webclient_authenticated_uid"] = account.id
|
||||
|
||||
elif webclient_uid:
|
||||
# Not logged into website, but logged into webclient
|
||||
if not website_uid:
|
||||
if website_uid is None:
|
||||
csession["website_authenticated_uid"] = account.id
|
||||
account = AccountDB.objects.get(id=webclient_uid)
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# general
|
||||
django >= 2.1, < 2.2
|
||||
twisted >= 18.0.0, < 19.0.0
|
||||
twisted >= 19.2.1, < 20.0.0
|
||||
pillow == 5.2.0
|
||||
pytz
|
||||
future >= 0.15.2
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ pypiwin32
|
|||
|
||||
# general
|
||||
django >= 2.1, < 2.2
|
||||
twisted >= 18.0.0, < 19.0.0
|
||||
twisted >= 19.2.1, < 20.0.0
|
||||
pillow == 5.2.0
|
||||
pytz
|
||||
future >= 0.15.2
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue