mirror of
https://github.com/evennia/evennia.git
synced 2026-03-17 05:16:31 +01:00
800 lines
29 KiB
Python
800 lines
29 KiB
Python
"""
|
|
This module defines handlers for storing sessions when handles
|
|
sessions of users connecting to the server.
|
|
|
|
There are two similar but separate stores of sessions:
|
|
|
|
- ServerSessionHandler - this stores generic game sessions
|
|
for the game. These sessions has no knowledge about
|
|
how they are connected to the world.
|
|
- PortalSessionHandler - this stores sessions created by
|
|
twisted protocols. These are dumb connectors that
|
|
handle network communication but holds no game info.
|
|
|
|
"""
|
|
import time
|
|
from builtins import object
|
|
from future.utils import listvalues
|
|
|
|
from django.conf import settings
|
|
from evennia.commands.cmdhandler import CMD_LOGINSTART
|
|
from evennia.utils.logger import log_trace
|
|
from evennia.utils.utils import (variable_from_module, is_iter,
|
|
to_str, to_unicode,
|
|
make_iter, delay,
|
|
callables_from_module)
|
|
from evennia.utils.inlinefuncs import parse_inlinefunc
|
|
|
|
try:
|
|
import cPickle as pickle
|
|
except ImportError:
|
|
import pickle
|
|
|
|
_INLINEFUNC_ENABLED = settings.INLINEFUNC_ENABLED
|
|
|
|
# delayed imports
|
|
_AccountDB = None
|
|
_ServerSession = None
|
|
_ServerConfig = None
|
|
_ScriptDB = None
|
|
_OOB_HANDLER = None
|
|
|
|
|
|
class DummySession(object):
|
|
sessid = 0
|
|
|
|
|
|
DUMMYSESSION = DummySession()
|
|
|
|
# AMP signals
|
|
PCONN = chr(1) # portal session connect
|
|
PDISCONN = chr(2) # portal session disconnect
|
|
PSYNC = chr(3) # portal session sync
|
|
SLOGIN = chr(4) # server session login
|
|
SDISCONN = chr(5) # server session disconnect
|
|
SDISCONNALL = chr(6) # server session disconnect all
|
|
SSHUTD = chr(7) # server shutdown
|
|
SSYNC = chr(8) # server session sync
|
|
SCONN = chr(11) # server portal connection (for bots)
|
|
PCONNSYNC = chr(12) # portal post-syncing session
|
|
PDISCONNALL = chr(13) # portal session discnnect all
|
|
|
|
# i18n
|
|
from django.utils.translation import ugettext as _
|
|
|
|
_SERVERNAME = settings.SERVERNAME
|
|
_MULTISESSION_MODE = settings.MULTISESSION_MODE
|
|
_IDLE_TIMEOUT = settings.IDLE_TIMEOUT
|
|
_DELAY_CMD_LOGINSTART = settings.DELAY_CMD_LOGINSTART
|
|
_MAX_SERVER_COMMANDS_PER_SECOND = 100.0
|
|
_MAX_SESSION_COMMANDS_PER_SECOND = 5.0
|
|
_MODEL_MAP = None
|
|
|
|
# input handlers
|
|
|
|
_INPUT_FUNCS = {}
|
|
for modname in make_iter(settings.INPUT_FUNC_MODULES):
|
|
_INPUT_FUNCS.update(callables_from_module(modname))
|
|
|
|
|
|
def delayed_import():
|
|
"""
|
|
Helper method for delayed import of all needed entities.
|
|
|
|
"""
|
|
global _ServerSession, _AccountDB, _ServerConfig, _ScriptDB
|
|
if not _ServerSession:
|
|
# we allow optional arbitrary serversession class for overloading
|
|
modulename, classname = settings.SERVER_SESSION_CLASS.rsplit(".", 1)
|
|
_ServerSession = variable_from_module(modulename, classname)
|
|
if not _AccountDB:
|
|
from evennia.accounts.models import AccountDB as _AccountDB
|
|
if not _ServerConfig:
|
|
from evennia.server.models import ServerConfig as _ServerConfig
|
|
if not _ScriptDB:
|
|
from evennia.scripts.models import ScriptDB as _ScriptDB
|
|
# including once to avoid warnings in Python syntax checkers
|
|
assert(_ServerSession)
|
|
assert(_AccountDB)
|
|
assert(_ServerConfig)
|
|
assert(_ScriptDB)
|
|
|
|
|
|
#-----------------------------------------------------------
|
|
# SessionHandler base class
|
|
#------------------------------------------------------------
|
|
|
|
class SessionHandler(dict):
|
|
"""
|
|
This handler holds a stack of sessions.
|
|
|
|
"""
|
|
|
|
def __getitem__(self, key):
|
|
"Clean out None-sessions automatically."
|
|
if None in self:
|
|
del self[None]
|
|
return super(SessionHandler, self).__getitem__(key)
|
|
|
|
def get(self, key, default=None):
|
|
"Clean out None-sessions automatically."
|
|
if None in self:
|
|
del self[None]
|
|
return super(SessionHandler, self).get(key, default)
|
|
|
|
def __setitem__(self, key, value):
|
|
"Don't assign None sessions"
|
|
if key is not None:
|
|
super(SessionHandler, self).__setitem__(key, value)
|
|
|
|
def __contains__(self, key):
|
|
"None-keys are not accepted."
|
|
return False if key is None else super(SessionHandler, self).__contains__(key)
|
|
|
|
def get_sessions(self, include_unloggedin=False):
|
|
"""
|
|
Returns the connected session objects.
|
|
|
|
Args:
|
|
include_unloggedin (bool, optional): Also list Sessions
|
|
that have not yet authenticated.
|
|
|
|
Returns:
|
|
sessions (list): A list of `Session` objects.
|
|
|
|
"""
|
|
if include_unloggedin:
|
|
return listvalues(self)
|
|
else:
|
|
return [session for session in self.values() if session.logged_in]
|
|
|
|
def get_all_sync_data(self):
|
|
"""
|
|
Create a dictionary of sessdata dicts representing all
|
|
sessions in store.
|
|
|
|
Returns:
|
|
syncdata (dict): A dict of sync data.
|
|
|
|
"""
|
|
return dict((sessid, sess.get_sync_data()) for sessid, sess in self.items())
|
|
|
|
def clean_senddata(self, session, kwargs):
|
|
"""
|
|
Clean up data for sending across the AMP wire. Also apply INLINEFUNCS.
|
|
|
|
Args:
|
|
session (Session): The relevant session instance.
|
|
kwargs (dict) Each keyword represents a
|
|
send-instruction, with the keyword itself being the name
|
|
of the instruction (like "text"). Suitable values for each
|
|
keyword are:
|
|
- arg -> [[arg], {}]
|
|
- [args] -> [[args], {}]
|
|
- {kwargs} -> [[], {kwargs}]
|
|
- [args, {kwargs}] -> [[arg], {kwargs}]
|
|
- [[args], {kwargs}] -> [[args], {kwargs}]
|
|
|
|
Returns:
|
|
kwargs (dict): A cleaned dictionary of cmdname:[[args],{kwargs}] pairs,
|
|
where the keys, args and kwargs have all been converted to
|
|
send-safe entities (strings or numbers), and inlinefuncs have been
|
|
applied.
|
|
|
|
"""
|
|
options = kwargs.pop("options", None) or {}
|
|
raw = options.get("raw", False)
|
|
strip_inlinefunc = options.get("strip_inlinefunc", False)
|
|
|
|
def _validate(data):
|
|
"Helper function to convert data to AMP-safe (picketable) values"
|
|
if isinstance(data, dict):
|
|
newdict = {}
|
|
for key, part in data.items():
|
|
newdict[key] = _validate(part)
|
|
return newdict
|
|
elif hasattr(data, "__iter__"):
|
|
return [_validate(part) for part in data]
|
|
elif isinstance(data, basestring):
|
|
# make sure strings are in a valid encoding
|
|
try:
|
|
data = data and to_str(to_unicode(data), encoding=session.protocol_flags["ENCODING"])
|
|
except LookupError:
|
|
# wrong encoding set on the session. Set it to a safe one
|
|
session.protocol_flags["ENCODING"] = "utf-8"
|
|
data = to_str(to_unicode(data), encoding=session.protocol_flags["ENCODING"])
|
|
if _INLINEFUNC_ENABLED and not raw and isinstance(self, ServerSessionHandler):
|
|
# only parse inlinefuncs on the outgoing path (sessionhandler->)
|
|
data = parse_inlinefunc(data, strip=strip_inlinefunc, session=session)
|
|
# At this point the object is certainly the right encoding, but may still be a unicode object--
|
|
# to_str does not actually force objects to become bytestrings.
|
|
# If the unicode object is a subclass of unicode, such as ANSIString, this can cause a problem,
|
|
# as special behavior for that class will still be in play. Since we're now transferring raw data,
|
|
# we must now force this to be a proper bytestring.
|
|
return str(data)
|
|
elif hasattr(data, "id") and hasattr(data, "db_date_created") \
|
|
and hasattr(data, '__dbclass__'):
|
|
# convert database-object to their string representation.
|
|
return _validate(unicode(data))
|
|
else:
|
|
return data
|
|
|
|
rkwargs = {}
|
|
for key, data in kwargs.iteritems():
|
|
key = _validate(key)
|
|
if not data:
|
|
if key == "text":
|
|
# we don't allow sending text = None, this must mean
|
|
# that the text command is not to be used.
|
|
continue
|
|
rkwargs[key] = [[], {}]
|
|
elif isinstance(data, dict):
|
|
rkwargs[key] = [[], _validate(data)]
|
|
elif hasattr(data, "__iter__"):
|
|
if isinstance(data[-1], dict):
|
|
if len(data) == 2:
|
|
if hasattr(data[0], "__iter__"):
|
|
rkwargs[key] = [_validate(data[0]), _validate(data[1])]
|
|
else:
|
|
rkwargs[key] = [[_validate(data[0])], _validate(data[1])]
|
|
else:
|
|
rkwargs[key] = [_validate(data[:-1]), _validate(data[-1])]
|
|
else:
|
|
rkwargs[key] = [_validate(data), {}]
|
|
else:
|
|
rkwargs[key] = [[_validate(data)], {}]
|
|
rkwargs[key][1]["options"] = options
|
|
return rkwargs
|
|
|
|
|
|
#------------------------------------------------------------
|
|
# Server-SessionHandler class
|
|
#------------------------------------------------------------
|
|
|
|
class ServerSessionHandler(SessionHandler):
|
|
"""
|
|
This object holds the stack of sessions active in the game at
|
|
any time.
|
|
|
|
A session register with the handler in two steps, first by
|
|
registering itself with the connect() method. This indicates an
|
|
non-authenticated session. Whenever the session is authenticated
|
|
the session together with the related account is sent to the login()
|
|
method.
|
|
|
|
"""
|
|
|
|
# AMP communication methods
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""
|
|
Init the handler.
|
|
|
|
"""
|
|
super(ServerSessionHandler, self).__init__(*args, **kwargs)
|
|
self.server = None
|
|
self.server_data = {"servername": _SERVERNAME}
|
|
|
|
def _run_cmd_login(self, session):
|
|
"""
|
|
Launch the CMD_LOGINSTART command. This is wrapped
|
|
for delays.
|
|
|
|
"""
|
|
if not session.logged_in:
|
|
self.data_in(session, text=[[CMD_LOGINSTART], {}])
|
|
|
|
|
|
def portal_connect(self, portalsessiondata):
|
|
"""
|
|
Called by Portal when a new session has connected.
|
|
Creates a new, unlogged-in game session.
|
|
|
|
Args:
|
|
portalsessiondata (dict): a dictionary of all property:value
|
|
keys defining the session and which is marked to be
|
|
synced.
|
|
|
|
"""
|
|
delayed_import()
|
|
global _ServerSession, _AccountDB, _ScriptDB
|
|
|
|
sess = _ServerSession()
|
|
sess.sessionhandler = self
|
|
sess.load_sync_data(portalsessiondata)
|
|
sess.at_sync()
|
|
# validate all scripts
|
|
_ScriptDB.objects.validate()
|
|
self[sess.sessid] = sess
|
|
|
|
if sess.logged_in and sess.uid:
|
|
# Session is already logged in. This can happen in the
|
|
# case of auto-authenticating protocols like SSH or
|
|
# webclient's session sharing
|
|
account = _AccountDB.objects.get_account_from_uid(sess.uid)
|
|
if account:
|
|
# this will set account.is_connected too
|
|
self.login(sess, account, force=True)
|
|
return
|
|
else:
|
|
sess.logged_in = False
|
|
sess.uid = None
|
|
|
|
# show the first login command, may delay slightly to allow
|
|
# the handshakes to finish.
|
|
delay(_DELAY_CMD_LOGINSTART, self._run_cmd_login, sess)
|
|
|
|
def portal_session_sync(self, portalsessiondata):
|
|
"""
|
|
Called by Portal when it wants to update a single session (e.g.
|
|
because of all negotiation protocols have finally replied)
|
|
|
|
Args:
|
|
portalsessiondata (dict): a dictionary of all property:value
|
|
keys defining the session and which is marked to be
|
|
synced.
|
|
|
|
"""
|
|
sessid = portalsessiondata.get("sessid")
|
|
session = self.get(sessid)
|
|
if session:
|
|
# since some of the session properties may have had
|
|
# a chance to change already before the portal gets here
|
|
# the portal doesn't send all sessiondata but only
|
|
# ones which should only be changed from portal (like
|
|
# protocol_flags etc)
|
|
session.load_sync_data(portalsessiondata)
|
|
|
|
def portal_sessions_sync(self, portalsessionsdata):
|
|
"""
|
|
Syncing all session ids of the portal with the ones of the
|
|
server. This is instantiated by the portal when reconnecting.
|
|
|
|
Args:
|
|
portalsessionsdata (dict): A dictionary
|
|
`{sessid: {property:value},...}` defining each session and
|
|
the properties in it which should be synced.
|
|
|
|
"""
|
|
delayed_import()
|
|
global _ServerSession, _AccountDB, _ServerConfig, _ScriptDB
|
|
|
|
for sess in self.values():
|
|
# we delete the old session to make sure to catch eventual
|
|
# lingering references.
|
|
del sess
|
|
|
|
for sessid, sessdict in portalsessionsdata.items():
|
|
sess = _ServerSession()
|
|
sess.sessionhandler = self
|
|
sess.load_sync_data(sessdict)
|
|
if sess.uid:
|
|
sess.account = _AccountDB.objects.get_account_from_uid(sess.uid)
|
|
self[sessid] = sess
|
|
sess.at_sync()
|
|
|
|
# tell the server hook we synced
|
|
self.server.at_post_portal_sync()
|
|
# announce the reconnection
|
|
self.announce_all(_(" ... Server restarted."))
|
|
|
|
def portal_disconnect(self, session):
|
|
"""
|
|
Called from Portal when Portal session closed from the portal
|
|
side. There is no message to report in this case.
|
|
|
|
Args:
|
|
session (Session): The Session to disconnect
|
|
|
|
"""
|
|
# disconnect us without calling Portal since
|
|
# Portal already knows.
|
|
self.disconnect(session, reason="", sync_portal=False)
|
|
|
|
def portal_disconnect_all(self):
|
|
"""
|
|
Called from Portal when Portal is closing down. All
|
|
Sessions should die. The Portal should not be informed.
|
|
|
|
"""
|
|
# 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:
|
|
session.disconnect()
|
|
del self._disconnect_all
|
|
|
|
# server-side access methods
|
|
|
|
def start_bot_session(self, protocol_path, configdict):
|
|
"""
|
|
This method allows the server-side to force the Portal to
|
|
create a new bot session.
|
|
|
|
Args:
|
|
protocol_path (str): The full python path to the bot's
|
|
class.
|
|
configdict (dict): This dict will be used to configure
|
|
the bot (this depends on the bot protocol).
|
|
|
|
Examples:
|
|
start_bot_session("evennia.server.portal.irc.IRCClient",
|
|
{"uid":1, "botname":"evbot", "channel":"#evennia",
|
|
"network:"irc.freenode.net", "port": 6667})
|
|
|
|
Notes:
|
|
The new session will use the supplied account-bot uid to
|
|
initiate an already logged-in connection. The Portal will
|
|
treat this as a normal connection and henceforth so will
|
|
the Server.
|
|
|
|
"""
|
|
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION, operation=SCONN,
|
|
protocol_path=protocol_path, config=configdict)
|
|
|
|
def portal_shutdown(self):
|
|
"""
|
|
Called by server when shutting down the portal.
|
|
|
|
"""
|
|
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION,
|
|
operation=SSHUTD)
|
|
|
|
def login(self, session, account, force=False, testmode=False):
|
|
"""
|
|
Log in the previously unloggedin session and the account we by
|
|
now should know is connected to it. After this point we assume
|
|
the session to be logged in one way or another.
|
|
|
|
Args:
|
|
session (Session): The Session to authenticate.
|
|
account (Account): The Account identified as associated with this Session.
|
|
force (bool): Login also if the session thinks it's already logged in
|
|
(this can happen for auto-authenticating protocols)
|
|
testmode (bool, optional): This is used by unittesting for
|
|
faking login without any AMP being actually active.
|
|
|
|
"""
|
|
|
|
if session.logged_in and not force:
|
|
# don't log in a session that is already logged in.
|
|
return
|
|
|
|
account.is_connected = True
|
|
|
|
# sets up and assigns all properties on the session
|
|
session.at_login(account)
|
|
|
|
# account init
|
|
account.at_init()
|
|
|
|
# Check if this is the first time the *account* logs in
|
|
if account.db.FIRST_LOGIN:
|
|
account.at_first_login()
|
|
del account.db.FIRST_LOGIN
|
|
|
|
account.at_pre_login()
|
|
|
|
if _MULTISESSION_MODE == 0:
|
|
# disconnect all previous sessions.
|
|
self.disconnect_duplicate_sessions(session)
|
|
|
|
nsess = len(self.sessions_from_account(account))
|
|
string = "Logged in: {account} {address} ({nsessions} session(s) total)"
|
|
string = string.format(account=account, address=session.address, nsessions=nsess)
|
|
session.log(string)
|
|
session.logged_in = True
|
|
# sync the portal to the session
|
|
if not testmode:
|
|
self.server.amp_protocol.send_AdminServer2Portal(session,
|
|
operation=SLOGIN,
|
|
sessiondata={"logged_in": True,
|
|
"uid": session.uid})
|
|
account.at_post_login(session=session)
|
|
|
|
def disconnect(self, session, reason="", sync_portal=True):
|
|
"""
|
|
Called from server side to remove session and inform portal
|
|
of this fact.
|
|
|
|
Args:
|
|
session (Session): The Session to disconnect.
|
|
reason (str, optional): A motivation for the disconnect.
|
|
sync_portal (bool, optional): Sync the disconnect to
|
|
Portal side. This should be done unless this was
|
|
called by self.portal_disconnect().
|
|
|
|
"""
|
|
session = self.get(session.sessid)
|
|
if not session:
|
|
return
|
|
|
|
if hasattr(session, "account") and session.account:
|
|
# only log accounts logging off
|
|
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)
|
|
session.log(string)
|
|
|
|
session.at_disconnect(reason)
|
|
sessid = session.sessid
|
|
if sessid in self and not hasattr(self, "_disconnect_all"):
|
|
del self[sessid]
|
|
if sync_portal:
|
|
# inform portal that session should be closed.
|
|
self.server.amp_protocol.send_AdminServer2Portal(session,
|
|
operation=SDISCONN,
|
|
reason=reason)
|
|
|
|
def all_sessions_portal_sync(self):
|
|
"""
|
|
This is called by the server when it reboots. It syncs all session data
|
|
to the portal. Returns a deferred!
|
|
|
|
"""
|
|
sessdata = self.get_all_sync_data()
|
|
return self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION,
|
|
operation=SSYNC,
|
|
sessiondata=sessdata)
|
|
|
|
def session_portal_sync(self, session):
|
|
"""
|
|
This is called by the server when it wants to sync a single session
|
|
with the Portal for whatever reason. Returns a deferred!
|
|
|
|
"""
|
|
sessdata = {session.sessid: session.get_sync_data()}
|
|
return self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION,
|
|
operation=SSYNC,
|
|
sessiondata=sessdata,
|
|
clean=False)
|
|
|
|
def session_portal_partial_sync(self, session_data):
|
|
"""
|
|
Call to make a partial update of the session, such as only a particular property.
|
|
|
|
Args:
|
|
session_data (dict): Store `{sessid: {property:value}, ...}` defining one or
|
|
more sessions in detail.
|
|
|
|
"""
|
|
return self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION,
|
|
operation=SSYNC,
|
|
sessiondata=session_data,
|
|
clean=False)
|
|
|
|
|
|
|
|
def disconnect_all_sessions(self, reason="You have been disconnected."):
|
|
"""
|
|
Cleanly disconnect all of the connected sessions.
|
|
|
|
Args:
|
|
reason (str, optional): The reason for the disconnection.
|
|
|
|
"""
|
|
|
|
for session in self:
|
|
del session
|
|
# tell portal to disconnect all sessions
|
|
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION,
|
|
operation=SDISCONNALL,
|
|
reason=reason)
|
|
|
|
def disconnect_duplicate_sessions(self, curr_session,
|
|
reason=_("Logged in from elsewhere. Disconnecting.")):
|
|
"""
|
|
Disconnects any existing sessions with the same user.
|
|
|
|
args:
|
|
curr_session (Session): Disconnect all Sessions matching this one.
|
|
reason (str, optional): A motivation for disconnecting.
|
|
|
|
"""
|
|
uid = curr_session.uid
|
|
doublet_sessions = [sess for sess in self.values()
|
|
if sess.logged_in and
|
|
sess.uid == uid and
|
|
sess != curr_session]
|
|
for session in doublet_sessions:
|
|
self.disconnect(session, reason)
|
|
|
|
def validate_sessions(self):
|
|
"""
|
|
Check all currently connected sessions (logged in and not) and
|
|
see if any are dead or idle.
|
|
|
|
"""
|
|
tcurr = time.time()
|
|
reason = _("Idle timeout exceeded, disconnecting.")
|
|
for session in (session for session in self.values()
|
|
if session.logged_in and _IDLE_TIMEOUT > 0 and
|
|
(tcurr - session.cmd_last) > _IDLE_TIMEOUT):
|
|
self.disconnect(session, reason=reason)
|
|
|
|
def account_count(self):
|
|
"""
|
|
Get the number of connected accounts (not sessions since a
|
|
account may have more than one session depending on settings).
|
|
Only logged-in accounts are counted here.
|
|
|
|
Returns:
|
|
naccount (int): Number of connected accounts
|
|
|
|
"""
|
|
return len(set(session.uid for session in self.values() if session.logged_in))
|
|
|
|
def all_connected_accounts(self):
|
|
"""
|
|
Get a unique list of connected and logged-in Accounts.
|
|
|
|
Returns:
|
|
accounts (list): All conected Accounts (which may be fewer than the
|
|
amount of Sessions due to multi-playing).
|
|
|
|
"""
|
|
return list(set(session.account for session in self.values() if session.logged_in and session.account))
|
|
|
|
def session_from_sessid(self, sessid):
|
|
"""
|
|
Get session based on sessid, or None if not found
|
|
|
|
Args:
|
|
sessid (int or list): Session id(s).
|
|
|
|
Return:
|
|
sessions (Session or list): Session(s) found. This
|
|
is a list if input was a list.
|
|
|
|
"""
|
|
if is_iter(sessid):
|
|
return [self.get(sid) for sid in sessid if sid in self]
|
|
return self.get(sessid)
|
|
|
|
def session_from_account(self, account, sessid):
|
|
"""
|
|
Given an account and a session id, return the actual session
|
|
object.
|
|
|
|
Args:
|
|
account (Account): The Account to get the Session from.
|
|
sessid (int or list): Session id(s).
|
|
|
|
Returns:
|
|
sessions (Session or list): Session(s) found.
|
|
|
|
"""
|
|
sessions = [self[sid] for sid in make_iter(sessid)
|
|
if sid in self and self[sid].logged_in and account.uid == self[sid].uid]
|
|
return sessions[0] if len(sessions) == 1 else sessions
|
|
|
|
def sessions_from_account(self, account):
|
|
"""
|
|
Given an account, return all matching sessions.
|
|
|
|
Args:
|
|
account (Account): Account to get sessions from.
|
|
|
|
Returns:
|
|
sessions (list): All Sessions associated with this account.
|
|
|
|
"""
|
|
uid = account.uid
|
|
return [session for session in self.values() if session.logged_in and session.uid == uid]
|
|
|
|
def sessions_from_puppet(self, puppet):
|
|
"""
|
|
Given a puppeted object, return all controlling sessions.
|
|
|
|
Args:
|
|
puppet (Object): Object puppeted
|
|
|
|
Returns.
|
|
sessions (Session or list): Can be more than one of Object is controlled by
|
|
more than one Session (MULTISESSION_MODE > 1).
|
|
|
|
"""
|
|
sessions = puppet.sessid.get()
|
|
return sessions[0] if len(sessions) == 1 else sessions
|
|
sessions_from_character = sessions_from_puppet
|
|
|
|
def sessions_from_csessid(self, csessid):
|
|
"""
|
|
Given a cliend identification hash (for session types that offer them) return all sessions with
|
|
a matching hash.
|
|
|
|
Args
|
|
csessid (str): The session hash
|
|
|
|
"""
|
|
return [session for session in self.values()
|
|
if session.csessid and session.csessid == csessid]
|
|
|
|
def announce_all(self, message):
|
|
"""
|
|
Send message to all connected sessions
|
|
|
|
Args:
|
|
message (str): Message to send.
|
|
|
|
"""
|
|
for session in self.values():
|
|
self.data_out(session, text=message)
|
|
|
|
def data_out(self, session, **kwargs):
|
|
"""
|
|
Sending data Server -> Portal
|
|
|
|
Args:
|
|
session (Session): Session to relay to.
|
|
text (str, optional): text data to return
|
|
|
|
Notes:
|
|
The outdata will be scrubbed for sending across
|
|
the wire here.
|
|
"""
|
|
# clean output for sending
|
|
kwargs = self.clean_senddata(session, kwargs)
|
|
|
|
# send across AMP
|
|
self.server.amp_protocol.send_MsgServer2Portal(session,
|
|
**kwargs)
|
|
|
|
def get_inputfuncs(self):
|
|
"""
|
|
Get all registered inputfuncs (access function)
|
|
|
|
Returns:
|
|
inputfuncs (dict): A dict of {key:inputfunc,...}
|
|
"""
|
|
return _INPUT_FUNCS
|
|
|
|
def data_in(self, session, **kwargs):
|
|
"""
|
|
We let the data take a "detour" to session.data_in
|
|
so the user can override and see it all in one place.
|
|
That method is responsible to in turn always call
|
|
this class' `sessionhandler.call_inputfunc` with the
|
|
(possibly processed) data.
|
|
|
|
"""
|
|
if session:
|
|
session.data_in(**kwargs)
|
|
|
|
def call_inputfuncs(self, session, **kwargs):
|
|
"""
|
|
Split incoming data into its inputfunc counterparts.
|
|
This should be called by the serversession.data_in
|
|
as sessionhandler.call_inputfunc(self, **kwargs).
|
|
|
|
We also intercept OOB communication here.
|
|
|
|
Args:
|
|
sessions (Session): Session.
|
|
|
|
Kwargs:
|
|
kwargs (any): Incoming data from protocol on
|
|
the form `{"commandname": ((args), {kwargs}),...}`
|
|
|
|
"""
|
|
|
|
# distribute incoming data to the correct receiving methods.
|
|
if session:
|
|
input_debug = session.protocol_flags.get("INPUTDEBUG", False)
|
|
for cmdname, (cmdargs, cmdkwargs) in kwargs.iteritems():
|
|
cname = cmdname.strip().lower()
|
|
try:
|
|
cmdkwargs.pop("options", None)
|
|
if cname in _INPUT_FUNCS:
|
|
_INPUT_FUNCS[cname](session, *cmdargs, **cmdkwargs)
|
|
else:
|
|
_INPUT_FUNCS["default"](session, cname, *cmdargs, **cmdkwargs)
|
|
except Exception as err:
|
|
if input_debug:
|
|
session.msg(err)
|
|
log_trace()
|
|
|
|
|
|
SESSION_HANDLER = ServerSessionHandler()
|
|
SESSIONS = SESSION_HANDLER # legacy
|