Format code with black. Add makefile to run fmt/tests

This commit is contained in:
Griatch 2019-09-28 18:18:11 +02:00
parent d00bce9288
commit c2c7fa311a
299 changed files with 19037 additions and 11611 deletions

View file

@ -12,10 +12,11 @@ class ServerConfigAdmin(admin.ModelAdmin):
Custom admin for server configs
"""
list_display = ('db_key', 'db_value')
list_display_links = ('db_key',)
ordering = ['db_key', 'db_value']
search_fields = ['db_key']
list_display = ("db_key", "db_value")
list_display_links = ("db_key",)
ordering = ["db_key", "db_value"]
search_fields = ["db_key"]
save_as = True
save_on_top = True
list_select_related = True

View file

@ -17,6 +17,7 @@ class AMPClientFactory(protocol.ReconnectingClientFactory):
connection error.
"""
# Initial reconnect delay in seconds.
initialDelay = 1
factor = 1.5
@ -95,6 +96,7 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
Portal (which acts as the AMP-server)
"""
# sending AMP data
def connectionMade(self):
@ -108,7 +110,8 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
# first thing we do is to request the Portal to sync all sessions
# back with the Server side. We also need the startup mode (reload, reset, shutdown)
self.send_AdminServer2Portal(
amp.DUMMYSESSION, operation=amp.PSYNC, spid=os.getpid(), info_dict=info_dict)
amp.DUMMYSESSION, operation=amp.PSYNC, spid=os.getpid(), info_dict=info_dict
)
# run the intial setup if needed
self.factory.server.run_initial_setup()
@ -131,7 +134,8 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
"""
# print("server data_to_portal: {}, {}, {}".format(command, sessid, kwargs))
return self.callRemote(command, packed_data=amp.dumps((sessid, kwargs))).addErrback(
self.errback, command.key)
self.errback, command.key
)
def send_MsgServer2Portal(self, session, **kwargs):
"""
@ -158,8 +162,9 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
kwargs (dict, optional): Data going into the adminstrative.
"""
return self.data_to_portal(amp.AdminServer2Portal, session.sessid,
operation=operation, **kwargs)
return self.data_to_portal(
amp.AdminServer2Portal, session.sessid, operation=operation, **kwargs
)
# receiving AMP data
@ -228,18 +233,18 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
elif operation == amp.SRELOAD: # server reload
# shut down in reload mode
server_sessionhandler.all_sessions_portal_sync()
server_sessionhandler.server.shutdown(mode='reload')
server_sessionhandler.server.shutdown(mode="reload")
elif operation == amp.SRESET:
# shut down in reset mode
server_sessionhandler.all_sessions_portal_sync()
server_sessionhandler.server.shutdown(mode='reset')
server_sessionhandler.server.shutdown(mode="reset")
elif operation == amp.SSHUTD: # server shutdown
# shutdown in stop mode
server_sessionhandler.server.shutdown(mode='shutdown')
server_sessionhandler.server.shutdown(mode="shutdown")
else:
raise Exception("operation %(op)s not recognized." % {'op': operation})
raise Exception("operation %(op)s not recognized." % {"op": operation})
return {}

View file

@ -10,7 +10,6 @@ from evennia.utils.utils import list_to_string, mod_import
class ConnectionWizard(object):
def __init__(self):
self.data = {}
self.prev_node = None
@ -132,19 +131,19 @@ class ConnectionWizard(object):
if not resp and default:
resp = str(default)
if resp.lower() in ('q', 'quit'):
if resp.lower() in ("q", "quit"):
sys.exit()
if resp.lower() == 'none':
resp = ''
if resp.lower() == "none":
resp = ""
if validator and not validator(resp):
continue
ok = input("\n Leave blank? [Y]/N: ")
if ok.lower() in ('n', 'no'):
if ok.lower() in ("n", "no"):
continue
elif ok.lower() in ('q', 'quit'):
elif ok.lower() in ("q", "quit"):
sys.exit()
return resp
@ -155,7 +154,7 @@ class ConnectionWizard(object):
ok = input("\n Is this correct? [Y]/N: ")
if ok.lower() in ("n", "no"):
continue
elif ok.lower() in ('q', 'quit'):
elif ok.lower() in ("q", "quit"):
sys.exit()
return resp
@ -170,15 +169,17 @@ def node_start(wizard):
Use `quit` at any time to abort and throw away unsaved changes.
"""
options = {
"1": ("Add your game to the Evennia game index (also for closed-dev games)",
node_game_index_start, {}),
"1": (
"Add your game to the Evennia game index (also for closed-dev games)",
node_game_index_start,
{},
),
# "2": ("Add MSSP information (for mud-list crawlers)",
# node_mssp_start, {}),
# "3": ("Add Grapevine listing",
# node_grapevine_start, {}),
"2": ("View and Save created settings",
node_view_and_apply_settings, {}),
}
"2": ("View and Save created settings", node_view_and_apply_settings, {}),
}
wizard.display(text)
wizard.ask_node(options)
@ -201,7 +202,7 @@ def node_game_index_start(wizard, **kwargs):
"""
wizard.display(text)
if wizard.ask_yesno("Continue adding/editing an Index entry?") == 'yes':
if wizard.ask_yesno("Continue adding/editing an Index entry?") == "yes":
node_game_index_fields(wizard)
else:
node_start(wizard)
@ -215,7 +216,7 @@ def node_game_index_fields(wizard, status=None):
# game status
status_default = wizard.game_index_listing['game_status']
status_default = wizard.game_index_listing["game_status"]
text = f"""
What is the status of your game?
- pre-alpha: a game in its very early stages, mostly unfinished or unstarted
@ -230,12 +231,11 @@ def node_game_index_fields(wizard, status=None):
options = ["pre-alpha", "alpha", "beta", "launched"]
wizard.display(text)
wizard.game_index_listing['game_status'] = \
wizard.ask_choice("Select one: ", options)
wizard.game_index_listing["game_status"] = wizard.ask_choice("Select one: ", options)
# short desc
sdesc_default = wizard.game_index_listing.get('short_description', None)
sdesc_default = wizard.game_index_listing.get("short_description", None)
text = f"""
Enter a short description of your game. Make it snappy and interesting!
@ -256,8 +256,9 @@ def node_game_index_fields(wizard, status=None):
return True
wizard.display(text)
wizard.game_index_listing['short_description'] = \
wizard.ask_input(default=sdesc_default, validator=sdesc_validator)
wizard.game_index_listing["short_description"] = wizard.ask_input(
default=sdesc_default, validator=sdesc_validator
)
# long desc
@ -273,8 +274,7 @@ def node_game_index_fields(wizard, status=None):
"""
wizard.display(text)
wizard.game_index_listing['long_description'] = \
wizard.ask_input(default=long_default)
wizard.game_index_listing["long_description"] = wizard.ask_input(default=long_default)
# listing contact
@ -297,12 +297,13 @@ def node_game_index_fields(wizard, status=None):
return True
wizard.display(text)
wizard.game_index_listing['listing_contact'] = \
wizard.ask_input(default=listing_default, validator=contact_validator)
wizard.game_index_listing["listing_contact"] = wizard.ask_input(
default=listing_default, validator=contact_validator
)
# telnet hostname
hostname_default = wizard.game_index_listing.get('telnet_hostname', None)
hostname_default = wizard.game_index_listing.get("telnet_hostname", None)
text = f"""
Enter the hostname to which third-party telnet mud clients can connect to
your game. This would be the name of the server your game is hosted on,
@ -315,13 +316,11 @@ def node_game_index_fields(wizard, status=None):
"""
wizard.display(text)
wizard.game_index_listing['telnet_hostname'] = \
wizard.ask_input(default=hostname_default)
wizard.game_index_listing["telnet_hostname"] = wizard.ask_input(default=hostname_default)
# telnet port
port_default = wizard.game_index_listing.get('telnet_port', None)
port_default = wizard.game_index_listing.get("telnet_port", None)
text = f"""
Enter the main telnet port. The Evennia default is 4000. You can change
this with the TELNET_PORTS server setting.
@ -333,13 +332,11 @@ def node_game_index_fields(wizard, status=None):
"""
wizard.display(text)
wizard.game_index_listing['telnet_port'] = \
wizard.ask_input(default=port_default)
wizard.game_index_listing["telnet_port"] = wizard.ask_input(default=port_default)
# website
website_default = wizard.game_index_listing.get('game_website', None)
website_default = wizard.game_index_listing.get("game_website", None)
text = f"""
Evennia is its own web server and runs your game's website. Enter the
URL of the website here, like http://yourwebsite.com, here.
@ -351,12 +348,11 @@ def node_game_index_fields(wizard, status=None):
"""
wizard.display(text)
wizard.game_index_listing['game_website'] = \
wizard.ask_input(default=website_default)
wizard.game_index_listing["game_website"] = wizard.ask_input(default=website_default)
# webclient
webclient_default = wizard.game_index_listing.get('web_client_url', None)
webclient_default = wizard.game_index_listing.get("web_client_url", None)
text = f"""
Evennia offers its own native webclient. Normally it will be found from the
game homepage at something like http://yourwebsite.com/webclient. Enter
@ -370,15 +366,17 @@ def node_game_index_fields(wizard, status=None):
"""
wizard.display(text)
wizard.game_index_listing['web_client_url'] = \
wizard.ask_input(default=webclient_default)
wizard.game_index_listing["web_client_url"] = wizard.ask_input(default=webclient_default)
if not (wizard.game_index_listing.get('web_client_url') or
(wizard.game_index_listing.get('telnet_host'))):
if not (
wizard.game_index_listing.get("web_client_url")
or (wizard.game_index_listing.get("telnet_host"))
):
wizard.display(
"\nNote: You have not specified any connection options. This means "
"your game \nwill be marked as being in 'closed development' in "
"the index.")
"the index."
)
wizard.display("\nDon't forget to inspect and save your changes.")
@ -417,6 +415,7 @@ def node_mssp_start(wizard):
# Admin
def _save_changes(wizard):
"""
Perform the save
@ -425,22 +424,25 @@ def _save_changes(wizard):
# add import statement to settings file
import_stanza = "from .connection_settings import *"
setting_module = mod_import("server.conf.settings")
with open(setting_module.__file__, 'r+') as f:
with open(setting_module.__file__, "r+") as f:
txt = f.read() # moves pointer to end of file
if import_stanza not in txt:
# add to the end of the file
f.write("\n\n"
"try:\n"
" # Created by the `evennia connections` wizard\n"
f" {import_stanza}\n"
"except ImportError:\n"
" pass")
f.write(
"\n\n"
"try:\n"
" # Created by the `evennia connections` wizard\n"
f" {import_stanza}\n"
"except ImportError:\n"
" pass"
)
connect_settings_file = path.join(settings.GAME_DIR,
"server", "conf", "connection_settings.py")
with open(connect_settings_file, 'w') as f:
f.write("# This file is auto-generated by the `evennia connections` wizard.\n"
"# Don't edit manually, your changes will be overwritten.\n\n")
connect_settings_file = path.join(settings.GAME_DIR, "server", "conf", "connection_settings.py")
with open(connect_settings_file, "w") as f:
f.write(
"# This file is auto-generated by the `evennia connections` wizard.\n"
"# Don't edit manually, your changes will be overwritten.\n\n"
)
f.write(wizard.save_output)
wizard.display(f"saving to {connect_settings_file} ...")
@ -459,9 +461,9 @@ def node_view_and_apply_settings(wizard):
if wizard.game_index_listing != settings.GAME_INDEX_LISTING:
game_index_txt = "No changes to save for Game Index."
else:
game_index_txt = ("GAME_INDEX_ENABLED = True\n"
"GAME_INDEX_LISTING = \\\n" +
pp.pformat(wizard.game_index_listing))
game_index_txt = "GAME_INDEX_ENABLED = True\n" "GAME_INDEX_LISTING = \\\n" + pp.pformat(
wizard.game_index_listing
)
saves = True
text = game_index_txt
@ -469,7 +471,7 @@ def node_view_and_apply_settings(wizard):
wizard.display(f"Settings to save:\n\n{text}")
if saves:
if wizard.ask_yesno("Do you want to save these settings?") == 'yes':
if wizard.ask_yesno("Do you want to save these settings?") == "yes":
wizard.save_output = text
_save_changes(wizard)
wizard.display("... saved!")

View file

@ -18,31 +18,32 @@ def check_errors(settings):
DeprecationWarning if a critical deprecation is found.
"""
deprstring = ("settings.%s should be renamed to %s. If defaults are used, "
"their path/classname must be updated "
"(see evennia/settings_default.py).")
deprstring = (
"settings.%s should be renamed to %s. If defaults are used, "
"their path/classname must be updated "
"(see evennia/settings_default.py)."
)
if hasattr(settings, "CMDSET_DEFAULT"):
raise DeprecationWarning(deprstring % (
"CMDSET_DEFAULT", "CMDSET_CHARACTER"))
raise DeprecationWarning(deprstring % ("CMDSET_DEFAULT", "CMDSET_CHARACTER"))
if hasattr(settings, "CMDSET_OOC"):
raise DeprecationWarning(deprstring % ("CMDSET_OOC", "CMDSET_ACCOUNT"))
if settings.WEBSERVER_ENABLED and not isinstance(settings.WEBSERVER_PORTS[0], tuple):
raise DeprecationWarning(
"settings.WEBSERVER_PORTS must be on the form "
"[(proxyport, serverport), ...]")
"settings.WEBSERVER_PORTS must be on the form " "[(proxyport, serverport), ...]"
)
if hasattr(settings, "BASE_COMM_TYPECLASS"):
raise DeprecationWarning(deprstring % (
"BASE_COMM_TYPECLASS", "BASE_CHANNEL_TYPECLASS"))
raise DeprecationWarning(deprstring % ("BASE_COMM_TYPECLASS", "BASE_CHANNEL_TYPECLASS"))
if hasattr(settings, "COMM_TYPECLASS_PATHS"):
raise DeprecationWarning(deprstring % (
"COMM_TYPECLASS_PATHS", "CHANNEL_TYPECLASS_PATHS"))
raise DeprecationWarning(deprstring % ("COMM_TYPECLASS_PATHS", "CHANNEL_TYPECLASS_PATHS"))
if hasattr(settings, "CHARACTER_DEFAULT_HOME"):
raise DeprecationWarning(
"settings.CHARACTER_DEFAULT_HOME should be renamed to "
"DEFAULT_HOME. See also settings.START_LOCATION "
"(see evennia/settings_default.py).")
deprstring = ("settings.%s is now merged into settings.TYPECLASS_PATHS. "
"Update your settings file.")
"(see evennia/settings_default.py)."
)
deprstring = (
"settings.%s is now merged into settings.TYPECLASS_PATHS. " "Update your settings file."
)
if hasattr(settings, "OBJECT_TYPECLASS_PATHS"):
raise DeprecationWarning(deprstring % "OBJECT_TYPECLASS_PATHS")
if hasattr(settings, "SCRIPT_TYPECLASS_PATHS"):
@ -56,31 +57,45 @@ def check_errors(settings):
"settings.SEARCH_MULTIMATCH_SEPARATOR was replaced by "
"SEARCH_MULTIMATCH_REGEX and SEARCH_MULTIMATCH_TEMPLATE. "
"Update your settings file (see evennia/settings_default.py "
"for more info).")
"for more info)."
)
gametime_deprecation = ("The settings TIME_SEC_PER_MIN, TIME_MIN_PER_HOUR,"
"TIME_HOUR_PER_DAY, TIME_DAY_PER_WEEK, \n"
"TIME_WEEK_PER_MONTH and TIME_MONTH_PER_YEAR "
"are no longer supported. Remove them from your "
"settings file to continue.\nIf you want to use "
"and manipulate these time units, the tools from utils.gametime "
"are now found in contrib/convert_gametime.py instead.")
if any(hasattr(settings, value) for value in ("TIME_SEC_PER_MIN", "TIME_MIN_PER_HOUR",
"TIME_HOUR_PER_DAY", "TIME_DAY_PER_WEEK",
"TIME_WEEK_PER_MONTH",
"TIME_MONTH_PER_YEAR")):
gametime_deprecation = (
"The settings TIME_SEC_PER_MIN, TIME_MIN_PER_HOUR,"
"TIME_HOUR_PER_DAY, TIME_DAY_PER_WEEK, \n"
"TIME_WEEK_PER_MONTH and TIME_MONTH_PER_YEAR "
"are no longer supported. Remove them from your "
"settings file to continue.\nIf you want to use "
"and manipulate these time units, the tools from utils.gametime "
"are now found in contrib/convert_gametime.py instead."
)
if any(
hasattr(settings, value)
for value in (
"TIME_SEC_PER_MIN",
"TIME_MIN_PER_HOUR",
"TIME_HOUR_PER_DAY",
"TIME_DAY_PER_WEEK",
"TIME_WEEK_PER_MONTH",
"TIME_MONTH_PER_YEAR",
)
):
raise DeprecationWarning(gametime_deprecation)
game_directory_deprecation = ("The setting GAME_DIRECTORY_LISTING was removed. It must be "
"renamed to GAME_INDEX_LISTING instead.")
game_directory_deprecation = (
"The setting GAME_DIRECTORY_LISTING was removed. It must be "
"renamed to GAME_INDEX_LISTING instead."
)
if hasattr(settings, "GAME_DIRECTORY_LISTING"):
raise DeprecationWarning(game_directory_deprecation)
chan_connectinfo = settings.CHANNEL_CONNECTINFO
if chan_connectinfo is not None and not isinstance(chan_connectinfo, dict):
raise DeprecationWarning("settings.CHANNEL_CONNECTINFO has changed. It "
"must now be either None or a dict "
"specifying the properties of the channel to create.")
raise DeprecationWarning(
"settings.CHANNEL_CONNECTINFO has changed. It "
"must now be either None or a dict "
"specifying the properties of the channel to create."
)
def check_warnings(settings):

File diff suppressed because it is too large Load diff

View file

@ -36,8 +36,8 @@ EVENNIA_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
EVENNIA_BIN = os.path.join(EVENNIA_ROOT, "bin")
EVENNIA_LIB = os.path.dirname(evennia.__file__)
SERVER_PY_FILE = os.path.join(EVENNIA_LIB, 'server', 'server.py')
PORTAL_PY_FILE = os.path.join(EVENNIA_LIB, 'server', 'portal', 'portal.py')
SERVER_PY_FILE = os.path.join(EVENNIA_LIB, "server", "server.py")
PORTAL_PY_FILE = os.path.join(EVENNIA_LIB, "server", "portal", "portal.py")
GAMEDIR = None
SERVERDIR = "server"
@ -53,8 +53,7 @@ SPROFILER_LOGFILE = None
# messages
CMDLINE_HELP = \
"""
CMDLINE_HELP = """
This program manages the running Evennia processes. It is called
by evennia and should not be started manually. Its main task is to
sit and watch the Server and restart it whenever the user reloads.
@ -63,13 +62,11 @@ CMDLINE_HELP = \
are stored in the game's server/ directory.
"""
PROCESS_ERROR = \
"""
PROCESS_ERROR = """
{component} process error: {traceback}.
"""
PROCESS_IOERROR = \
"""
PROCESS_IOERROR = """
{component} IOError: {traceback}
One possible explanation is that 'twistd' was not found.
"""
@ -85,7 +82,7 @@ def set_restart_mode(restart_file, flag="reload"):
"""
This sets a flag file for the restart mode.
"""
with open(restart_file, 'w') as f:
with open(restart_file, "w") as f:
f.write(str(flag))
@ -96,7 +93,7 @@ def getenv():
sep = ";" if os.name == "nt" else ":"
env = os.environ.copy()
sys.path.insert(0, GAMEDIR)
env['PYTHONPATH'] = sep.join(sys.path)
env["PYTHONPATH"] = sep.join(sys.path)
return env
@ -105,7 +102,7 @@ def get_restart_mode(restart_file):
Parse the server/portal restart status
"""
if os.path.exists(restart_file):
with open(restart_file, 'r') as f:
with open(restart_file, "r") as f:
return f.read()
return "shutdown"
@ -117,7 +114,7 @@ def get_pid(pidfile):
"""
pid = None
if os.path.exists(pidfile):
with open(pidfile, 'r') as f:
with open(pidfile, "r") as f:
pid = f.read()
return pid
@ -126,7 +123,7 @@ def cycle_logfile(logfile):
"""
Rotate the old log files to <filename>.old
"""
logfile_old = logfile + '.old'
logfile_old = logfile + ".old"
if os.path.exists(logfile):
# Cycle the old logfiles to *.old
if os.path.exists(logfile_old):
@ -134,6 +131,7 @@ def cycle_logfile(logfile):
os.remove(logfile_old)
os.rename(logfile, logfile_old)
# Start program management
@ -167,7 +165,7 @@ def start_services(server_argv, portal_argv, doexit=False):
try:
if not doexit and get_restart_mode(PORTAL_RESTART) == "True":
# start portal as interactive, reloadable thread
PORTAL = _thread.start_new_thread(portal_waiter, (processes, ))
PORTAL = _thread.start_new_thread(portal_waiter, (processes,))
else:
# normal operation: start portal as a daemon;
# we don't care to monitor it for restart
@ -182,7 +180,7 @@ def start_services(server_argv, portal_argv, doexit=False):
SERVER = Popen(server_argv, env=getenv())
else:
# start server as a reloadable thread
SERVER = _thread.start_new_thread(server_waiter, (processes, ))
SERVER = _thread.start_new_thread(server_waiter, (processes,))
except IOError as e:
print(PROCESS_IOERROR.format(component="Server", traceback=e))
return
@ -196,6 +194,7 @@ def start_services(server_argv, portal_argv, doexit=False):
# this blocks until something is actually returned.
from twisted.internet.error import ReactorNotRunning
try:
try:
message, rc = processes.get()
@ -204,17 +203,23 @@ def start_services(server_argv, portal_argv, doexit=False):
break
# restart only if process stopped cleanly
if (message == "server_stopped" and int(rc) == 0 and
get_restart_mode(SERVER_RESTART) in ("True", "reload", "reset")):
if (
message == "server_stopped"
and int(rc) == 0
and get_restart_mode(SERVER_RESTART) in ("True", "reload", "reset")
):
print(PROCESS_RESTART.format(component="Server"))
SERVER = _thread.start_new_thread(server_waiter, (processes, ))
SERVER = _thread.start_new_thread(server_waiter, (processes,))
continue
# normally the portal is not reloaded since it's run as a daemon.
if (message == "portal_stopped" and int(rc) == 0 and
get_restart_mode(PORTAL_RESTART) == "True"):
if (
message == "portal_stopped"
and int(rc) == 0
and get_restart_mode(PORTAL_RESTART) == "True"
):
print(PROCESS_RESTART.format(component="Portal"))
PORTAL = _thread.start_new_thread(portal_waiter, (processes, ))
PORTAL = _thread.start_new_thread(portal_waiter, (processes,))
continue
break
except ReactorNotRunning:
@ -228,29 +233,66 @@ def main():
"""
parser = ArgumentParser(description=CMDLINE_HELP)
parser.add_argument('--noserver', action='store_true', dest='noserver',
default=False, help='Do not start Server process')
parser.add_argument('--noportal', action='store_true', dest='noportal',
default=False, help='Do not start Portal process')
parser.add_argument('--logserver', action='store_true', dest='logserver',
default=False, help='Log Server output to logfile')
parser.add_argument('--iserver', action='store_true', dest='iserver',
default=False, help='Server in interactive mode')
parser.add_argument('--iportal', action='store_true', dest='iportal',
default=False, help='Portal in interactive mode')
parser.add_argument('--pserver', action='store_true', dest='pserver',
default=False, help='Profile Server')
parser.add_argument('--pportal', action='store_true', dest='pportal',
default=False, help='Profile Portal')
parser.add_argument('--nologcycle', action='store_false', dest='nologcycle',
default=True, help='Do not cycle log files')
parser.add_argument('--doexit', action='store_true', dest='doexit',
default=False, help='Immediately exit after processes have started.')
parser.add_argument('gamedir', help="path to game dir")
parser.add_argument('twistdbinary', help="path to twistd binary")
parser.add_argument('slogfile', help="path to server log file")
parser.add_argument('plogfile', help="path to portal log file")
parser.add_argument('hlogfile', help="path to http log file")
parser.add_argument(
"--noserver",
action="store_true",
dest="noserver",
default=False,
help="Do not start Server process",
)
parser.add_argument(
"--noportal",
action="store_true",
dest="noportal",
default=False,
help="Do not start Portal process",
)
parser.add_argument(
"--logserver",
action="store_true",
dest="logserver",
default=False,
help="Log Server output to logfile",
)
parser.add_argument(
"--iserver",
action="store_true",
dest="iserver",
default=False,
help="Server in interactive mode",
)
parser.add_argument(
"--iportal",
action="store_true",
dest="iportal",
default=False,
help="Portal in interactive mode",
)
parser.add_argument(
"--pserver", action="store_true", dest="pserver", default=False, help="Profile Server"
)
parser.add_argument(
"--pportal", action="store_true", dest="pportal", default=False, help="Profile Portal"
)
parser.add_argument(
"--nologcycle",
action="store_false",
dest="nologcycle",
default=True,
help="Do not cycle log files",
)
parser.add_argument(
"--doexit",
action="store_true",
dest="doexit",
default=False,
help="Immediately exit after processes have started.",
)
parser.add_argument("gamedir", help="path to game dir")
parser.add_argument("twistdbinary", help="path to twistd binary")
parser.add_argument("slogfile", help="path to server log file")
parser.add_argument("plogfile", help="path to portal log file")
parser.add_argument("hlogfile", help="path to http log file")
args = parser.parse_args()
@ -275,30 +317,32 @@ def main():
PPROFILER_LOGFILE = os.path.join(GAMEDIR, SERVERDIR, "logs", "portal.prof")
# set up default project calls
server_argv = [TWISTED_BINARY,
'--nodaemon',
'--logfile=%s' % SERVER_LOGFILE,
'--pidfile=%s' % SERVER_PIDFILE,
'--python=%s' % SERVER_PY_FILE]
portal_argv = [TWISTED_BINARY,
'--logfile=%s' % PORTAL_LOGFILE,
'--pidfile=%s' % PORTAL_PIDFILE,
'--python=%s' % PORTAL_PY_FILE]
server_argv = [
TWISTED_BINARY,
"--nodaemon",
"--logfile=%s" % SERVER_LOGFILE,
"--pidfile=%s" % SERVER_PIDFILE,
"--python=%s" % SERVER_PY_FILE,
]
portal_argv = [
TWISTED_BINARY,
"--logfile=%s" % PORTAL_LOGFILE,
"--pidfile=%s" % PORTAL_PIDFILE,
"--python=%s" % PORTAL_PY_FILE,
]
# Profiling settings (read file from python shell e.g with
# p = pstats.Stats('server.prof')
pserver_argv = ['--savestats',
'--profiler=cprofile',
'--profile=%s' % SPROFILER_LOGFILE]
pportal_argv = ['--savestats',
'--profiler=cprofile',
'--profile=%s' % PPROFILER_LOGFILE]
pserver_argv = ["--savestats", "--profiler=cprofile", "--profile=%s" % SPROFILER_LOGFILE]
pportal_argv = ["--savestats", "--profiler=cprofile", "--profile=%s" % PPROFILER_LOGFILE]
# Server
pid = get_pid(SERVER_PIDFILE)
if pid and not args.noserver:
print("\nEvennia Server is already running as process %(pid)s. Not restarted." % {'pid': pid})
print(
"\nEvennia Server is already running as process %(pid)s. Not restarted." % {"pid": pid}
)
args.noserver = True
if args.noserver:
server_argv = None
@ -320,14 +364,16 @@ def main():
pid = get_pid(PORTAL_PIDFILE)
if pid and not args.noportal:
print("\nEvennia Portal is already running as process %(pid)s. Not restarted." % {'pid': pid})
print(
"\nEvennia Portal is already running as process %(pid)s. Not restarted." % {"pid": pid}
)
args.noportal = True
if args.noportal:
portal_argv = None
else:
if args.iportal:
# make portal interactive
portal_argv[1] = '--nodaemon'
portal_argv[1] = "--nodaemon"
set_restart_mode(PORTAL_RESTART, True)
print("\nStarting Evennia Portal in non-Daemon mode (output to stdout).")
else:
@ -343,7 +389,7 @@ def main():
print(PROCESS_DOEXIT)
# Windows fixes (Windows don't support pidfiles natively)
if os.name == 'nt':
if os.name == "nt":
if server_argv:
del server_argv[-2]
if portal_argv:
@ -353,5 +399,5 @@ def main():
start_services(server_argv, portal_argv, doexit=args.doexit)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View file

@ -21,8 +21,8 @@ from evennia.accounts.models import AccountDB
from evennia.server.sessionhandler import SESSIONS
from evennia.utils import get_evennia_version, logger
_EGI_HOST = 'http://evennia-game-index.appspot.com'
_EGI_REPORT_PATH = '/api/v1/game/check_in'
_EGI_HOST = "http://evennia-game-index.appspot.com"
_EGI_REPORT_PATH = "/api/v1/game/check_in"
class EvenniaGameIndexClient(object):
@ -56,15 +56,14 @@ class EvenniaGameIndexClient(object):
status_code, response_body = yield self._form_and_send_request()
if status_code == 200:
if not self.logged_first_connect:
logger.log_infomsg(
"Successfully sent game details to Evennia Game Index.")
logger.log_infomsg("Successfully sent game details to Evennia Game Index.")
self.logged_first_connect = True
return
# At this point, either EGD is having issues or the payload we sent
# is improperly formed (probably due to mis-configuration).
logger.log_errmsg(
'Failed to send game details to Evennia Game Index. HTTP '
'status code was %s. Message was: %s' % (status_code, response_body)
"Failed to send game details to Evennia Game Index. HTTP "
"status code was %s. Message was: %s" % (status_code, response_body)
)
if status_code == 400 and self._on_bad_request:
@ -80,8 +79,8 @@ class EvenniaGameIndexClient(object):
"""
agent = Agent(reactor, pool=self._conn_pool)
headers = {
b'User-Agent': [b'Evennia Game Index Client'],
b'Content-Type': [b'application/x-www-form-urlencoded'],
b"User-Agent": [b"Evennia Game Index Client"],
b"Content-Type": [b"application/x-www-form-urlencoded"],
}
egi_config = settings.GAME_INDEX_LISTING
# We are using `or` statements below with dict.get() to avoid sending
@ -89,27 +88,24 @@ class EvenniaGameIndexClient(object):
try:
values = {
# Game listing stuff
'game_name': egi_config.get('game_name', settings.SERVERNAME),
'game_status': egi_config['game_status'],
'game_website': egi_config.get('game_website', ''),
'short_description': egi_config['short_description'],
'long_description': egi_config.get('long_description', ''),
'listing_contact': egi_config['listing_contact'],
"game_name": egi_config.get("game_name", settings.SERVERNAME),
"game_status": egi_config["game_status"],
"game_website": egi_config.get("game_website", ""),
"short_description": egi_config["short_description"],
"long_description": egi_config.get("long_description", ""),
"listing_contact": egi_config["listing_contact"],
# How to play
'telnet_hostname': egi_config.get('telnet_hostname', ''),
'telnet_port': egi_config.get('telnet_port', ''),
'web_client_url': egi_config.get('web_client_url', ''),
"telnet_hostname": egi_config.get("telnet_hostname", ""),
"telnet_port": egi_config.get("telnet_port", ""),
"web_client_url": egi_config.get("web_client_url", ""),
# Game stats
'connected_account_count': SESSIONS.account_count(),
'total_account_count': AccountDB.objects.num_total_accounts() or 0,
"connected_account_count": SESSIONS.account_count(),
"total_account_count": AccountDB.objects.num_total_accounts() or 0,
# System info
'evennia_version': get_evennia_version(),
'python_version': platform.python_version(),
'django_version': django.get_version(),
'server_platform': platform.platform(),
"evennia_version": get_evennia_version(),
"python_version": platform.python_version(),
"django_version": django.get_version(),
"server_platform": platform.platform(),
}
except KeyError as err:
raise KeyError(f"Error loading GAME_INDEX_LISTING: {err}")
@ -117,16 +113,18 @@ class EvenniaGameIndexClient(object):
data = urllib.parse.urlencode(values)
d = agent.request(
b'POST', bytes(self.report_url, 'utf-8'),
b"POST",
bytes(self.report_url, "utf-8"),
headers=Headers(headers),
bodyProducer=StringProducer(data))
bodyProducer=StringProducer(data),
)
d.addCallback(self.handle_egd_response)
return d
def handle_egd_response(self, response):
if 200 <= response.code < 300:
d = defer.succeed((response.code, 'OK'))
d = defer.succeed((response.code, "OK"))
else:
# Go through the horrifying process of getting the response body
# out of Twisted's plumbing.
@ -142,7 +140,7 @@ class SimpleResponseReceiver(protocol.Protocol):
def __init__(self, status_code, d):
self.status_code = status_code
self.buf = ''
self.buf = ""
self.d = d
def dataReceived(self, data):
@ -159,7 +157,7 @@ class StringProducer(object):
"""
def __init__(self, body):
self.body = bytes(body, 'utf-8')
self.body = bytes(body, "utf-8")
self.length = len(body)
def startProducing(self, consumer):
@ -177,4 +175,5 @@ class QuietHTTP11ClientFactory(_HTTP11ClientFactory):
"""
Silences the obnoxious factory start/stop messages in the default client.
"""
noisy = False

View file

@ -21,13 +21,13 @@ class EvenniaGameIndexService(Service):
to the Evennia Game Index.
"""
# We didn't stick the Evennia prefix on here because it'd get marked as
# a core system service.
name = 'GameIndexClient'
name = "GameIndexClient"
def __init__(self):
self.client = EvenniaGameIndexClient(
on_bad_request=self._die_on_bad_request)
self.client = EvenniaGameIndexClient(on_bad_request=self._die_on_bad_request)
self.loop = LoopingCall(self.client.send_game_details)
def startService(self):
@ -36,8 +36,7 @@ class EvenniaGameIndexService(Service):
# Start the loop, but only after a short delay. This allows the
# portal and the server time to sync up as far as total player counts.
# Prevents always reporting a count of 0.
reactor.callLater(
_FIRST_UPDATE_DELAY, self.loop.start, _CLIENT_UPDATE_RATE)
reactor.callLater(_FIRST_UPDATE_DELAY, self.loop.start, _CLIENT_UPDATE_RATE)
def stopService(self):
if self.running == 0:
@ -54,6 +53,6 @@ class EvenniaGameIndexService(Service):
Stop the service so we're not wasting resources.
"""
logger.log_infomsg(
"Shutting down Evennia Game Index client service due to "
"invalid configuration.")
"Shutting down Evennia Game Index client service due to " "invalid configuration."
)
self.stopService()

View file

@ -25,11 +25,13 @@ ERROR_NO_SUPERUSER = """
"""
LIMBO_DESC = _("""
LIMBO_DESC = _(
"""
Welcome to your new |wEvennia|n-based game! Visit http://www.evennia.com if you need
help, want to contribute, report issues or just join the community.
As Account #1 you can create a demo/tutorial area with |w@batchcommand tutorial_world.build|n.
""")
"""
)
WARNING_POSTGRESQL_FIX = """
@ -74,7 +76,9 @@ def create_objects():
god_account.swap_typeclass(account_typeclass, clean_attributes=True)
god_account.basetype_setup()
god_account.at_account_creation()
god_account.locks.add("examine:perm(Developer);edit:false();delete:false();boot:false();msg:all()")
god_account.locks.add(
"examine:perm(Developer);edit:false();delete:false();boot:false();msg:all()"
)
# this is necessary for quelling to work correctly.
god_account.permissions.add("Developer")
@ -83,14 +87,14 @@ def create_objects():
# Create the in-game god-character for account #1 and set
# it to exist in Limbo.
character_typeclass = settings.BASE_CHARACTER_TYPECLASS
god_character = create.create_object(character_typeclass,
key=god_account.username,
nohome=True)
god_character = create.create_object(character_typeclass, key=god_account.username, nohome=True)
god_character.id = 1
god_character.save()
god_character.db.desc = _('This is User #1.')
god_character.locks.add("examine:perm(Developer);edit:false();delete:false();boot:false();msg:all();puppet:false()")
god_character.db.desc = _("This is User #1.")
god_character.locks.add(
"examine:perm(Developer);edit:false();delete:false();boot:false();msg:all();puppet:false()"
)
god_character.permissions.add("Developer")
god_account.attributes.add("_first_login", True)
@ -102,7 +106,7 @@ def create_objects():
god_account.db_playable_characters = [god_character]
room_typeclass = settings.BASE_ROOM_TYPECLASS
limbo_obj = create.create_object(room_typeclass, _('Limbo'), nohome=True)
limbo_obj = create.create_object(room_typeclass, _("Limbo"), nohome=True)
limbo_obj.id = 2
limbo_obj.save()
limbo_obj.db.desc = LIMBO_DESC.strip()
@ -166,8 +170,9 @@ def collectstatic():
"""
from django.core.management import call_command
logger.log_info("Initial setup: Gathering static resources using 'collectstatic'")
call_command('collectstatic', '--noinput')
call_command("collectstatic", "--noinput")
def reset_server():
@ -180,6 +185,7 @@ def reset_server():
"""
ServerConfig.objects.conf("server_epoch", time.time())
from evennia.server.sessionhandler import SESSIONS
logger.log_info("Initial setup complete. Restarting Server once.")
SESSIONS.portal_reset_server()
@ -204,11 +210,7 @@ def handle_setup(last_step):
last_step = last_step or 0
# setting up the list of functions to run
setup_queue = [create_objects,
create_channels,
at_initial_setup,
collectstatic,
reset_server]
setup_queue = [create_objects, create_channels, at_initial_setup, collectstatic, reset_server]
# step through queue, from last completed function
for num, setup_func in enumerate(setup_queue[last_step:]):
@ -221,10 +223,12 @@ def handle_setup(last_step):
except Exception:
if last_step + num == 1:
from evennia.objects.models import ObjectDB
for obj in ObjectDB.objects.all():
obj.delete()
elif last_step + num == 2:
from evennia.comms.models import ChannelDB
ChannelDB.objects.all().delete()
raise
# save this step

View file

@ -33,7 +33,7 @@ BrowserSessionStore = importlib.import_module(settings.SESSION_ENGINE).SessionSt
# always let "idle" work since we use this in the webclient
_IDLE_COMMAND = settings.IDLE_COMMAND
_IDLE_COMMAND = (_IDLE_COMMAND, ) if _IDLE_COMMAND == "idle" else (_IDLE_COMMAND, "idle")
_IDLE_COMMAND = (_IDLE_COMMAND,) if _IDLE_COMMAND == "idle" else (_IDLE_COMMAND, "idle")
_GA = object.__getattribute__
_SA = object.__setattr__
@ -47,6 +47,7 @@ _ERROR_INPUT = "Inputfunc {name}({session}): Wrong/unrecognized input: {inp}"
# All global functions are inputfuncs available to process inputs
def text(session, *args, **kwargs):
"""
Main text input from the client. This will execute a command
@ -58,8 +59,8 @@ def text(session, *args, **kwargs):
arguments are ignored.
"""
#from evennia.server.profiling.timetrace import timetrace
#text = timetrace(text, "ServerSession.data_in")
# from evennia.server.profiling.timetrace import timetrace
# text = timetrace(text, "ServerSession.data_in")
txt = args[0] if args else None
@ -76,11 +77,13 @@ def text(session, *args, **kwargs):
# nick replacement
puppet = session.puppet
if puppet:
txt = puppet.nicks.nickreplace(txt,
categories=("inputline", "channel"), include_account=True)
txt = puppet.nicks.nickreplace(
txt, categories=("inputline", "channel"), include_account=True
)
else:
txt = session.account.nicks.nickreplace(txt,
categories=("inputline", "channel"), include_account=False)
txt = session.account.nicks.nickreplace(
txt, categories=("inputline", "channel"), include_account=False
)
kwargs.pop("options", None)
cmdhandler(session, txt, callertype="session", session=session, **kwargs)
session.update_session_counters()
@ -127,20 +130,33 @@ def default(session, cmdname, *args, **kwargs):
it will get `cmdname` as the first argument.
"""
err = "Session {sessid}: Input command not recognized:\n" \
" name: '{cmdname}'\n" \
" args, kwargs: {args}, {kwargs}".format(sessid=session.sessid,
cmdname=cmdname,
args=args,
kwargs=kwargs)
err = (
"Session {sessid}: Input command not recognized:\n"
" name: '{cmdname}'\n"
" args, kwargs: {args}, {kwargs}".format(
sessid=session.sessid, cmdname=cmdname, args=args, kwargs=kwargs
)
)
if session.protocol_flags.get("INPUTDEBUG", False):
session.msg(err)
log_err(err)
_CLIENT_OPTIONS = \
("ANSI", "XTERM256", "MXP", "UTF-8", "SCREENREADER", "ENCODING", "MCCP",
"SCREENHEIGHT", "SCREENWIDTH", "INPUTDEBUG", "RAW", "NOCOLOR", "NOGOAHEAD")
_CLIENT_OPTIONS = (
"ANSI",
"XTERM256",
"MXP",
"UTF-8",
"SCREENREADER",
"ENCODING",
"MCCP",
"SCREENHEIGHT",
"SCREENWIDTH",
"INPUTDEBUG",
"RAW",
"NOCOLOR",
"NOGOAHEAD",
)
def client_options(session, *args, **kwargs):
@ -169,8 +185,7 @@ def client_options(session, *args, **kwargs):
old_flags = session.protocol_flags
if not kwargs or kwargs.get("get", False):
# return current settings
options = dict((key, old_flags[key]) for key in old_flags
if key.upper() in _CLIENT_OPTIONS)
options = dict((key, old_flags[key]) for key in old_flags if key.upper() in _CLIENT_OPTIONS)
session.msg(client_options=options)
return
@ -224,19 +239,23 @@ def client_options(session, *args, **kwargs):
flags["RAW"] = validate_bool(value)
elif key == "nogoahead":
flags["NOGOAHEAD"] = validate_bool(value)
elif key in ('Char 1', 'Char.Skills 1', 'Char.Items 1',
'Room 1', 'IRE.Rift 1', 'IRE.Composer 1'):
elif key in (
"Char 1",
"Char.Skills 1",
"Char.Items 1",
"Room 1",
"IRE.Rift 1",
"IRE.Composer 1",
):
# ignore mudlet's default send (aimed at IRE games)
pass
elif key not in ("options", "cmdid"):
err = _ERROR_INPUT.format(
name="client_settings", session=session, inp=key)
err = _ERROR_INPUT.format(name="client_settings", session=session, inp=key)
session.msg(text=err)
session.protocol_flags.update(flags)
# we must update the protocol flags on the portal session copy as well
session.sessionhandler.session_portal_partial_sync(
{session.sessid: {"protocol_flags": flags}})
session.sessionhandler.session_portal_partial_sync({session.sessid: {"protocol_flags": flags}})
def get_client_options(session, *args, **kwargs):
@ -252,8 +271,9 @@ def get_inputfuncs(session, *args, **kwargs):
it from this module alone since multiple modules could be added.
So we get it from the sessionhandler.
"""
inputfuncsdict = dict((key, func.__doc__) for key, func
in session.sessionhandler.get_inputfuncs().items())
inputfuncsdict = dict(
(key, func.__doc__) for key, func in session.sessionhandler.get_inputfuncs().items()
)
session.msg(get_inputfuncs=inputfuncsdict)
@ -269,6 +289,7 @@ def login(session, *args, **kwargs):
"""
if not session.logged_in and "name" in kwargs and "password" in kwargs:
from evennia.commands.default.unloggedin import create_normal_account
account = create_normal_account(session, kwargs["name"], kwargs["password"])
if account:
session.sessionhandler.login(session, account)
@ -278,7 +299,7 @@ _gettable = {
"name": lambda obj: obj.key,
"key": lambda obj: obj.key,
"location": lambda obj: obj.location.key if obj.location else "None",
"servername": lambda obj: settings.SERVERNAME
"servername": lambda obj: settings.SERVERNAME,
}
@ -308,11 +329,11 @@ def _testrepeat(**kwargs):
session (Session): Session to return to.
"""
import time
kwargs["session"].msg(repeat="Repeat called: %s" % time.time())
_repeatable = {"test1": _testrepeat, # example only
"test2": _testrepeat} # "
_repeatable = {"test1": _testrepeat, "test2": _testrepeat} # example only # "
def repeat(session, *args, **kwargs):
@ -333,14 +354,23 @@ def repeat(session, *args, **kwargs):
"""
from evennia.scripts.tickerhandler import TICKER_HANDLER
name = kwargs.get("callback", "")
interval = max(5, int(kwargs.get("interval", 60)))
if name in _repeatable:
if kwargs.get("stop", False):
TICKER_HANDLER.remove(interval, _repeatable[name], idstring=session.sessid, persistent=False)
TICKER_HANDLER.remove(
interval, _repeatable[name], idstring=session.sessid, persistent=False
)
else:
TICKER_HANDLER.add(interval, _repeatable[name], idstring=session.sessid, persistent=False, session=session)
TICKER_HANDLER.add(
interval,
_repeatable[name],
idstring=session.sessid,
persistent=False,
session=session,
)
else:
session.msg("Allowed repeating functions are: %s" % (", ".join(_repeatable)))
@ -351,11 +381,7 @@ def unrepeat(session, *args, **kwargs):
repeat(session, *args, **kwargs)
_monitorable = {
"name": "db_key",
"location": "db_location",
"desc": "desc"
}
_monitorable = {"name": "db_key", "location": "db_location", "desc": "desc"}
def _on_monitor_change(**kwargs):
@ -363,7 +389,7 @@ def _on_monitor_change(**kwargs):
obj = kwargs["obj"]
name = kwargs["name"]
session = kwargs["session"]
outputfunc_name = kwargs['outputfunc_name']
outputfunc_name = kwargs["outputfunc_name"]
# the session may be None if the char quits and someone
# else then edits the object
@ -389,6 +415,7 @@ def monitor(session, *args, **kwargs):
"""
from evennia.scripts.monitorhandler import MONITOR_HANDLER
name = kwargs.get("name", None)
outputfunc_name = kwargs("outputfunc_name", "monitor")
if name and name in _monitorable and session.puppet:
@ -398,9 +425,16 @@ def monitor(session, *args, **kwargs):
MONITOR_HANDLER.remove(obj, field_name, idstring=session.sessid)
else:
# the handler will add fieldname and obj to the kwargs automatically
MONITOR_HANDLER.add(obj, field_name, _on_monitor_change, idstring=session.sessid,
persistent=False, name=name, session=session,
outputfunc_name=outputfunc_name)
MONITOR_HANDLER.add(
obj,
field_name,
_on_monitor_change,
idstring=session.sessid,
persistent=False,
name=name,
session=session,
outputfunc_name=outputfunc_name,
)
def unmonitor(session, *args, **kwargs):
@ -417,6 +451,7 @@ def monitored(session, *args, **kwargs):
"""
from evennia.scripts.monitorhandler import MONITOR_HANDLER
obj = session.puppet
monitors = MONITOR_HANDLER.all(obj=obj)
session.msg(monitored=(monitors, {}))
@ -476,10 +511,15 @@ def webclient_options(session, *args, **kwargs):
# Create a monitor. If a monitor already exists then it will replace
# the previous one since it would use the same idstring
from evennia.scripts.monitorhandler import MONITOR_HANDLER
MONITOR_HANDLER.add(account, "_saved_webclient_options",
_on_webclient_options_change,
idstring=session.sessid, persistent=False,
session=session)
MONITOR_HANDLER.add(
account,
"_saved_webclient_options",
_on_webclient_options_change,
idstring=session.sessid,
persistent=False,
session=session,
)
else:
# kwargs provided: persist them to the account object
for key, value in kwargs.items():
@ -504,14 +544,28 @@ def msdp_list(session, *args, **kwargs):
"""
from evennia.scripts.monitorhandler import MONITOR_HANDLER
args_lower = [arg.lower() for arg in args]
if "commands" in args_lower:
inputfuncs = [key[5:] if key.startswith("msdp_") else key
for key in session.sessionhandler.get_inputfuncs().keys()]
inputfuncs = [
key[5:] if key.startswith("msdp_") else key
for key in session.sessionhandler.get_inputfuncs().keys()
]
session.msg(commands=(inputfuncs, {}))
if "lists" in args_lower:
session.msg(lists=(['commands', 'lists', 'configurable_variables', 'reportable_variables',
'reported_variables', 'sendable_variables'], {}))
session.msg(
lists=(
[
"commands",
"lists",
"configurable_variables",
"reportable_variables",
"reported_variables",
"sendable_variables",
],
{},
)
)
if "configurable_variables" in args_lower:
session.msg(configurable_variables=(_CLIENT_OPTIONS, {}))
if "reportable_variables" in args_lower:
@ -531,7 +585,7 @@ def msdp_report(session, *args, **kwargs):
MSDP REPORT command
"""
kwargs['outputfunc_name': 'report']
kwargs["outputfunc_name":"report"]
monitor(session, *args, **kwargs)
@ -545,6 +599,7 @@ def msdp_unreport(session, *args, **kwargs):
# client specific
def external_discord_hello(session, *args, **kwargs):
"""
Sent by Mudlet as a greeting; added here to avoid

View file

@ -6,21 +6,25 @@ from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
]
dependencies = []
operations = [
migrations.CreateModel(
name='ServerConfig',
name="ServerConfig",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('db_key', models.CharField(unique=True, max_length=64)),
('db_value', models.TextField(blank=True)),
(
"id",
models.AutoField(
verbose_name="ID", serialize=False, auto_created=True, primary_key=True
),
),
("db_key", models.CharField(unique=True, max_length=64)),
("db_value", models.TextField(blank=True)),
],
options={
'verbose_name': 'Server Config value',
'verbose_name_plural': 'Server Config values',
"verbose_name": "Server Config value",
"verbose_name_plural": "Server Config values",
},
bases=(models.Model,),
),
)
]

View file

@ -10,26 +10,27 @@ from copy import deepcopy
def forwards(apps, schema_editor):
ServerConfig = apps.get_model('server', 'ServerConfig')
ServerConfig = apps.get_model("server", "ServerConfig")
for conf in ServerConfig.objects.all():
# picklefield requires base64 encoding
value = loads(to_bytes(conf.db_value), encoding='bytes') # py2->py3 byte encoding
value = loads(to_bytes(conf.db_value), encoding="bytes") # py2->py3 byte encoding
conf.db_value = b64encode(dumps(deepcopy(value), protocol=4)).decode()
conf.save(update_fields=['db_value'])
conf.save(update_fields=["db_value"])
class Migration(migrations.Migration):
dependencies = [
('server', '0001_initial'),
]
dependencies = [("server", "0001_initial")]
operations = [
migrations.RunPython(forwards, migrations.RunPython.noop),
migrations.AlterField(
model_name='serverconfig',
name='db_value',
field=evennia.utils.picklefield.PickledObjectField(help_text='The data returned when the config value is accessed. Must be written as a Python literal if editing through the admin interface. Attribute values which are not Python literals cannot be edited through the admin interface.', null=True, verbose_name='value'),
model_name="serverconfig",
name="db_value",
field=evennia.utils.picklefield.PickledObjectField(
help_text="The data returned when the config value is accessed. Must be written as a Python literal if editing through the admin interface. Attribute values which are not Python literals cannot be edited through the admin interface.",
null=True,
verbose_name="value",
),
),
]

View file

@ -18,11 +18,12 @@ from evennia.server.manager import ServerConfigManager
from evennia.utils import picklefield
#------------------------------------------------------------
# ------------------------------------------------------------
#
# ServerConfig
#
#------------------------------------------------------------
# ------------------------------------------------------------
class ServerConfig(WeakSharedMemoryModel):
"""
@ -48,11 +49,13 @@ class ServerConfig(WeakSharedMemoryModel):
# db_value = models.BinaryField(blank=True)
db_value = picklefield.PickledObjectField(
'value', null=True,
"value",
null=True,
help_text="The data returned when the config value is accessed. Must be "
"written as a Python literal if editing through the admin "
"interface. Attribute values which are not Python literals "
"cannot be edited through the admin interface.")
"written as a Python literal if editing through the admin "
"interface. Attribute values which are not Python literals "
"cannot be edited through the admin interface.",
)
objects = ServerConfigManager()
_is_deleted = False
@ -66,43 +69,45 @@ class ServerConfig(WeakSharedMemoryModel):
# is the object in question).
# key property (wraps db_key)
#@property
# @property
def __key_get(self):
"Getter. Allows for value = self.key"
return self.db_key
#@key.setter
# @key.setter
def __key_set(self, value):
"Setter. Allows for self.key = value"
self.db_key = value
self.save()
#@key.deleter
# @key.deleter
def __key_del(self):
"Deleter. Allows for del self.key. Deletes entry."
self.delete()
key = property(__key_get, __key_set, __key_del)
# value property (wraps db_value)
#@property
# @property
def __value_get(self):
"Getter. Allows for value = self.value"
return from_pickle(self.db_value, db_obj=self)
#@value.setter
# @value.setter
def __value_set(self, value):
"Setter. Allows for self.value = value"
if utils.has_parent('django.db.models.base.Model', value):
if utils.has_parent("django.db.models.base.Model", value):
# we have to protect against storing db objects.
logger.log_err("ServerConfig cannot store db objects! (%s)" % value)
return
self.db_value = to_pickle(value)
self.save()
#@value.deleter
# @value.deleter
def __value_del(self):
"Deleter. Allows for del self.value. Deletes entry."
self.delete()
value = property(__value_get, __value_set, __value_del)
class Meta(object):

View file

@ -23,28 +23,28 @@ _LOGGER = None
# communication bits
# (chr(9) and chr(10) are \t and \n, so skipping them)
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 creating new connection (for irc bots and etc)
PCONNSYNC = chr(12) # portal post-syncing a session
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 creating new connection (for irc bots and etc)
PCONNSYNC = chr(12) # portal post-syncing a session
PDISCONNALL = chr(13) # portal session disconnect all
SRELOAD = chr(14) # server shutdown in reload mode
SSTART = chr(15) # server start (portal must already be running anyway)
PSHUTD = chr(16) # portal (+server) shutdown
SSHUTD = chr(17) # server shutdown
PSTATUS = chr(18) # ping server or portal status
SRESET = chr(19) # server shutdown in reset mode
SRELOAD = chr(14) # server shutdown in reload mode
SSTART = chr(15) # server start (portal must already be running anyway)
PSHUTD = chr(16) # portal (+server) shutdown
SSHUTD = chr(17) # server shutdown
PSTATUS = chr(18) # ping server or portal status
SRESET = chr(19) # server shutdown in reset mode
NUL = b'\x00'
NULNUL = b'\x00\x00'
NUL = b"\x00"
NULNUL = b"\x00\x00"
AMP_MAXLEN = amp.MAX_VALUE_LENGTH # max allowed data length in AMP protocol (cannot be changed)
AMP_MAXLEN = amp.MAX_VALUE_LENGTH # max allowed data length in AMP protocol (cannot be changed)
# buffers
_SENDBATCH = defaultdict(list)
@ -52,10 +52,11 @@ _MSGBUFFER = defaultdict(list)
# resources
DUMMYSESSION = namedtuple('DummySession', ['sessid'])(0)
DUMMYSESSION = namedtuple("DummySession", ["sessid"])(0)
_HTTP_WARNING = bytes("""
_HTTP_WARNING = bytes(
"""
HTTP/1.1 200 OK
Content-Type: text/html
@ -67,11 +68,14 @@ Content-Type: text/html
<h3>This port should NOT be publicly visible.</h3>
</p>
</body>
</html>""".strip(), 'utf-8')
</html>""".strip(),
"utf-8",
)
# Helper functions for pickling.
def dumps(data):
return pickle.dumps(data, pickle.HIGHEST_PROTOCOL)
@ -91,6 +95,7 @@ def _get_logger():
@wraps
def catch_traceback(func):
"Helper decorator"
def decorator(*args, **kwargs):
try:
func(*args, **kwargs)
@ -98,11 +103,13 @@ def catch_traceback(func):
_get_logger().log_trace()
raise # make sure the error is visible on the other side of the connection too
print(err)
return decorator
# AMP Communication Command types
class Compressed(amp.String):
"""
This is a custom AMP command Argument that both handles too-long
@ -127,7 +134,7 @@ class Compressed(amp.String):
if chunk is None:
break
value.write(self.fromStringProto(chunk, proto))
objects[str(name, 'utf-8')] = value.getvalue()
objects[str(name, "utf-8")] = value.getvalue()
def toBox(self, name, strings, objects, proto):
"""
@ -138,7 +145,7 @@ class Compressed(amp.String):
# print("toBox: name={}, strings={}, objects={}, proto{}".format(name, strings, objects, proto))
value = BytesIO(objects[str(name, 'utf-8')])
value = BytesIO(objects[str(name, "utf-8")])
strings[name] = self.toStringProto(value.read(AMP_MAXLEN), proto)
# print("toBox strings[name] = {}".format(strings[name]))
@ -171,10 +178,10 @@ class MsgLauncher2Portal(amp.Command):
Message Launcher -> Portal
"""
key = "MsgLauncher2Portal"
arguments = [(b'operation', amp.String()),
(b'arguments', amp.String())]
errors = {Exception: b'EXCEPTION'}
arguments = [(b"operation", amp.String()), (b"arguments", amp.String())]
errors = {Exception: b"EXCEPTION"}
response = []
@ -183,9 +190,10 @@ class MsgPortal2Server(amp.Command):
Message Portal -> Server
"""
key = b"MsgPortal2Server"
arguments = [(b'packed_data', Compressed())]
errors = {Exception: b'EXCEPTION'}
arguments = [(b"packed_data", Compressed())]
errors = {Exception: b"EXCEPTION"}
response = []
@ -194,9 +202,10 @@ class MsgServer2Portal(amp.Command):
Message Server -> Portal
"""
key = "MsgServer2Portal"
arguments = [(b'packed_data', Compressed())]
errors = {Exception: b'EXCEPTION'}
arguments = [(b"packed_data", Compressed())]
errors = {Exception: b"EXCEPTION"}
response = []
@ -208,9 +217,10 @@ class AdminPortal2Server(amp.Command):
server, such as when a new session connects or resyncs
"""
key = "AdminPortal2Server"
arguments = [(b'packed_data', Compressed())]
errors = {Exception: b'EXCEPTION'}
arguments = [(b"packed_data", Compressed())]
errors = {Exception: b"EXCEPTION"}
response = []
@ -222,9 +232,10 @@ class AdminServer2Portal(amp.Command):
portal.
"""
key = "AdminServer2Portal"
arguments = [(b'packed_data', Compressed())]
errors = {Exception: b'EXCEPTION'}
arguments = [(b"packed_data", Compressed())]
errors = {Exception: b"EXCEPTION"}
response = []
@ -233,10 +244,11 @@ class MsgStatus(amp.Command):
Check Status between AMP services
"""
key = "MsgStatus"
arguments = [(b'status', amp.String())]
errors = {Exception: b'EXCEPTION'}
response = [(b'status', amp.String())]
arguments = [(b"status", amp.String())]
errors = {Exception: b"EXCEPTION"}
response = [(b"status", amp.String())]
class FunctionCall(amp.Command):
@ -247,19 +259,23 @@ class FunctionCall(amp.Command):
the other. This does not use the batch-send functionality.
"""
key = "FunctionCall"
arguments = [(b'module', amp.String()),
(b'function', amp.String()),
(b'args', amp.String()),
(b'kwargs', amp.String())]
errors = {Exception: b'EXCEPTION'}
response = [(b'result', amp.String())]
arguments = [
(b"module", amp.String()),
(b"function", amp.String()),
(b"args", amp.String()),
(b"kwargs", amp.String()),
]
errors = {Exception: b"EXCEPTION"}
response = [(b"result", amp.String())]
# -------------------------------------------------------------
# Core AMP protocol for communication Server <-> Portal
# -------------------------------------------------------------
class AMPMultiConnectionProtocol(amp.AMP):
"""
AMP protocol that safely handle multiple connections to the same
@ -364,8 +380,11 @@ class AMPMultiConnectionProtocol(amp.AMP):
"""
e.trap(Exception)
_get_logger().log_err("AMP Error for {info}: {trcbck} {err}".format(
info=info, trcbck=e.getTraceback(), err=e.getErrorMessage()))
_get_logger().log_err(
"AMP Error for {info}: {trcbck} {err}".format(
info=info, trcbck=e.getTraceback(), err=e.getErrorMessage()
)
)
def data_in(self, packed_data):
"""
@ -400,8 +419,9 @@ class AMPMultiConnectionProtocol(amp.AMP):
# print("broadcast: {} {}: {}".format(command, sessid, kwargs))
for protcl in self.factory.broadcasts:
deferreds.append(protcl.callRemote(command, **kwargs).addErrback(
self.errback, command.key))
deferreds.append(
protcl.callRemote(command, **kwargs).addErrback(self.errback, command.key)
)
return DeferredList(deferreds)
@ -423,12 +443,17 @@ class AMPMultiConnectionProtocol(amp.AMP):
function call
"""
return self.callRemote(FunctionCall,
module=modulepath,
function=functionname,
args=dumps(args),
kwargs=dumps(kwargs)).addCallback(
lambda r: loads(r["result"])).addErrback(self.errback, "FunctionCall")
return (
self.callRemote(
FunctionCall,
module=modulepath,
function=functionname,
args=dumps(args),
kwargs=dumps(kwargs),
)
.addCallback(lambda r: loads(r["result"]))
.addErrback(self.errback, "FunctionCall")
)
@FunctionCall.responder
@catch_traceback
@ -459,4 +484,4 @@ class AMPMultiConnectionProtocol(amp.AMP):
result.addCallback(lambda r: {"result": dumps(r)})
return result
else:
return {'result': dumps(result)}
return {"result": dumps(result)}

View file

@ -14,7 +14,7 @@ from evennia.utils import logger
def _is_windows():
return os.name == 'nt'
return os.name == "nt"
def getenv():
@ -27,7 +27,7 @@ def getenv():
"""
sep = ";" if _is_windows() else ":"
env = os.environ.copy()
env['PYTHONPATH'] = sep.join(sys.path)
env["PYTHONPATH"] = sep.join(sys.path)
return env
@ -38,6 +38,7 @@ class AMPServerFactory(protocol.ServerFactory):
'Server' process.
"""
noisy = False
def logPrefix(self):
@ -83,6 +84,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
Protocol subclass for the AMP-server run by the Portal.
"""
def connectionLost(self, reason):
"""
Set up a simple callback mechanism to let the amp-server wait for a connection to close.
@ -112,8 +114,9 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
(portal_live, server_live, portal_PID, server_PID).
"""
server_connected = bool(self.factory.server_connection and
self.factory.server_connection.transport.connected)
server_connected = bool(
self.factory.server_connection and self.factory.server_connection.transport.connected
)
portal_info_dict = self.factory.portal.get_info_dict()
server_info_dict = self.factory.portal.server_info_dict
server_pid = self.factory.portal.server_process_id
@ -140,8 +143,8 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
# print("portal data_to_server: {}, {}, {}".format(command, sessid, kwargs))
if self.factory.server_connection:
return self.factory.server_connection.callRemote(
command, packed_data=amp.dumps((sessid, kwargs))).addErrback(
self.errback, command.key)
command, packed_data=amp.dumps((sessid, kwargs))
).addErrback(self.errback, command.key)
else:
# if no server connection is available, broadcast
return self.broadcast(command, sessid, packed_data=amp.dumps((sessid, kwargs)))
@ -158,7 +161,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
# start the Server
print("Portal starting server ... {}".format(server_twistd_cmd))
process = None
with open(settings.SERVER_LOG_FILE, 'a') as logfile:
with open(settings.SERVER_LOG_FILE, "a") as logfile:
# we link stdout to a file in order to catch
# eventual errors happening before the Server has
# opened its logger.
@ -166,13 +169,19 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
if _is_windows():
# Windows requires special care
create_no_window = 0x08000000
process = Popen(server_twistd_cmd, env=getenv(), bufsize=-1,
stdout=logfile, stderr=STDOUT,
creationflags=create_no_window)
process = Popen(
server_twistd_cmd,
env=getenv(),
bufsize=-1,
stdout=logfile,
stderr=STDOUT,
creationflags=create_no_window,
)
else:
process = Popen(server_twistd_cmd, env=getenv(), bufsize=-1,
stdout=logfile, stderr=STDOUT)
process = Popen(
server_twistd_cmd, env=getenv(), bufsize=-1, stdout=logfile, stderr=STDOUT
)
except Exception:
logger.log_trace()
@ -205,7 +214,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
"""
self.factory.server_connect_callbacks.append((callback, args, kwargs))
def stop_server(self, mode='shutdown'):
def stop_server(self, mode="shutdown"):
"""
Shut down server in one or more modes.
@ -213,11 +222,11 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
mode (str): One of 'shutdown', 'reload' or 'reset'.
"""
if mode == 'reload':
if mode == "reload":
self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRELOAD)
elif mode == 'reset':
elif mode == "reset":
self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRESET)
elif mode == 'shutdown':
elif mode == "shutdown":
self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SSHUTD)
self.factory.portal.server_restart_mode = mode
@ -232,9 +241,8 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
# print("self.get_status(): {}".format(self.get_status()))
if self.factory.launcher_connection:
self.factory.launcher_connection.callRemote(
amp.MsgStatus,
status=amp.dumps(self.get_status())).addErrback(
self.errback, amp.MsgStatus.key)
amp.MsgStatus, status=amp.dumps(self.get_status())
).addErrback(self.errback, amp.MsgStatus.key)
def send_MsgPortal2Server(self, session, **kwargs):
"""
@ -262,8 +270,9 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
data (str or dict, optional): Data used in the administrative operation.
"""
return self.data_to_server(amp.AdminPortal2Server, session.sessid,
operation=operation, **kwargs)
return self.data_to_server(
amp.AdminPortal2Server, session.sessid, operation=operation, **kwargs
)
# receive amp data
@ -303,7 +312,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
"""
# Since the launcher command uses amp.String() we need to convert from byte here.
operation = str(operation, 'utf-8')
operation = str(operation, "utf-8")
self.factory.launcher_connection = self
_, server_connected, _, _, _, _ = self.get_status()
@ -311,7 +320,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
# logger.log_msg("operation == amp.SSTART: {}: {}".format(operation == amp.SSTART, amp.loads(arguments)))
if operation == amp.SSTART: # portal start #15
if operation == amp.SSTART: # portal start #15
# first, check if server is already running
if not server_connected:
self.wait_for_server_connect(self.send_Status2Launcher)
@ -320,38 +329,34 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
elif operation == amp.SRELOAD: # reload server #14
if server_connected:
# We let the launcher restart us once they get the signal
self.factory.server_connection.wait_for_disconnect(
self.send_Status2Launcher)
self.stop_server(mode='reload')
self.factory.server_connection.wait_for_disconnect(self.send_Status2Launcher)
self.stop_server(mode="reload")
else:
self.wait_for_server_connect(self.send_Status2Launcher)
self.start_server(amp.loads(arguments))
elif operation == amp.SRESET: # reload server #19
if server_connected:
self.factory.server_connection.wait_for_disconnect(
self.send_Status2Launcher)
self.stop_server(mode='reset')
self.factory.server_connection.wait_for_disconnect(self.send_Status2Launcher)
self.stop_server(mode="reset")
else:
self.wait_for_server_connect(self.send_Status2Launcher)
self.start_server(amp.loads(arguments))
elif operation == amp.SSHUTD: # server-only shutdown #17
if server_connected:
self.factory.server_connection.wait_for_disconnect(
self.send_Status2Launcher)
self.stop_server(mode='shutdown')
self.factory.server_connection.wait_for_disconnect(self.send_Status2Launcher)
self.stop_server(mode="shutdown")
elif operation == amp.PSHUTD: # portal + server shutdown #16
if server_connected:
self.factory.server_connection.wait_for_disconnect(
self.factory.portal.shutdown )
self.factory.server_connection.wait_for_disconnect(self.factory.portal.shutdown)
else:
self.factory.portal.shutdown()
else:
logger.log_err("Operation {} not recognized".format(operation))
raise Exception("operation %(op)s not recognized." % {'op': operation})
raise Exception("operation %(op)s not recognized." % {"op": operation})
return {}
@ -413,22 +418,23 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
portal_sessionhandler.server_disconnect_all(reason=kwargs.get("reason"))
elif operation == amp.SRELOAD: # server reload
self.factory.server_connection.wait_for_disconnect(
self.start_server, self.factory.portal.server_twistd_cmd)
self.stop_server(mode='reload')
self.factory.server_connection.wait_for_disconnect(
self.start_server, self.factory.portal.server_twistd_cmd
)
self.stop_server(mode="reload")
elif operation == amp.SRESET: # server reset
self.factory.server_connection.wait_for_disconnect(
self.start_server, self.factory.portal.server_twistd_cmd)
self.stop_server(mode='reset')
self.factory.server_connection.wait_for_disconnect(
self.start_server, self.factory.portal.server_twistd_cmd
)
self.stop_server(mode="reset")
elif operation == amp.SSHUTD: # server-only shutdown
self.stop_server(mode='shutdown')
self.stop_server(mode="shutdown")
elif operation == amp.PSHUTD: # full server+server shutdown
self.factory.server_connection.wait_for_disconnect(
self.factory.portal.shutdown)
self.stop_server(mode='shutdown')
self.factory.server_connection.wait_for_disconnect(self.factory.portal.shutdown)
self.stop_server(mode="shutdown")
elif operation == amp.PSYNC: # portal sync
# Server has (re-)connected and wants the session data from portal
@ -438,11 +444,13 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
server_restart_mode = self.factory.portal.server_restart_mode
sessdata = self.factory.portal.sessions.get_all_sync_data()
self.send_AdminPortal2Server(amp.DUMMYSESSION,
amp.PSYNC,
server_restart_mode=server_restart_mode,
sessiondata=sessdata,
portal_start_time=self.factory.portal.start_time)
self.send_AdminPortal2Server(
amp.DUMMYSESSION,
amp.PSYNC,
server_restart_mode=server_restart_mode,
sessiondata=sessdata,
portal_start_time=self.factory.portal.start_time,
)
self.factory.portal.sessions.at_server_connection()
if self.factory.server_connection:
@ -458,8 +466,9 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
elif operation == amp.SSYNC: # server_session_sync
# server wants to save session data to the portal,
# maybe because it's about to shut down.
portal_sessionhandler.server_session_sync(kwargs.get("sessiondata"),
kwargs.get("clean", True))
portal_sessionhandler.server_session_sync(
kwargs.get("sessiondata"), kwargs.get("clean", True)
)
# set a flag in case we are about to shut down soon
self.factory.server_restart_mode = True
@ -468,5 +477,5 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
portal_sessionhandler.server_connect(**kwargs)
else:
raise Exception("operation %(op)s not recognized." % {'op': operation})
raise Exception("operation %(op)s not recognized." % {"op": operation})
return {}

View file

@ -15,8 +15,7 @@ from django.conf import settings
from evennia.server.session import Session
from evennia.utils import get_evennia_version
from evennia.utils.logger import log_info, log_err
from autobahn.twisted.websocket import (
WebSocketClientProtocol, WebSocketClientFactory, connectWS)
from autobahn.twisted.websocket import WebSocketClientProtocol, WebSocketClientFactory, connectWS
# There is only one at this time
GRAPEVINE_URI = "wss://grapevine.haus/socket"
@ -31,8 +30,7 @@ GRAPEVINE_AUTH_ERROR = 4000
GRAPEVINE_HEARTBEAT_FAILURE = 4001
class RestartingWebsocketServerFactory(WebSocketClientFactory,
protocol.ReconnectingClientFactory):
class RestartingWebsocketServerFactory(WebSocketClientFactory, protocol.ReconnectingClientFactory):
"""
A variant of the websocket-factory that auto-reconnects.
@ -44,8 +42,8 @@ class RestartingWebsocketServerFactory(WebSocketClientFactory,
def __init__(self, sessionhandler, *args, **kwargs):
self.uid = kwargs.pop('uid')
self.channel = kwargs.pop('grapevine_channel')
self.uid = kwargs.pop("uid")
self.channel = kwargs.pop("grapevine_channel")
self.sessionhandler = sessionhandler
# self.noisy = False
@ -170,7 +168,7 @@ class GrapevineClient(WebSocketClientProtocol, Session):
"""
if not isBinary:
data = json.loads(str(payload, 'utf-8'))
data = json.loads(str(payload, "utf-8"))
self.data_in(data=data)
self.retry_task = None
@ -205,7 +203,7 @@ class GrapevineClient(WebSocketClientProtocol, Session):
data (str): Text to send.
"""
return self.sendMessage(json.dumps(data).encode('utf-8'))
return self.sendMessage(json.dumps(data).encode("utf-8"))
def disconnect(self, reason=None):
"""
@ -238,9 +236,9 @@ class GrapevineClient(WebSocketClientProtocol, Session):
"supports": ["channels"],
"channels": GRAPEVINE_CHANNELS,
"version": "1.0.0",
"user_agent": get_evennia_version('pretty')
}
}
"user_agent": get_evennia_version("pretty"),
},
}
# override on-the-fly
data.update(kwargs)
@ -252,14 +250,11 @@ class GrapevineClient(WebSocketClientProtocol, Session):
"""
# pass along all connected players
data = {
"event": "heartbeat",
"payload": {
}
}
data = {"event": "heartbeat", "payload": {}}
sessions = self.sessionhandler.get_sessions(include_unloggedin=False)
data['payload']['players'] = [sess.account.key for sess in sessions
if hasattr(sess, "account")]
data["payload"]["players"] = [
sess.account.key for sess in sessions if hasattr(sess, "account")
]
self._send_json(data)
@ -269,12 +264,7 @@ class GrapevineClient(WebSocketClientProtocol, Session):
Use with session.msg(subscribe="channelname")
"""
data = {
"event": "channels/subscribe",
"payload": {
"channel": channelname
}
}
data = {"event": "channels/subscribe", "payload": {"channel": channelname}}
self._send_json(data)
def send_unsubscribe(self, channelname, *args, **kwargs):
@ -283,12 +273,7 @@ class GrapevineClient(WebSocketClientProtocol, Session):
Use with session.msg(unsubscribe="channelname")
"""
data = {
"event": "channels/unsubscribe",
"payload": {
"channel": channelname
}
}
data = {"event": "channels/unsubscribe", "payload": {"channel": channelname}}
self._send_json(data)
def send_channel(self, text, channel, sender, *args, **kwargs):
@ -303,11 +288,7 @@ class GrapevineClient(WebSocketClientProtocol, Session):
data = {
"event": "channels/send",
"payload": {
"message": text,
"channel": channel,
"name": sender
}
"payload": {"message": text, "channel": channel, "name": sender},
}
self._send_json(data)
@ -326,10 +307,10 @@ class GrapevineClient(WebSocketClientProtocol, Session):
data (dict): Converted json data.
"""
event = data['event']
event = data["event"]
if event == "authenticate":
# server replies to our auth handshake
if data['status'] != "success":
if data["status"] != "success":
log_err("Grapevine authentication failed.")
self.disconnect()
else:
@ -339,13 +320,14 @@ class GrapevineClient(WebSocketClientProtocol, Session):
self.send_heartbeat()
elif event == "restart":
# set the expected downtime
self.restart_downtime = data['payload']['downtime']
self.restart_downtime = data["payload"]["downtime"]
elif event == "channels/subscribe":
# subscription verification
if data.get('status', 'success') == "failure":
if data.get("status", "success") == "failure":
err = data.get("error", "N/A")
self.sessionhandler.data_in(bot_data_in=((f"Grapevine error: {err}"),
{'event': event}))
self.sessionhandler.data_in(
bot_data_in=((f"Grapevine error: {err}"), {"event": event})
)
elif event == "channels/unsubscribe":
# unsubscribe-verification
pass
@ -353,19 +335,24 @@ class GrapevineClient(WebSocketClientProtocol, Session):
# incoming broadcast from network
payload = data["payload"]
print("channels/broadcast:", payload['channel'], self.channel)
if str(payload['channel']) != self.channel:
print("channels/broadcast:", payload["channel"], self.channel)
if str(payload["channel"]) != self.channel:
# only echo from channels this particular bot actually listens to
return
else:
# correct channel
self.sessionhandler.data_in(
self, bot_data_in=(
str(payload['message'],),
{"event": event,
"grapevine_channel": str(payload['channel']),
"sender": str(payload['name']),
"game": str(payload['game'])}))
self,
bot_data_in=(
str(payload["message"]),
{
"event": event,
"grapevine_channel": str(payload["channel"]),
"sender": str(payload["name"]),
"game": str(payload["game"]),
},
),
)
elif event == "channels/send":
pass
else:

View file

@ -49,52 +49,51 @@ IRC_GREY = "15"
# test irc->evennia
# Use Ctrl+C <num> to produce mIRC colors in e.g. irssi
IRC_COLOR_MAP = dict((
(r'|n', IRC_COLOR + IRC_NORMAL), # normal mode
(r'|H', IRC_RESET), # un-highlight
(r'|/', "\n"), # line break
(r'|t', " "), # tab
(r'|-', " "), # fixed tab
(r'|_', " "), # space
(r'|*', IRC_INVERT), # invert
(r'|^', ""), # blinking text
(r'|h', IRC_BOLD), # highlight, use bold instead
(r'|r', IRC_COLOR + IRC_RED),
(r'|g', IRC_COLOR + IRC_GREEN),
(r'|y', IRC_COLOR + IRC_YELLOW),
(r'|b', IRC_COLOR + IRC_BLUE),
(r'|m', IRC_COLOR + IRC_MAGENTA),
(r'|c', IRC_COLOR + IRC_CYAN),
(r'|w', IRC_COLOR + IRC_WHITE), # pure white
(r'|x', IRC_COLOR + IRC_DGREY), # dark grey
(r'|R', IRC_COLOR + IRC_DRED),
(r'|G', IRC_COLOR + IRC_DGREEN),
(r'|Y', IRC_COLOR + IRC_DYELLOW),
(r'|B', IRC_COLOR + IRC_DBLUE),
(r'|M', IRC_COLOR + IRC_DMAGENTA),
(r'|C', IRC_COLOR + IRC_DCYAN),
(r'|W', IRC_COLOR + IRC_GREY), # light grey
(r'|X', IRC_COLOR + IRC_BLACK), # pure black
(r'|[r', IRC_COLOR + IRC_NORMAL + "," + IRC_DRED),
(r'|[g', IRC_COLOR + IRC_NORMAL + "," + IRC_DGREEN),
(r'|[y', IRC_COLOR + IRC_NORMAL + "," + IRC_DYELLOW),
(r'|[b', IRC_COLOR + IRC_NORMAL + "," + IRC_DBLUE),
(r'|[m', IRC_COLOR + IRC_NORMAL + "," + IRC_DMAGENTA),
(r'|[c', IRC_COLOR + IRC_NORMAL + "," + IRC_DCYAN),
(r'|[w', IRC_COLOR + IRC_NORMAL + "," + IRC_GREY), # light grey background
(r'|[x', IRC_COLOR + IRC_NORMAL + "," + IRC_BLACK) # pure black background
))
IRC_COLOR_MAP = dict(
(
(r"|n", IRC_COLOR + IRC_NORMAL), # normal mode
(r"|H", IRC_RESET), # un-highlight
(r"|/", "\n"), # line break
(r"|t", " "), # tab
(r"|-", " "), # fixed tab
(r"|_", " "), # space
(r"|*", IRC_INVERT), # invert
(r"|^", ""), # blinking text
(r"|h", IRC_BOLD), # highlight, use bold instead
(r"|r", IRC_COLOR + IRC_RED),
(r"|g", IRC_COLOR + IRC_GREEN),
(r"|y", IRC_COLOR + IRC_YELLOW),
(r"|b", IRC_COLOR + IRC_BLUE),
(r"|m", IRC_COLOR + IRC_MAGENTA),
(r"|c", IRC_COLOR + IRC_CYAN),
(r"|w", IRC_COLOR + IRC_WHITE), # pure white
(r"|x", IRC_COLOR + IRC_DGREY), # dark grey
(r"|R", IRC_COLOR + IRC_DRED),
(r"|G", IRC_COLOR + IRC_DGREEN),
(r"|Y", IRC_COLOR + IRC_DYELLOW),
(r"|B", IRC_COLOR + IRC_DBLUE),
(r"|M", IRC_COLOR + IRC_DMAGENTA),
(r"|C", IRC_COLOR + IRC_DCYAN),
(r"|W", IRC_COLOR + IRC_GREY), # light grey
(r"|X", IRC_COLOR + IRC_BLACK), # pure black
(r"|[r", IRC_COLOR + IRC_NORMAL + "," + IRC_DRED),
(r"|[g", IRC_COLOR + IRC_NORMAL + "," + IRC_DGREEN),
(r"|[y", IRC_COLOR + IRC_NORMAL + "," + IRC_DYELLOW),
(r"|[b", IRC_COLOR + IRC_NORMAL + "," + IRC_DBLUE),
(r"|[m", IRC_COLOR + IRC_NORMAL + "," + IRC_DMAGENTA),
(r"|[c", IRC_COLOR + IRC_NORMAL + "," + IRC_DCYAN),
(r"|[w", IRC_COLOR + IRC_NORMAL + "," + IRC_GREY), # light grey background
(r"|[x", IRC_COLOR + IRC_NORMAL + "," + IRC_BLACK), # pure black background
)
)
# ansi->irc
RE_ANSI_COLOR = re.compile(r"|".join(
[re.escape(key) for key in IRC_COLOR_MAP.keys()]), re.DOTALL)
RE_MXP = re.compile(r'\|lc(.*?)\|lt(.*?)\|le', re.DOTALL)
RE_ANSI_COLOR = re.compile(r"|".join([re.escape(key) for key in IRC_COLOR_MAP.keys()]), re.DOTALL)
RE_MXP = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
RE_ANSI_ESCAPES = re.compile(r"(%s)" % "|".join(("{{", "%%", "\\\\")), re.DOTALL)
# irc->ansi
_CLR_LIST = [re.escape(val)
for val in sorted(IRC_COLOR_MAP.values(), key=len, reverse=True) if val.strip()]
_CLR_LIST = [
re.escape(val) for val in sorted(IRC_COLOR_MAP.values(), key=len, reverse=True) if val.strip()
]
_CLR_LIST = _CLR_LIST[-2:] + _CLR_LIST[:-2]
RE_IRC_COLOR = re.compile(r"|".join(_CLR_LIST), re.DOTALL)
ANSI_COLOR_MAP = dict((tup[1], tup[0]) for tup in IRC_COLOR_MAP.items() if tup[1].strip())
@ -122,7 +121,7 @@ def parse_ansi_to_irc(string):
pstring = RE_ANSI_COLOR.sub(_sub_to_irc, part)
parsed_string.append("%s%s" % (pstring, sep[0].strip()))
# strip mxp
parsed_string = RE_MXP.sub(r'\2', "".join(parsed_string))
parsed_string = RE_MXP.sub(r"\2", "".join(parsed_string))
return parsed_string
@ -148,12 +147,14 @@ def parse_irc_to_ansi(string):
# IRC bot
class IRCBot(irc.IRCClient, Session):
"""
An IRC bot that tracks activity in a channel as well
as sends text to it when prompted
"""
lineRate = 1
# assigned by factory at creation
@ -179,8 +180,10 @@ class IRCBot(irc.IRCClient, Session):
self.uid = int(self.factory.uid)
self.logged_in = True
self.factory.sessionhandler.connect(self)
logger.log_info("IRC bot '%s' connected to %s at %s:%s." % (self.nickname, self.channel,
self.network, self.port))
logger.log_info(
"IRC bot '%s' connected to %s at %s:%s."
% (self.nickname, self.channel, self.network, self.port)
)
def disconnect(self, reason=""):
"""
@ -209,11 +212,11 @@ class IRCBot(irc.IRCClient, Session):
"""
if channel == self.nickname:
# private message
user = user.split('!', 1)[0]
user = user.split("!", 1)[0]
self.data_in(text=msg, type="privmsg", user=user, channel=channel)
elif not msg.startswith('***'):
elif not msg.startswith("***"):
# channel message
user = user.split('!', 1)[0]
user = user.split("!", 1)[0]
user = ansi.raw(user)
self.data_in(text=msg, type="msg", user=user, channel=channel)
@ -227,8 +230,8 @@ class IRCBot(irc.IRCClient, Session):
msg (str): The message arriving from channel.
"""
if not msg.startswith('**'):
user = user.split('!', 1)[0]
if not msg.startswith("**"):
user = user.split("!", 1)[0]
self.data_in(text=msg, type="action", user=user, channel=channel)
def get_nicklist(self):
@ -245,14 +248,16 @@ class IRCBot(irc.IRCClient, Session):
channel = params[2].lower()
if channel != self.channel.lower():
return
self.nicklist += params[3].split(' ')
self.nicklist += params[3].split(" ")
def irc_RPL_ENDOFNAMES(self, prefix, params):
"""Called when the nicklist has finished being returned."""
channel = params[1].lower()
if channel != self.channel.lower():
return
self.data_in(text="", type="nicklist", user="server", channel=channel, nicklist=self.nicklist)
self.data_in(
text="", type="nicklist", user="server", channel=channel, nicklist=self.nicklist
)
self.nicklist = []
def pong(self, user, time):
@ -348,12 +353,22 @@ class IRCBotFactory(protocol.ReconnectingClientFactory):
increase in delay
"""
# scaling reconnect time
initialDelay = 1
factor = 1.5
maxDelay = 60
def __init__(self, sessionhandler, uid=None, botname=None, channel=None, network=None, port=None, ssl=None):
def __init__(
self,
sessionhandler,
uid=None,
botname=None,
channel=None,
network=None,
port=None,
ssl=None,
):
"""
Storing some important protocol properties.
@ -452,7 +467,10 @@ class IRCBotFactory(protocol.ReconnectingClientFactory):
if self.ssl:
try:
from twisted.internet import ssl
service = reactor.connectSSL(self.network, int(self.port), self, ssl.ClientContextFactory())
service = reactor.connectSSL(
self.network, int(self.port), self, ssl.ClientContextFactory()
)
except ImportError:
logger.log_err("To use SSL, the PyOpenSSL module must be installed.")
else:

View file

@ -17,7 +17,7 @@ mccp_compress and calling it from its write methods.
import zlib
# negotiations for v1 and v2 of the protocol
MCCP = b'\x56'
MCCP = b"\x56"
FLUSH = zlib.Z_SYNC_FLUSH
@ -32,7 +32,7 @@ def mccp_compress(protocol, data):
stream (binary): Zlib-compressed data.
"""
if hasattr(protocol, 'zlib'):
if hasattr(protocol, "zlib"):
return protocol.zlib.compress(data) + protocol.zlib.flush(FLUSH)
return data
@ -57,7 +57,7 @@ class Mccp(object):
"""
self.protocol = protocol
self.protocol.protocol_flags['MCCP'] = False
self.protocol.protocol_flags["MCCP"] = False
# ask if client will mccp, connect callbacks to handle answer
self.protocol.will(MCCP).addCallbacks(self.do_mccp, self.no_mccp)
@ -69,9 +69,9 @@ class Mccp(object):
option (Option): Option dict (not used).
"""
if hasattr(self.protocol, 'zlib'):
if hasattr(self.protocol, "zlib"):
del self.protocol.zlib
self.protocol.protocol_flags['MCCP'] = False
self.protocol.protocol_flags["MCCP"] = False
self.protocol.handshake_done()
def do_mccp(self, option):
@ -83,7 +83,7 @@ class Mccp(object):
option (Option): Option dict (not used).
"""
self.protocol.protocol_flags['MCCP'] = True
self.protocol.requestNegotiation(MCCP, b'')
self.protocol.protocol_flags["MCCP"] = True
self.protocol.requestNegotiation(MCCP, b"")
self.protocol.zlib = zlib.compressobj(9)
self.protocol.handshake_done()

View file

@ -13,9 +13,9 @@ active players and so on.
from django.conf import settings
from evennia.utils import utils
MSSP = b'\x46'
MSSP_VAR = b'\x01'
MSSP_VAL = b'\x02'
MSSP = b"\x46"
MSSP_VAR = b"\x01"
MSSP_VAL = b"\x02"
# try to get the customized mssp info, if it exists.
MSSPTable_CUSTOM = utils.variable_from_module(settings.MSSP_META_MODULE, "MSSPTable", default={})
@ -81,18 +81,16 @@ class Mssp(object):
"""
self.mssp_table = {
# Required fields
"NAME": settings.SERVERNAME,
"PLAYERS": self.get_player_count,
"UPTIME": self.get_uptime,
"PORT": list(reversed(settings.TELNET_PORTS)), # most important port should be last in list
"PORT": list(
reversed(settings.TELNET_PORTS)
), # most important port should be last in list
# Evennia auto-filled
"CRAWL DELAY": "-1",
"CODEBASE": utils.get_evennia_version(mode='pretty'),
"CODEBASE": utils.get_evennia_version(mode="pretty"),
"FAMILY": "Custom",
"ANSI": "1",
"GMCP": "1" if settings.TELNET_OOB_ENABLED else "0",
@ -114,16 +112,17 @@ class Mssp(object):
if MSSPTable_CUSTOM:
self.mssp_table.update(MSSPTable_CUSTOM)
varlist = b''
varlist = b""
for variable, value in self.mssp_table.items():
if callable(value):
value = value()
if utils.is_iter(value):
for partval in value:
varlist += (MSSP_VAR + bytes(variable, 'utf-8') +
MSSP_VAL + bytes(partval, 'utf-8'))
varlist += (
MSSP_VAR + bytes(variable, "utf-8") + MSSP_VAL + bytes(partval, "utf-8")
)
else:
varlist += MSSP_VAR + bytes(variable, 'utf-8') + MSSP_VAL + bytes(value, 'utf-8')
varlist += MSSP_VAR + bytes(variable, "utf-8") + MSSP_VAL + bytes(value, "utf-8")
# send to crawler by subnegotiation
self.protocol.requestNegotiation(MSSP, varlist)

View file

@ -15,17 +15,13 @@ http://www.gammon.com.au/mushclient/addingservermxp.htm
"""
import re
LINKS_SUB = re.compile(r'\|lc(.*?)\|lt(.*?)\|le', re.DOTALL)
LINKS_SUB = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
# MXP Telnet option
MXP = b'\x5b'
MXP = b"\x5b"
MXP_TEMPSECURE = "\x1B[4z"
MXP_SEND = MXP_TEMPSECURE + \
"<SEND HREF=\"\\1\">" + \
"\\2" + \
MXP_TEMPSECURE + \
"</SEND>"
MXP_SEND = MXP_TEMPSECURE + '<SEND HREF="\\1">' + "\\2" + MXP_TEMPSECURE + "</SEND>"
def mxp_parse(text):
@ -39,9 +35,7 @@ def mxp_parse(text):
parsed (str): The parsed text.
"""
text = text.replace("&", "&amp;") \
.replace("<", "&lt;") \
.replace(">", "&gt;")
text = text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
text = LINKS_SUB.sub(MXP_SEND, text)
return text
@ -85,5 +79,5 @@ class Mxp(object):
"""
self.protocol.protocol_flags["MXP"] = True
self.protocol.requestNegotiation(MXP, b'')
self.protocol.requestNegotiation(MXP, b"")
self.protocol.handshake_done()

View file

@ -12,8 +12,8 @@ client and update it when the size changes
from codecs import encode as codecs_encode
from django.conf import settings
NAWS = b'\x1f'
IS = b'\x00'
NAWS = b"\x1f"
IS = b"\x00"
# default taken from telnet specification
DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
DEFAULT_HEIGHT = settings.CLIENT_DEFAULT_HEIGHT
@ -39,8 +39,10 @@ class Naws(object):
"""
self.naws_step = 0
self.protocol = protocol
self.protocol.protocol_flags['SCREENWIDTH'] = {0: DEFAULT_WIDTH} # windowID (0 is root):width
self.protocol.protocol_flags['SCREENHEIGHT'] = {0: DEFAULT_HEIGHT} # windowID:width
self.protocol.protocol_flags["SCREENWIDTH"] = {
0: DEFAULT_WIDTH
} # windowID (0 is root):width
self.protocol.protocol_flags["SCREENHEIGHT"] = {0: DEFAULT_HEIGHT} # windowID:width
self.protocol.negotiationMap[NAWS] = self.negotiate_sizes
self.protocol.do(NAWS).addCallbacks(self.do_naws, self.no_naws)
@ -76,6 +78,6 @@ class Naws(object):
if len(options) == 4:
# NAWS is negotiated with 16bit words
width = options[0] + options[1]
self.protocol.protocol_flags['SCREENWIDTH'][0] = int(codecs_encode(width, 'hex'), 16)
self.protocol.protocol_flags["SCREENWIDTH"][0] = int(codecs_encode(width, "hex"), 16)
height = options[2] + options[3]
self.protocol.protocol_flags['SCREENHEIGHT'][0] = int(codecs_encode(height, 'hex'), 16)
self.protocol.protocol_flags["SCREENHEIGHT"][0] = int(codecs_encode(height, "hex"), 16)

View file

@ -17,10 +17,12 @@ from twisted.internet import protocol, reactor
from twisted.python.log import ILogObserver
import django
django.setup()
from django.conf import settings
import evennia
evennia._init()
from evennia.utils.utils import get_evennia_version, mod_import, make_iter
@ -36,7 +38,9 @@ try:
except Exception:
pass
PORTAL_SERVICES_PLUGIN_MODULES = [mod_import(module) for module in make_iter(settings.PORTAL_SERVICES_PLUGIN_MODULES)]
PORTAL_SERVICES_PLUGIN_MODULES = [
mod_import(module) for module in make_iter(settings.PORTAL_SERVICES_PLUGIN_MODULES)
]
LOCKDOWN_MODE = settings.LOCKDOWN_MODE
# -------------------------------------------------------------
@ -47,7 +51,7 @@ VERSION = get_evennia_version()
SERVERNAME = settings.SERVERNAME
PORTAL_RESTART = os.path.join(settings.GAME_DIR, "server", 'portal.restart')
PORTAL_RESTART = os.path.join(settings.GAME_DIR, "server", "portal.restart")
TELNET_PORTS = settings.TELNET_PORTS
SSL_PORTS = settings.SSL_PORTS
@ -55,11 +59,11 @@ SSH_PORTS = settings.SSH_PORTS
WEBSERVER_PORTS = settings.WEBSERVER_PORTS
WEBSOCKET_CLIENT_PORT = settings.WEBSOCKET_CLIENT_PORT
TELNET_INTERFACES = ['127.0.0.1'] if LOCKDOWN_MODE else settings.TELNET_INTERFACES
SSL_INTERFACES = ['127.0.0.1'] if LOCKDOWN_MODE else settings.SSL_INTERFACES
SSH_INTERFACES = ['127.0.0.1'] if LOCKDOWN_MODE else settings.SSH_INTERFACES
WEBSERVER_INTERFACES = ['127.0.0.1'] if LOCKDOWN_MODE else settings.WEBSERVER_INTERFACES
WEBSOCKET_CLIENT_INTERFACE = '127.0.0.1' if LOCKDOWN_MODE else settings.WEBSOCKET_CLIENT_INTERFACE
TELNET_INTERFACES = ["127.0.0.1"] if LOCKDOWN_MODE else settings.TELNET_INTERFACES
SSL_INTERFACES = ["127.0.0.1"] if LOCKDOWN_MODE else settings.SSL_INTERFACES
SSH_INTERFACES = ["127.0.0.1"] if LOCKDOWN_MODE else settings.SSH_INTERFACES
WEBSERVER_INTERFACES = ["127.0.0.1"] if LOCKDOWN_MODE else settings.WEBSERVER_INTERFACES
WEBSOCKET_CLIENT_INTERFACE = "127.0.0.1" if LOCKDOWN_MODE else settings.WEBSOCKET_CLIENT_INTERFACE
WEBSOCKET_CLIENT_URL = settings.WEBSOCKET_CLIENT_URL
TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS and TELNET_INTERFACES
@ -67,16 +71,29 @@ SSL_ENABLED = settings.SSL_ENABLED and SSL_PORTS and SSL_INTERFACES
SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS and SSH_INTERFACES
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
WEBSOCKET_CLIENT_ENABLED = settings.WEBSOCKET_CLIENT_ENABLED and WEBSOCKET_CLIENT_PORT and WEBSOCKET_CLIENT_INTERFACE
WEBSOCKET_CLIENT_ENABLED = (
settings.WEBSOCKET_CLIENT_ENABLED and WEBSOCKET_CLIENT_PORT and WEBSOCKET_CLIENT_INTERFACE
)
AMP_HOST = settings.AMP_HOST
AMP_PORT = settings.AMP_PORT
AMP_INTERFACE = settings.AMP_INTERFACE
AMP_ENABLED = AMP_HOST and AMP_PORT and AMP_INTERFACE
INFO_DICT = {"servername": SERVERNAME, "version": VERSION, "errors": "", "info": "",
"lockdown_mode": "", "amp": "", "telnet": [], "telnet_ssl": [], "ssh": [],
"webclient": [], "webserver_proxy": [], "webserver_internal": []}
INFO_DICT = {
"servername": SERVERNAME,
"version": VERSION,
"errors": "",
"info": "",
"lockdown_mode": "",
"amp": "",
"telnet": [],
"telnet_ssl": [],
"ssh": [],
"webclient": [],
"webserver_proxy": [],
"webserver_internal": [],
}
# -------------------------------------------------------------
# Portal Service object
@ -100,7 +117,7 @@ class Portal(object):
application (Application): An instantiated Twisted application
"""
sys.path.append('.')
sys.path.append(".")
# create a store of services
self.services = service.MultiService()
@ -122,8 +139,9 @@ class Portal(object):
# set a callback if the server is killed abruptly,
# by Ctrl-C, reboot etc.
reactor.addSystemEventTrigger('before', 'shutdown',
self.shutdown, _reactor_stopping=True, _stop_server=True)
reactor.addSystemEventTrigger(
"before", "shutdown", self.shutdown, _reactor_stopping=True, _stop_server=True
)
def _get_backup_server_twistd_cmd(self):
"""
@ -135,11 +153,13 @@ class Portal(object):
"""
server_twistd_cmd = [
"twistd",
"--python={}".format(os.path.join(dirname(dirname(abspath(__file__))), "server.py"))]
if os.name != 'nt':
"--python={}".format(os.path.join(dirname(dirname(abspath(__file__))), "server.py")),
]
if os.name != "nt":
gamedir = os.getcwd()
server_twistd_cmd.append("--pidfile={}".format(
os.path.join(gamedir, "server", "server.pid")))
server_twistd_cmd.append(
"--pidfile={}".format(os.path.join(gamedir, "server", "server.pid"))
)
return server_twistd_cmd
def get_info_dict(self):
@ -169,7 +189,7 @@ class Portal(object):
self.sessions.disconnect_all()
if _stop_server:
self.amp_protocol.stop_server(mode='shutdown')
self.amp_protocol.stop_server(mode="shutdown")
if not _reactor_stopping:
# shutting down the reactor will trigger another signal. We set
@ -177,6 +197,7 @@ class Portal(object):
self.shutdown_complete = True
reactor.callLater(0, reactor.stop)
# -------------------------------------------------------------
#
# Start the Portal proxy server and add all active services
@ -186,13 +207,14 @@ class Portal(object):
# twistd requires us to define the variable 'application' so it knows
# what to execute from.
application = service.Application('Portal')
application = service.Application("Portal")
# custom logging
if "--nodaemon" not in sys.argv:
logfile = logger.WeeklyLogFile(os.path.basename(settings.PORTAL_LOG_FILE),
os.path.dirname(settings.PORTAL_LOG_FILE))
logfile = logger.WeeklyLogFile(
os.path.basename(settings.PORTAL_LOG_FILE), os.path.dirname(settings.PORTAL_LOG_FILE)
)
application.setComponent(ILogObserver, logger.PortalLogObserver(logfile).emit)
# The main Portal server program. This sets up the database
@ -201,7 +223,7 @@ PORTAL = Portal(application)
if LOCKDOWN_MODE:
INFO_DICT["lockdown_mode"] = ' LOCKDOWN_MODE active: Only local connections.'
INFO_DICT["lockdown_mode"] = " LOCKDOWN_MODE active: Only local connections."
if AMP_ENABLED:
@ -211,7 +233,7 @@ if AMP_ENABLED:
from evennia.server.portal import amp_server
INFO_DICT["amp"] = 'amp: %s' % AMP_PORT
INFO_DICT["amp"] = "amp: %s" % AMP_PORT
factory = amp_server.AMPServerFactory(PORTAL)
amp_service = internet.TCPServer(AMP_PORT, factory, interface=AMP_INTERFACE)
@ -230,7 +252,7 @@ if TELNET_ENABLED:
for interface in TELNET_INTERFACES:
ifacestr = ""
if interface not in ('0.0.0.0', '::') or len(TELNET_INTERFACES) > 1:
if interface not in ("0.0.0.0", "::") or len(TELNET_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in TELNET_PORTS:
pstring = "%s:%s" % (ifacestr, port)
@ -239,10 +261,10 @@ if TELNET_ENABLED:
factory.protocol = telnet.TelnetProtocol
factory.sessionhandler = PORTAL_SESSIONS
telnet_service = internet.TCPServer(port, factory, interface=interface)
telnet_service.setName('EvenniaTelnet%s' % pstring)
telnet_service.setName("EvenniaTelnet%s" % pstring)
PORTAL.services.addService(telnet_service)
INFO_DICT["telnet"].append('telnet%s: %s' % (ifacestr, port))
INFO_DICT["telnet"].append("telnet%s: %s" % (ifacestr, port))
if SSL_ENABLED:
@ -253,7 +275,7 @@ if SSL_ENABLED:
for interface in SSL_INTERFACES:
ifacestr = ""
if interface not in ('0.0.0.0', '::') or len(SSL_INTERFACES) > 1:
if interface not in ("0.0.0.0", "::") or len(SSL_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in SSL_PORTS:
pstring = "%s:%s" % (ifacestr, port)
@ -264,17 +286,17 @@ if SSL_ENABLED:
ssl_context = telnet_ssl.getSSLContext()
if ssl_context:
ssl_service = internet.SSLServer(port,
factory,
telnet_ssl.getSSLContext(),
interface=interface)
ssl_service.setName('EvenniaSSL%s' % pstring)
ssl_service = internet.SSLServer(
port, factory, telnet_ssl.getSSLContext(), interface=interface
)
ssl_service.setName("EvenniaSSL%s" % pstring)
PORTAL.services.addService(ssl_service)
INFO_DICT["telnet_ssl"].append("telnet+ssl%s: %s" % (ifacestr, port))
else:
INFO_DICT["telnet_ssl"].append(
"telnet+ssl%s: %s (deactivated - keys/cert unset)" % (ifacestr, port))
"telnet+ssl%s: %s (deactivated - keys/cert unset)" % (ifacestr, port)
)
if SSH_ENABLED:
@ -286,16 +308,20 @@ if SSH_ENABLED:
for interface in SSH_INTERFACES:
ifacestr = ""
if interface not in ('0.0.0.0', '::') or len(SSH_INTERFACES) > 1:
if interface not in ("0.0.0.0", "::") or len(SSH_INTERFACES) > 1:
ifacestr = "-%s" % interface
for port in SSH_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = ssh.makeFactory({'protocolFactory': ssh.SshProtocol,
'protocolArgs': (),
'sessions': PORTAL_SESSIONS})
factory = ssh.makeFactory(
{
"protocolFactory": ssh.SshProtocol,
"protocolArgs": (),
"sessions": PORTAL_SESSIONS,
}
)
factory.noisy = False
ssh_service = internet.TCPServer(port, factory, interface=interface)
ssh_service.setName('EvenniaSSH%s' % pstring)
ssh_service.setName("EvenniaSSH%s" % pstring)
PORTAL.services.addService(ssh_service)
INFO_DICT["ssh"].append("ssh%s: %s" % (ifacestr, port))
@ -309,10 +335,10 @@ if WEBSERVER_ENABLED:
websocket_started = False
for interface in WEBSERVER_INTERFACES:
ifacestr = ""
if interface not in ('0.0.0.0', '::') or len(WEBSERVER_INTERFACES) > 1:
if interface not in ("0.0.0.0", "::") or len(WEBSERVER_INTERFACES) > 1:
ifacestr = "-%s" % interface
for proxyport, serverport in WEBSERVER_PORTS:
web_root = EvenniaReverseProxyResource('127.0.0.1', serverport, '')
web_root = EvenniaReverseProxyResource("127.0.0.1", serverport, "")
webclientstr = ""
if WEBCLIENT_ENABLED:
# create ajax client processes at /webclientdata
@ -330,8 +356,8 @@ if WEBSERVER_ENABLED:
from autobahn.twisted.websocket import WebSocketServerFactory
w_interface = WEBSOCKET_CLIENT_INTERFACE
w_ifacestr = ''
if w_interface not in ('0.0.0.0', '::') or len(WEBSERVER_INTERFACES) > 1:
w_ifacestr = ""
if w_interface not in ("0.0.0.0", "::") or len(WEBSERVER_INTERFACES) > 1:
w_ifacestr = "-%s" % interface
port = WEBSOCKET_CLIENT_PORT
@ -344,7 +370,7 @@ if WEBSERVER_ENABLED:
factory.protocol = webclient.WebSocketClient
factory.sessionhandler = PORTAL_SESSIONS
websocket_service = internet.TCPServer(port, factory, interface=w_interface)
websocket_service.setName('EvenniaWebSocket%s:%s' % (w_ifacestr, port))
websocket_service.setName("EvenniaWebSocket%s:%s" % (w_ifacestr, port))
PORTAL.services.addService(websocket_service)
websocket_started = True
webclientstr = "webclient-websocket%s: %s" % (w_ifacestr, port)
@ -352,10 +378,8 @@ if WEBSERVER_ENABLED:
web_root = Website(web_root, logPath=settings.HTTP_LOG_FILE)
web_root.is_portal = True
proxy_service = internet.TCPServer(proxyport,
web_root,
interface=interface)
proxy_service.setName('EvenniaWebProxy%s:%s' % (ifacestr, proxyport))
proxy_service = internet.TCPServer(proxyport, web_root, interface=interface)
proxy_service.setName("EvenniaWebProxy%s:%s" % (ifacestr, proxyport))
PORTAL.services.addService(proxy_service)
INFO_DICT["webserver_proxy"].append("webserver-proxy%s: %s" % (ifacestr, proxyport))
INFO_DICT["webserver_internal"].append("webserver: %s" % serverport)

View file

@ -3,13 +3,11 @@ Sessionhandler for portal sessions
"""
import time
from collections import deque, namedtuple
from twisted.internet import reactor
from django.conf import settings
from evennia.server.sessionhandler import SessionHandler, PCONN, PDISCONN, \
PCONNSYNC, PDISCONNALL
from evennia.server.sessionhandler import SessionHandler, PCONN, PDISCONN, PCONNSYNC, PDISCONNALL
from evennia.utils.logger import log_trace
# module import
@ -29,7 +27,7 @@ _ERROR_MAX_CHAR = settings.MAX_CHAR_LIMIT_WARNING
_CONNECTION_QUEUE = deque()
DUMMYSESSION = namedtuple('DummySession', ['sessid'])(0)
DUMMYSESSION = namedtuple("DummySession", ["sessid"])(0)
# -------------------------------------------------------------
# Portal-SessionHandler class
@ -98,19 +96,34 @@ class PortalSessionHandler(SessionHandler):
session.server_connected = False
_CONNECTION_QUEUE.appendleft(session)
if len(_CONNECTION_QUEUE) > 1:
session.data_out(text=[["%s DoS protection is active. You are queued to connect in %g seconds ..." % (
settings.SERVERNAME,
len(_CONNECTION_QUEUE) * _MIN_TIME_BETWEEN_CONNECTS)], {}])
session.data_out(
text=[
[
"%s DoS protection is active. You are queued to connect in %g seconds ..."
% (
settings.SERVERNAME,
len(_CONNECTION_QUEUE) * _MIN_TIME_BETWEEN_CONNECTS,
)
],
{},
]
)
now = time.time()
if (now - self.connection_last < _MIN_TIME_BETWEEN_CONNECTS) or not self.portal.amp_protocol:
if (
now - self.connection_last < _MIN_TIME_BETWEEN_CONNECTS
) or not self.portal.amp_protocol:
if not session or not self.connection_task:
self.connection_task = reactor.callLater(_MIN_TIME_BETWEEN_CONNECTS, self.connect, None)
self.connection_task = reactor.callLater(
_MIN_TIME_BETWEEN_CONNECTS, self.connect, None
)
self.connection_last = now
return
elif not session:
if _CONNECTION_QUEUE:
# keep launching tasks until queue is empty
self.connection_task = reactor.callLater(_MIN_TIME_BETWEEN_CONNECTS, self.connect, None)
self.connection_task = reactor.callLater(
_MIN_TIME_BETWEEN_CONNECTS, self.connect, None
)
else:
self.connection_task = None
self.connection_last = now
@ -122,9 +135,9 @@ class PortalSessionHandler(SessionHandler):
self[session.sessid] = session
session.server_connected = True
self.portal.amp_protocol.send_AdminPortal2Server(session,
operation=PCONN,
sessiondata=sessdata)
self.portal.amp_protocol.send_AdminPortal2Server(
session, operation=PCONN, sessiondata=sessdata
)
def sync(self, session):
"""
@ -144,16 +157,23 @@ class PortalSessionHandler(SessionHandler):
if self.portal.amp_protocol:
# we only send sessdata that should not have changed
# at the server level at this point
sessdata = dict((key, val) for key, val in sessdata.items() if key in ("protocol_key",
"address",
"sessid",
"csessid",
"conn_time",
"protocol_flags",
"server_data",))
self.portal.amp_protocol.send_AdminPortal2Server(session,
operation=PCONNSYNC,
sessiondata=sessdata)
sessdata = dict(
(key, val)
for key, val in sessdata.items()
if key
in (
"protocol_key",
"address",
"sessid",
"csessid",
"conn_time",
"protocol_flags",
"server_data",
)
)
self.portal.amp_protocol.send_AdminPortal2Server(
session, operation=PCONNSYNC, sessiondata=sessdata
)
def disconnect(self, session):
"""
@ -187,6 +207,7 @@ class PortalSessionHandler(SessionHandler):
"""
Disconnect all sessions, informing the Server.
"""
def _callback(result, sessionhandler):
# we set a watchdog to stop self.disconnect from deleting
# sessions while we are looping over them.
@ -197,8 +218,9 @@ class PortalSessionHandler(SessionHandler):
# inform Server; wait until finished sending before we continue
# removing all the sessions.
self.portal.amp_protocol.send_AdminPortal2Server(DUMMYSESSION,
operation=PDISCONNALL).addCallback(_callback, self)
self.portal.amp_protocol.send_AdminPortal2Server(
DUMMYSESSION, operation=PDISCONNALL
).addCallback(_callback, self)
def server_connect(self, protocol_path="", config=dict()):
"""
@ -324,8 +346,11 @@ class PortalSessionHandler(SessionHandler):
session (list): The matching session, if found.
"""
return [sess for sess in self.get_sessions(include_unloggedin=True)
if hasattr(sess, 'csessid') and sess.csessid and sess.csessid == csessid]
return [
sess
for sess in self.get_sessions(include_unloggedin=True)
if hasattr(sess, "csessid") and sess.csessid and sess.csessid == csessid
]
def announce_all(self, message):
"""
@ -358,7 +383,7 @@ class PortalSessionHandler(SessionHandler):
"""
try:
text = kwargs['text']
text = kwargs["text"]
if (_MAX_CHAR_LIMIT > 0) and len(text) > _MAX_CHAR_LIMIT:
if session:
self.data_out(session, text=[[_ERROR_MAX_CHAR], {}])
@ -398,8 +423,7 @@ class PortalSessionHandler(SessionHandler):
# relay data to Server
session.cmd_last = now
self.portal.amp_protocol.send_MsgPortal2Server(session,
**kwargs)
self.portal.amp_protocol.send_MsgPortal2Server(session, **kwargs)
def data_out(self, session, **kwargs):
"""

View file

@ -11,13 +11,15 @@ from evennia.server.session import Session
from evennia.utils import logger
RSS_ENABLED = settings.RSS_ENABLED
#RETAG = re.compile(r'<[^>]*?>')
# RETAG = re.compile(r'<[^>]*?>')
if RSS_ENABLED:
try:
import feedparser
except ImportError:
raise ImportError("RSS requires python-feedparser to be installed. Install or set RSS_ENABLED=False.")
raise ImportError(
"RSS requires python-feedparser to be installed. Install or set RSS_ENABLED=False."
)
class RSSReader(Session):
@ -48,8 +50,8 @@ class RSSReader(Session):
"""
feed = feedparser.parse(self.url)
new_entries = []
for entry in feed['entries']:
idval = entry['id'] + entry.get("updated", "")
for entry in feed["entries"]:
idval = entry["id"] + entry.get("updated", "")
if idval not in self.old_entries:
self.old_entries[idval] = entry
new_entries.append(entry)
@ -110,7 +112,11 @@ class RSSReader(Session):
on slow connections.
"""
return threads.deferToThread(self.get_new).addCallback(self._callback, init).addErrback(self._errback)
return (
threads.deferToThread(self.get_new)
.addCallback(self._callback, init)
.addErrback(self._errback)
)
class RSSBotFactory(object):
@ -140,6 +146,7 @@ class RSSBotFactory(object):
"""
Called by portalsessionhandler. Starts the bot.
"""
def errback(fail):
logger.log_err(fail.value)

View file

@ -49,16 +49,18 @@ from evennia.utils import ansi
from evennia.utils.utils import to_str
_RE_N = re.compile(r"\|n$")
_RE_SCREENREADER_REGEX = re.compile(r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE)
_RE_SCREENREADER_REGEX = re.compile(
r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE
)
_GAME_DIR = settings.GAME_DIR
_PRIVATE_KEY_FILE = os.path.join(_GAME_DIR, "server", "ssh-private.key")
_PUBLIC_KEY_FILE = os.path.join(_GAME_DIR, "server", "ssh-public.key")
_KEY_LENGTH = 2048
CTRL_C = '\x03'
CTRL_D = '\x04'
CTRL_BACKSLASH = '\x1c'
CTRL_L = '\x0c'
CTRL_C = "\x03"
CTRL_D = "\x04"
CTRL_BACKSLASH = "\x1c"
CTRL_L = "\x0c"
_NO_AUTOGEN = """
Evennia could not generate SSH private- and public keys ({{err}})
@ -68,7 +70,9 @@ If this error persists, create the keys manually (using the tools for your OS)
and put them here:
{}
{}
""".format(_PRIVATE_KEY_FILE, _PUBLIC_KEY_FILE)
""".format(
_PRIVATE_KEY_FILE, _PUBLIC_KEY_FILE
)
# not used atm
@ -87,6 +91,7 @@ class SshProtocol(Manhole, session.Session):
here.
"""
noisy = False
def __init__(self, starttuple):
@ -162,7 +167,7 @@ class SshProtocol(Manhole, session.Session):
"""
if self.lineBuffer:
self.terminal.write('\a')
self.terminal.write("\a")
else:
self.handle_QUIT()
@ -229,7 +234,7 @@ class SshProtocol(Manhole, session.Session):
string (str): Output text.
"""
for line in string.split('\n'):
for line in string.split("\n"):
# the telnet-specific method for sending
self.terminal.write(line)
self.terminal.nextLine()
@ -251,7 +256,7 @@ class SshProtocol(Manhole, session.Session):
"""
if reason:
self.data_out(text=((reason, ), {}))
self.data_out(text=((reason,), {}))
self.connectionLost(reason)
def data_out(self, **kwargs):
@ -294,8 +299,8 @@ class SshProtocol(Manhole, session.Session):
# handle arguments
options = kwargs.get("options", {})
flags = self.protocol_flags
xterm256 = options.get("xterm256", flags.get('XTERM256', True))
useansi = options.get("ansi", flags.get('ANSI', True))
xterm256 = options.get("xterm256", flags.get("XTERM256", True))
useansi = options.get("ansi", flags.get("ANSI", True))
raw = options.get("raw", flags.get("RAW", False))
nocolor = options.get("nocolor", flags.get("NOCOLOR") or not (xterm256 or useansi))
# echo = options.get("echo", None) # DEBUG
@ -313,8 +318,12 @@ class SshProtocol(Manhole, session.Session):
else:
# we need to make sure to kill the color at the end in order
# to match the webclient output.
linetosend = ansi.parse_ansi(_RE_N.sub("", text) + ("||n" if text.endswith("|") else "|n"),
strip_ansi=nocolor, xterm256=xterm256, mxp=False)
linetosend = ansi.parse_ansi(
_RE_N.sub("", text) + ("||n" if text.endswith("|") else "|n"),
strip_ansi=nocolor,
xterm256=xterm256,
mxp=False,
)
self.sendLine(linetosend)
def send_prompt(self, *args, **kwargs):
@ -342,8 +351,7 @@ class ExtraInfoAuthServer(SSHUserAuthServer):
password = common.getNS(packet[1:])[0]
c = credentials.UsernamePassword(self.user, password)
c.transport = self.transport
return self.portal.login(c, None, IConchUser).addErrback(
self._ebPassword)
return self.portal.login(c, None, IConchUser).addErrback(self._ebPassword)
class AccountDBPasswordChecker(object):
@ -353,6 +361,7 @@ class AccountDBPasswordChecker(object):
useful for the Realm.
"""
noisy = False
credentialInterfaces = (credentials.IUsernamePassword,)
@ -422,9 +431,12 @@ class TerminalSessionTransport_getPeer(object):
session = self.proto.session
self.proto.makeConnection(
_Glue(write=self.chainedProtocol.dataReceived,
loseConnection=lambda: avatar.conn.sendClose(session),
name="SSH Proto Transport"))
_Glue(
write=self.chainedProtocol.dataReceived,
loseConnection=lambda: avatar.conn.sendClose(session),
name="SSH Proto Transport",
)
)
def loseConnection():
self.proto.loseConnection()
@ -433,9 +445,13 @@ class TerminalSessionTransport_getPeer(object):
return session.conn.transport.transport.getPeer()
self.chainedProtocol.makeConnection(
_Glue(getPeer=getPeer, write=self.proto.write,
loseConnection=loseConnection,
name="Chained Proto Transport"))
_Glue(
getPeer=getPeer,
write=self.proto.write,
loseConnection=loseConnection,
name="Chained Proto Transport",
)
)
self.chainedProtocol.terminalProtocol.terminalSize(width, height)
@ -455,10 +471,10 @@ def getKeyPair(pubkeyfile, privkeyfile):
private_key_string = rsa_key.toString(type="OPENSSH")
# save keys for the future.
with open(privkeyfile, 'wt') as pfile:
with open(privkeyfile, "wt") as pfile:
pfile.write(private_key_string)
print("Created SSH private key in '{}'".format(_PRIVATE_KEY_FILE))
with open(pubkeyfile, 'wt') as pfile:
with open(pubkeyfile, "wt") as pfile:
pfile.write(public_key_string)
print("Created SSH public key in '{}'".format(_PUBLIC_KEY_FILE))
else:
@ -474,28 +490,30 @@ def makeFactory(configdict):
"""
Creates the ssh server factory.
"""
def chainProtocolFactory(username=None):
return insults.ServerProtocol(
configdict['protocolFactory'],
*configdict.get('protocolConfigdict', (username,)),
**configdict.get('protocolKwArgs', {}))
configdict["protocolFactory"],
*configdict.get("protocolConfigdict", (username,)),
**configdict.get("protocolKwArgs", {}),
)
rlm = PassAvatarIdTerminalRealm()
rlm.transportFactory = TerminalSessionTransport_getPeer
rlm.chainedProtocolFactory = chainProtocolFactory
factory = ConchFactory(Portal(rlm))
factory.sessionhandler = configdict['sessions']
factory.sessionhandler = configdict["sessions"]
try:
# create/get RSA keypair
publicKey, privateKey = getKeyPair(_PUBLIC_KEY_FILE, _PRIVATE_KEY_FILE)
factory.publicKeys = {b'ssh-rsa': publicKey}
factory.privateKeys = {b'ssh-rsa': privateKey}
factory.publicKeys = {b"ssh-rsa": publicKey}
factory.privateKeys = {b"ssh-rsa": privateKey}
except Exception as err:
print(_NO_AUTOGEN.format(err=err))
factory.services = factory.services.copy()
factory.services['ssh-userauth'] = ExtraInfoAuthServer
factory.services["ssh-userauth"] = ExtraInfoAuthServer
factory.portal.registerChecker(AccountDBPasswordChecker(factory))

View file

@ -5,6 +5,7 @@ SSL keys and certificates.
"""
import os
import sys
try:
import OpenSSL
from twisted.internet import ssl as twisted_ssl
@ -67,14 +68,14 @@ def verify_SSL_key_and_cert(keyfile, certfile):
from Crypto.PublicKey import RSA
from twisted.conch.ssh.keys import Key
print(" Creating SSL key and certificate ... ", end=' ')
print(" Creating SSL key and certificate ... ", end=" ")
try:
# create the RSA key and store it.
KEY_LENGTH = 1024
rsaKey = Key(RSA.generate(KEY_LENGTH))
keyString = rsaKey.toString(type="OPENSSH")
file(keyfile, 'w+b').write(keyString)
file(keyfile, "w+b").write(keyString)
except Exception as err:
print(NO_AUTOGEN.format(err=err, keyfile=keyfile))
sys.exit(5)
@ -83,11 +84,17 @@ def verify_SSL_key_and_cert(keyfile, certfile):
CERT_EXPIRE = 365 * 20 # twenty years validity
# default:
# openssl req -new -x509 -key ssl.key -out ssl.cert -days 7300
exestring = "openssl req -new -x509 -key %s -out %s -days %s" % (keyfile, certfile, CERT_EXPIRE)
exestring = "openssl req -new -x509 -key %s -out %s -days %s" % (
keyfile,
certfile,
CERT_EXPIRE,
)
try:
subprocess.call(exestring)
except OSError as err:
raise OSError(NO_AUTOCERT.format(err=err, certfile=certfile, keyfile=keyfile, exestring=exestring))
raise OSError(
NO_AUTOCERT.format(err=err, certfile=certfile, keyfile=keyfile, exestring=exestring)
)
print("done.")

View file

@ -13,7 +13,7 @@ It is set as the NOGOAHEAD protocol_flag option.
http://www.faqs.org/rfcs/rfc858.html
"""
SUPPRESS_GA = b'\x03'
SUPPRESS_GA = b"\x03"
# default taken from telnet specification

View file

@ -11,8 +11,19 @@ import re
from twisted.internet import protocol
from twisted.internet.task import LoopingCall
from twisted.conch.telnet import Telnet, StatefulTelnetProtocol
from twisted.conch.telnet import (IAC, NOP, LINEMODE, GA, WILL, WONT, ECHO, NULL,
MODE, LINEMODE_EDIT, LINEMODE_TRAPSIG)
from twisted.conch.telnet import (
IAC,
NOP,
LINEMODE,
GA,
WILL,
WONT,
ECHO,
NULL,
MODE,
LINEMODE_EDIT,
LINEMODE_TRAPSIG,
)
from django.conf import settings
from evennia.server.session import Session
from evennia.server.portal import ttype, mssp, telnet_oob, naws, suppress_ga
@ -24,7 +35,9 @@ from evennia.utils.utils import to_bytes
_RE_N = re.compile(r"\|n$")
_RE_LEND = re.compile(br"\n$|\r$|\r\n$|\r\x00$|", re.MULTILINE)
_RE_LINEBREAK = re.compile(br"\n\r|\r\n|\n|\r", re.DOTALL + re.MULTILINE)
_RE_SCREENREADER_REGEX = re.compile(r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE)
_RE_SCREENREADER_REGEX = re.compile(
r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE
)
_IDLE_COMMAND = str.encode(settings.IDLE_COMMAND + "\n")
@ -63,7 +76,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
self.handshakes = 8 # suppress-go-ahead, naws, ttype, mccp, mssp, msdp, gmcp, mxp
self.init_session(self.protocol_key, client_address, self.factory.sessionhandler)
self.protocol_flags["ENCODING"] = settings.ENCODINGS[0] if settings.ENCODINGS else 'utf-8'
self.protocol_flags["ENCODING"] = settings.ENCODINGS[0] if settings.ENCODINGS else "utf-8"
# add this new connection to sessionhandler so
# the Server becomes aware of it.
self.sessionhandler.connect(self)
@ -86,6 +99,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
self.mxp = Mxp(self)
from evennia.utils.utils import delay
# timeout the handshakes in case the client doesn't reply at all
self._handshake_delay = delay(2, callback=self.handshake_done, timeout=True)
@ -151,16 +165,18 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
"""
if option == LINEMODE:
# make sure to activate line mode with local editing for all clients
self.requestNegotiation(LINEMODE, MODE +
bytes(chr(ord(LINEMODE_EDIT) +
ord(LINEMODE_TRAPSIG)), 'ascii'))
self.requestNegotiation(
LINEMODE, MODE + bytes(chr(ord(LINEMODE_EDIT) + ord(LINEMODE_TRAPSIG)), "ascii")
)
return True
else:
return (option == ttype.TTYPE or
option == naws.NAWS or
option == MCCP or
option == mssp.MSSP or
option == suppress_ga.SUPPRESS_GA)
return (
option == ttype.TTYPE
or option == naws.NAWS
or option == MCCP
or option == mssp.MSSP
or option == suppress_ga.SUPPRESS_GA
)
def enableLocal(self, option):
"""
@ -173,10 +189,12 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
enable (bool): If this option should be enabled.
"""
return (option == LINEMODE or
option == MCCP or
option == ECHO or
option == suppress_ga.SUPPRESS_GA)
return (
option == LINEMODE
or option == MCCP
or option == ECHO
or option == suppress_ga.SUPPRESS_GA
)
def disableLocal(self, option):
"""
@ -242,7 +260,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
def _write(self, data):
"""hook overloading the one used in plain telnet"""
data = data.replace(b'\n', b'\r\n').replace(b'\r\r\n', b'\r\n')
data = data.replace(b"\n", b"\r\n").replace(b"\r\r\n", b"\r\n")
super()._write(mccp_compress(self, data))
def sendLine(self, line):
@ -256,7 +274,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
line = to_bytes(line, self)
# escape IAC in line mode, and correctly add \r\n (the TELNET end-of-line)
line = line.replace(IAC, IAC + IAC)
line = line.replace(b'\n', b'\r\n')
line = line.replace(b"\n", b"\r\n")
if not line.endswith(b"\r\n") and self.protocol_flags.get("FORCEDENDLINE", True):
line += b"\r\n"
if not self.protocol_flags.get("NOGOAHEAD", True):
@ -330,8 +348,12 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
# handle arguments
options = kwargs.get("options", {})
flags = self.protocol_flags
xterm256 = options.get("xterm256", flags.get('XTERM256', False) if flags.get("TTYPE", False) else True)
useansi = options.get("ansi", flags.get('ANSI', False) if flags.get("TTYPE", False) else True)
xterm256 = options.get(
"xterm256", flags.get("XTERM256", False) if flags.get("TTYPE", False) else True
)
useansi = options.get(
"ansi", flags.get("ANSI", False) if flags.get("TTYPE", False) else True
)
raw = options.get("raw", flags.get("RAW", False))
nocolor = options.get("nocolor", flags.get("NOCOLOR") or not (xterm256 or useansi))
echo = options.get("echo", None)
@ -348,12 +370,15 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
prompt = text
if not raw:
# processing
prompt = ansi.parse_ansi(_RE_N.sub("", prompt) + ("||n" if prompt.endswith("|") else "|n"),
strip_ansi=nocolor, xterm256=xterm256)
prompt = ansi.parse_ansi(
_RE_N.sub("", prompt) + ("||n" if prompt.endswith("|") else "|n"),
strip_ansi=nocolor,
xterm256=xterm256,
)
if mxp:
prompt = mxp_parse(prompt)
prompt = to_bytes(prompt, self)
prompt = prompt.replace(IAC, IAC + IAC).replace(b'\n', b'\r\n')
prompt = prompt.replace(IAC, IAC + IAC).replace(b"\n", b"\r\n")
prompt += IAC + GA
self.transport.write(mccp_compress(self, prompt))
else:
@ -377,8 +402,12 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
else:
# we need to make sure to kill the color at the end in order
# to match the webclient output.
linetosend = ansi.parse_ansi(_RE_N.sub("", text) + ("||n" if text.endswith("|") else "|n"),
strip_ansi=nocolor, xterm256=xterm256, mxp=mxp)
linetosend = ansi.parse_ansi(
_RE_N.sub("", text) + ("||n" if text.endswith("|") else "|n"),
strip_ansi=nocolor,
xterm256=xterm256,
mxp=mxp,
)
if mxp:
linetosend = mxp_parse(linetosend)
self.sendLine(linetosend)

View file

@ -30,16 +30,16 @@ import json
from evennia.utils.utils import is_iter
# MSDP-relevant telnet cmd/opt-codes
MSDP = b'\x45'
MSDP_VAR = b'\x01' # ^A
MSDP_VAL = b'\x02' # ^B
MSDP_TABLE_OPEN = b'\x03' # ^C
MSDP_TABLE_CLOSE = b'\x04' # ^D
MSDP_ARRAY_OPEN = b'\x05' # ^E
MSDP_ARRAY_CLOSE = b'\x06' # ^F
MSDP = b"\x45"
MSDP_VAR = b"\x01" # ^A
MSDP_VAL = b"\x02" # ^B
MSDP_TABLE_OPEN = b"\x03" # ^C
MSDP_TABLE_CLOSE = b"\x04" # ^D
MSDP_ARRAY_OPEN = b"\x05" # ^E
MSDP_ARRAY_CLOSE = b"\x06" # ^F
# GMCP
GMCP = b'\xc9'
GMCP = b"\xc9"
# General Telnet
from twisted.conch.telnet import IAC, SB, SE
@ -47,27 +47,28 @@ from twisted.conch.telnet import IAC, SB, SE
# pre-compiled regexes
# returns 2-tuple
msdp_regex_table = re.compile(br"%s\s*(\w*?)\s*%s\s*%s(.*?)%s"
% (MSDP_VAR, MSDP_VAL,
MSDP_TABLE_OPEN,
MSDP_TABLE_CLOSE))
msdp_regex_table = re.compile(
br"%s\s*(\w*?)\s*%s\s*%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, MSDP_TABLE_OPEN, MSDP_TABLE_CLOSE)
)
# returns 2-tuple
msdp_regex_array = re.compile(br"%s\s*(\w*?)\s*%s\s*%s(.*?)%s"
% (MSDP_VAR, MSDP_VAL,
MSDP_ARRAY_OPEN,
MSDP_ARRAY_CLOSE))
msdp_regex_array = re.compile(
br"%s\s*(\w*?)\s*%s\s*%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, MSDP_ARRAY_OPEN, MSDP_ARRAY_CLOSE)
)
msdp_regex_var = re.compile(br"%s" % MSDP_VAR)
msdp_regex_val = re.compile(br"%s" % MSDP_VAL)
EVENNIA_TO_GMCP = {"client_options": "Core.Supports.Get",
"get_inputfuncs": "Core.Commands.Get",
"get_value": "Char.Value.Get",
"repeat": "Char.Repeat.Update",
"monitor": "Char.Monitor.Update"}
EVENNIA_TO_GMCP = {
"client_options": "Core.Supports.Get",
"get_inputfuncs": "Core.Commands.Get",
"get_value": "Char.Value.Get",
"repeat": "Char.Repeat.Update",
"monitor": "Char.Monitor.Update",
}
# MSDP/GMCP communication handler
class TelnetOOB(object):
"""
Implements the MSDP and GMCP protocols.
@ -83,7 +84,7 @@ class TelnetOOB(object):
"""
self.protocol = protocol
self.protocol.protocol_flags['OOB'] = False
self.protocol.protocol_flags["OOB"] = False
self.MSDP = False
self.GMCP = False
# ask for the available protocols and assign decoders
@ -114,7 +115,7 @@ class TelnetOOB(object):
"""
self.MSDP = True
self.protocol.protocol_flags['OOB'] = True
self.protocol.protocol_flags["OOB"] = True
self.protocol.handshake_done()
def no_gmcp(self, option):
@ -137,7 +138,7 @@ class TelnetOOB(object):
"""
self.GMCP = True
self.protocol.protocol_flags['OOB'] = True
self.protocol.protocol_flags["OOB"] = True
self.protocol.handshake_done()
# encoders
@ -167,40 +168,45 @@ class TelnetOOB(object):
"""
msdp_cmdname = "{msdp_var}{msdp_cmdname}{msdp_val}".format(
msdp_var=MSDP_VAR, msdp_cmdname=cmdname, msdp_val=MSDP_VAL)
msdp_var=MSDP_VAR, msdp_cmdname=cmdname, msdp_val=MSDP_VAL
)
if not (args or kwargs):
return msdp_cmdname.encode()
# print("encode_msdp in:", cmdname, args, kwargs) # DEBUG
msdp_args = ''
msdp_args = ""
if args:
msdp_args = msdp_cmdname
if len(args) == 1:
msdp_args += args[0]
else:
msdp_args += "{msdp_array_open}" \
"{msdp_args}" \
"{msdp_array_close}".format(
msdp_array_open=MSDP_ARRAY_OPEN,
msdp_array_close=MSDP_ARRAY_CLOSE,
msdp_args="".join("%s%s"
% (MSDP_VAL, json.dumps(val))
for val in args))
msdp_args += (
"{msdp_array_open}"
"{msdp_args}"
"{msdp_array_close}".format(
msdp_array_open=MSDP_ARRAY_OPEN,
msdp_array_close=MSDP_ARRAY_CLOSE,
msdp_args="".join("%s%s" % (MSDP_VAL, json.dumps(val)) for val in args),
)
)
msdp_kwargs = ""
if kwargs:
msdp_kwargs = msdp_cmdname
msdp_kwargs += "{msdp_table_open}" \
"{msdp_kwargs}" \
"{msdp_table_close}".format(
msdp_table_open=MSDP_TABLE_OPEN,
msdp_table_close=MSDP_TABLE_CLOSE,
msdp_kwargs="".join("%s%s%s%s"
% (MSDP_VAR, key, MSDP_VAL,
json.dumps(val))
for key, val in kwargs.items()))
msdp_kwargs += (
"{msdp_table_open}"
"{msdp_kwargs}"
"{msdp_table_close}".format(
msdp_table_open=MSDP_TABLE_OPEN,
msdp_table_close=MSDP_TABLE_CLOSE,
msdp_kwargs="".join(
"%s%s%s%s" % (MSDP_VAR, key, MSDP_VAL, json.dumps(val))
for key, val in kwargs.items()
),
)
)
msdp_string = msdp_args + msdp_kwargs

View file

@ -8,6 +8,7 @@ ssl.cert in mygame/server/.
"""
import os
try:
from OpenSSL import crypto
from twisted.internet import ssl as twisted_ssl
@ -34,8 +35,14 @@ _PRIVATE_KEY_FILE = os.path.join(_GAME_DIR, "server", "ssl.key")
_PUBLIC_KEY_FILE = os.path.join(_GAME_DIR, "server", "ssl-public.key")
_CERTIFICATE_FILE = os.path.join(_GAME_DIR, "server", "ssl.cert")
_CERTIFICATE_EXPIRE = 365 * 24 * 60 * 60 * 20 # 20 years
_CERTIFICATE_ISSUER = {"C": "EV", "ST": "Evennia", "L": "Evennia", "O":
"Evennia Security", "OU": "Evennia Department", "CN": "evennia"}
_CERTIFICATE_ISSUER = {
"C": "EV",
"ST": "Evennia",
"L": "Evennia",
"O": "Evennia Security",
"OU": "Evennia Department",
"CN": "evennia",
}
# messages
@ -45,7 +52,9 @@ If this error persists, create them manually (using the tools for your OS). The
should be placed and named like this:
{}
{}
""".format(_PRIVATE_KEY_FILE, _PUBLIC_KEY_FILE)
""".format(
_PRIVATE_KEY_FILE, _PUBLIC_KEY_FILE
)
NO_AUTOCERT = """
Evennia's could not auto-generate the SSL certificate ({{err}}).
@ -54,7 +63,9 @@ The private key already exists here:
If this error persists, create the certificate manually (using the private key and
the tools for your OS). The file should be placed and named like this:
{}
""".format(_PRIVATE_KEY_FILE, _CERTIFICATE_FILE)
""".format(
_PRIVATE_KEY_FILE, _CERTIFICATE_FILE
)
class SSLProtocol(TelnetProtocol):
@ -88,11 +99,11 @@ def verify_or_create_SSL_key_and_cert(keyfile, certfile):
keypair = crypto.PKey()
keypair.generate_key(crypto.TYPE_RSA, _PRIVATE_KEY_LENGTH)
with open(_PRIVATE_KEY_FILE, 'wt') as pfile:
with open(_PRIVATE_KEY_FILE, "wt") as pfile:
pfile.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, keypair))
print("Created SSL private key in '{}'.".format(_PRIVATE_KEY_FILE))
with open(_PUBLIC_KEY_FILE, 'wt') as pfile:
with open(_PUBLIC_KEY_FILE, "wt") as pfile:
pfile.write(crypto.dump_publickey(crypto.FILETYPE_PEM, keypair))
print("Created SSL public key in '{}'.".format(_PUBLIC_KEY_FILE))
@ -114,9 +125,9 @@ def verify_or_create_SSL_key_and_cert(keyfile, certfile):
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(_CERTIFICATE_EXPIRE)
cert.set_pubkey(keypair)
cert.sign(keypair, 'sha1')
cert.sign(keypair, "sha1")
with open(_CERTIFICATE_FILE, 'wt') as cfile:
with open(_CERTIFICATE_FILE, "wt") as cfile:
cfile.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
print("Created SSL certificate in '{}'.".format(_CERTIFICATE_FILE))

View file

@ -36,6 +36,7 @@ class TestAMPServer(TwistedTestCase):
"""
Test AMP communication
"""
def setUp(self):
super(TestAMPServer, self).setUp()
portal = Mock()
@ -49,9 +50,11 @@ class TestAMPServer(TwistedTestCase):
self.proto.makeConnection(self.transport)
self.proto.data_to_server(MsgServer2Portal, 1, test=2)
byte_out = (b'\x00\x04_ask\x00\x011\x00\x08_command\x00\x10MsgServer2Portal\x00\x0b'
b'packed_data\x00 x\xdak`\x99*\xc8\x00\x01\xde\x8c\xb5SzXJR'
b'\x8bK\xa6x3\x15\xb7M\xd1\x03\x00V:\x07t\x00\x00')
byte_out = (
b"\x00\x04_ask\x00\x011\x00\x08_command\x00\x10MsgServer2Portal\x00\x0b"
b"packed_data\x00 x\xdak`\x99*\xc8\x00\x01\xde\x8c\xb5SzXJR"
b"\x8bK\xa6x3\x15\xb7M\xd1\x03\x00V:\x07t\x00\x00"
)
self.transport.write.assert_called_with(byte_out)
with mock.patch("evennia.server.portal.amp.amp.AMP.dataReceived") as mocked_amprecv:
self.proto.dataReceived(byte_out)
@ -61,9 +64,11 @@ class TestAMPServer(TwistedTestCase):
self.proto.makeConnection(self.transport)
self.proto.data_to_server(MsgPortal2Server, 1, test=2)
byte_out = (b'\x00\x04_ask\x00\x011\x00\x08_command\x00\x10MsgPortal2Server\x00\x0b'
b'packed_data\x00 x\xdak`\x99*\xc8\x00\x01\xde\x8c\xb5SzXJR'
b'\x8bK\xa6x3\x15\xb7M\xd1\x03\x00V:\x07t\x00\x00')
byte_out = (
b"\x00\x04_ask\x00\x011\x00\x08_command\x00\x10MsgPortal2Server\x00\x0b"
b"packed_data\x00 x\xdak`\x99*\xc8\x00\x01\xde\x8c\xb5SzXJR"
b"\x8bK\xa6x3\x15\xb7M\xd1\x03\x00V:\x07t\x00\x00"
)
self.transport.write.assert_called_with(byte_out)
with mock.patch("evennia.server.portal.amp.amp.AMP.dataReceived") as mocked_amprecv:
self.proto.dataReceived(byte_out)
@ -74,52 +79,52 @@ class TestAMPServer(TwistedTestCase):
Send message larger than AMP_MAXLEN - should be split into several
"""
self.proto.makeConnection(self.transport)
outstr = 'test' * AMP_MAXLEN
outstr = "test" * AMP_MAXLEN
self.proto.data_to_server(MsgServer2Portal, 1, test=outstr)
if sys.version < "3.7":
self.transport.write.assert_called_with(
b'\x00\x04_ask\x00\x011\x00\x08_command\x00\x10MsgServer2Portal\x00\x0bpacked_data'
b'\x00xx\xda\xed\xc6\xc1\t\x800\x10\x00\xc1\x13\xaf\x01\xeb\xb2\x01\x1bH'
b"\x00\x04_ask\x00\x011\x00\x08_command\x00\x10MsgServer2Portal\x00\x0bpacked_data"
b"\x00xx\xda\xed\xc6\xc1\t\x800\x10\x00\xc1\x13\xaf\x01\xeb\xb2\x01\x1bH"
b'\x05\xe6+X\x80\xcf\xd8m@I\x1d\x99\x85\x81\xbd\xf3\xdd"c\xb4/W{'
b'\xb2\x96\xb3\xb6\xa3\x7fk\x8c\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00`\x0e?Pv\x02\x16\x00\r'
b'packed_data.2\x00Zx\xda\xed\xc3\x01\r\x00\x00\x08\xc0\xa0\xb4&\xf0\xfdg\x10a'
b'\xa3\xd9RUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU\xf5\xfb'
b'\x03m\xe0\x06\x1d\x00\rpacked_data.3\x00Zx\xda\xed\xc3\x01\r\x00\x00\x08\xc0\xa0'
b'\xb4&\xf0\xfdg\x10a\xa3fSUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU'
b'UUUUU\xf5\xfb\x03n\x1c\x06\x1e\x00\rpacked_data.4\x00Zx\xda\xed\xc3\x01\t\x00'
b'\x00\x0c\x03\xa0\xb4O\xb0\xf5gA\xae`\xda\x8b\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa'
b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa'
b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa'
b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xdf\x0fnI'
b'\x06,\x00\rpacked_data.5\x00\x14x\xdaK-.)I\xc5\x8e\xa7\x14\xb7M\xd1\x03\x00'
b'\xe7s\x0e\x1c\x00\x00')
b"\xb2\x96\xb3\xb6\xa3\x7fk\x8c\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00`\x0e?Pv\x02\x16\x00\r"
b"packed_data.2\x00Zx\xda\xed\xc3\x01\r\x00\x00\x08\xc0\xa0\xb4&\xf0\xfdg\x10a"
b"\xa3\xd9RUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU\xf5\xfb"
b"\x03m\xe0\x06\x1d\x00\rpacked_data.3\x00Zx\xda\xed\xc3\x01\r\x00\x00\x08\xc0\xa0"
b"\xb4&\xf0\xfdg\x10a\xa3fSUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU"
b"UUUUU\xf5\xfb\x03n\x1c\x06\x1e\x00\rpacked_data.4\x00Zx\xda\xed\xc3\x01\t\x00"
b"\x00\x0c\x03\xa0\xb4O\xb0\xf5gA\xae`\xda\x8b\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"
b"\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"
b"\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"
b"\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xdf\x0fnI"
b"\x06,\x00\rpacked_data.5\x00\x14x\xdaK-.)I\xc5\x8e\xa7\x14\xb7M\xd1\x03\x00"
b"\xe7s\x0e\x1c\x00\x00"
)
else:
self.transport.write.assert_called_with(
b'\x00\x04_ask\x00\x011\x00\x08_command\x00\x10MsgServer2Portal\x00\x0bpacked_data'
b'\x00wx\xda\xed\xc6\xc1\t\x80 \x00@Q#o\x8e\xd6\x02-\xe0\x04z\r\x1a\xa0\xa3m+$\xd2'
b'\x18\xbe\x0f\x0f\xfe\x1d\xdf\x14\xfe\x8e\xedjO\xac\xb9\xd4v\xf6o\x0f\xf3\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00X\xc3\x00P\x10\x02\x0c\x00\rpacked_data.2\x00Zx\xda\xed\xc3\x01\r\x00\x00\x08'
b'\xc0\xa0\xb4&\xf0\xfdg\x10a\xa3\xd9RUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU'
b'\xf5\xfb\x03m\xe0\x06\x1d\x00\rpacked_data.3\x00Zx\xda\xed\xc3\x01\r\x00\x00\x08'
b'\xc0\xa0\xb4&\xf0\xfdg\x10a\xa3fSUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU'
b'\xf5\xfb\x03n\x1c\x06\x1e\x00\rpacked_data.4\x00Zx\xda\xed\xc3\x01\t\x00\x00\x0c'
b'\x03\xa0\xb4O\xb0\xf5gA\xae`\xda\x8b\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa'
b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa'
b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa'
b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xdf\x0fnI\x06,\x00\rpacked_data.5'
b'\x00\x18x\xdaK-.)I\xc5\x8e\xa7\xb22@\xc0\x94\xe2\xb6)z\x00Z\x1e\x0e\xb6\x00\x00')
b"\x00\x04_ask\x00\x011\x00\x08_command\x00\x10MsgServer2Portal\x00\x0bpacked_data"
b"\x00wx\xda\xed\xc6\xc1\t\x80 \x00@Q#o\x8e\xd6\x02-\xe0\x04z\r\x1a\xa0\xa3m+$\xd2"
b"\x18\xbe\x0f\x0f\xfe\x1d\xdf\x14\xfe\x8e\xedjO\xac\xb9\xd4v\xf6o\x0f\xf3\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
b"\x00X\xc3\x00P\x10\x02\x0c\x00\rpacked_data.2\x00Zx\xda\xed\xc3\x01\r\x00\x00\x08"
b"\xc0\xa0\xb4&\xf0\xfdg\x10a\xa3\xd9RUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU"
b"\xf5\xfb\x03m\xe0\x06\x1d\x00\rpacked_data.3\x00Zx\xda\xed\xc3\x01\r\x00\x00\x08"
b"\xc0\xa0\xb4&\xf0\xfdg\x10a\xa3fSUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU"
b"\xf5\xfb\x03n\x1c\x06\x1e\x00\rpacked_data.4\x00Zx\xda\xed\xc3\x01\t\x00\x00\x0c"
b"\x03\xa0\xb4O\xb0\xf5gA\xae`\xda\x8b\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"
b"\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"
b"\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa"
b"\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xdf\x0fnI\x06,\x00\rpacked_data.5"
b"\x00\x18x\xdaK-.)I\xc5\x8e\xa7\xb22@\xc0\x94\xe2\xb6)z\x00Z\x1e\x0e\xb6\x00\x00"
)
class TestIRC(TestCase):
def test_plain_ansi(self):
"""
Test that printable characters do not get mangled.
@ -131,40 +136,42 @@ class TestIRC(TestCase):
def test_bold(self):
s_irc = "\x02thisisatest"
s_eve = r'|hthisisatest'
s_eve = r"|hthisisatest"
self.assertEqual(irc.parse_ansi_to_irc(s_eve), s_irc)
self.assertEqual(s_eve, irc.parse_irc_to_ansi(s_irc))
def test_italic(self):
s_irc = "\x02thisisatest"
s_eve = r'|hthisisatest'
s_eve = r"|hthisisatest"
self.assertEqual(irc.parse_ansi_to_irc(s_eve), s_irc)
def test_colors(self):
color_map = (("\0030", r'|w'),
("\0031", r'|X'),
("\0032", r'|B'),
("\0033", r'|G'),
("\0034", r'|r'),
("\0035", r'|R'),
("\0036", r'|M'),
("\0037", r'|Y'),
("\0038", r'|y'),
("\0039", r'|g'),
("\00310", r'|C'),
("\00311", r'|c'),
("\00312", r'|b'),
("\00313", r'|m'),
("\00314", r'|x'),
("\00315", r'|W'),
("\00399,5", r'|[r'),
("\00399,3", r'|[g'),
("\00399,7", r'|[y'),
("\00399,2", r'|[b'),
("\00399,6", r'|[m'),
("\00399,10", r'|[c'),
("\00399,15", r'|[w'),
("\00399,1", r'|[x'))
color_map = (
("\0030", r"|w"),
("\0031", r"|X"),
("\0032", r"|B"),
("\0033", r"|G"),
("\0034", r"|r"),
("\0035", r"|R"),
("\0036", r"|M"),
("\0037", r"|Y"),
("\0038", r"|y"),
("\0039", r"|g"),
("\00310", r"|C"),
("\00311", r"|c"),
("\00312", r"|b"),
("\00313", r"|m"),
("\00314", r"|x"),
("\00315", r"|W"),
("\00399,5", r"|[r"),
("\00399,3", r"|[g"),
("\00399,7", r"|[y"),
("\00399,2", r"|[b"),
("\00399,6", r"|[m"),
("\00399,10", r"|[c"),
("\00399,15", r"|[w"),
("\00399,1", r"|[x"),
)
for m in color_map:
self.assertEqual(irc.parse_irc_to_ansi(m[0]), m[1])
@ -176,7 +183,7 @@ class TestIRC(TestCase):
its inverse gives the correct string.
"""
s = r'|wthis|Xis|gis|Ma|C|complex|*string'
s = r"|wthis|Xis|gis|Ma|C|complex|*string"
self.assertEqual(irc.parse_irc_to_ansi(irc.parse_ansi_to_irc(s)), s)
@ -202,12 +209,12 @@ class TestTelnet(TwistedTestCase):
self.assertFalse(self.proto.protocol_flags["NOGOAHEAD"])
self.assertEqual(self.proto.handshakes, 7)
# test naws
self.assertEqual(self.proto.protocol_flags['SCREENWIDTH'], {0: DEFAULT_WIDTH})
self.assertEqual(self.proto.protocol_flags['SCREENHEIGHT'], {0: DEFAULT_HEIGHT})
self.assertEqual(self.proto.protocol_flags["SCREENWIDTH"], {0: DEFAULT_WIDTH})
self.assertEqual(self.proto.protocol_flags["SCREENHEIGHT"], {0: DEFAULT_HEIGHT})
self.proto.dataReceived(IAC + WILL + NAWS)
self.proto.dataReceived(b"".join([IAC, SB, NAWS, b'', b'x', b'', b'd', IAC, SE]))
self.assertEqual(self.proto.protocol_flags['SCREENWIDTH'][0], 78)
self.assertEqual(self.proto.protocol_flags['SCREENHEIGHT'][0], 45)
self.proto.dataReceived(b"".join([IAC, SB, NAWS, b"", b"x", b"", b"d", IAC, SE]))
self.assertEqual(self.proto.protocol_flags["SCREENWIDTH"][0], 78)
self.assertEqual(self.proto.protocol_flags["SCREENHEIGHT"][0], 45)
self.assertEqual(self.proto.handshakes, 6)
# test ttype
self.assertTrue(self.proto.protocol_flags["FORCEDENDLINE"])
@ -222,19 +229,21 @@ class TestTelnet(TwistedTestCase):
self.assertEqual(self.proto.handshakes, 5)
# test mccp
self.proto.dataReceived(IAC + DONT + MCCP)
self.assertFalse(self.proto.protocol_flags['MCCP'])
self.assertFalse(self.proto.protocol_flags["MCCP"])
self.assertEqual(self.proto.handshakes, 4)
# test mssp
self.proto.dataReceived(IAC + DONT + MSSP)
self.assertEqual(self.proto.handshakes, 3)
# test oob
self.proto.dataReceived(IAC + DO + MSDP)
self.proto.dataReceived(b"".join([IAC, SB, MSDP, MSDP_VAR, b"LIST", MSDP_VAL, b"COMMANDS", IAC, SE]))
self.assertTrue(self.proto.protocol_flags['OOB'])
self.proto.dataReceived(
b"".join([IAC, SB, MSDP, MSDP_VAR, b"LIST", MSDP_VAL, b"COMMANDS", IAC, SE])
)
self.assertTrue(self.proto.protocol_flags["OOB"])
self.assertEqual(self.proto.handshakes, 2)
# test mxp
self.proto.dataReceived(IAC + DONT + MXP)
self.assertFalse(self.proto.protocol_flags['MXP'])
self.assertFalse(self.proto.protocol_flags["MXP"])
self.assertEqual(self.proto.handshakes, 1)
# clean up to prevent Unclean reactor
self.proto.nop_keep_alive.stop()

View file

@ -12,19 +12,21 @@ under the 'TTYPE' key.
"""
# telnet option codes
TTYPE = b'\x18'
IS = b'\x00'
SEND = b'\x01'
TTYPE = b"\x18"
IS = b"\x00"
SEND = b"\x01"
# terminal capabilities and their codes
MTTS = [(128, 'PROXY'),
(64, 'SCREENREADER'),
(32, 'OSC_COLOR_PALETTE'),
(16, 'MOUSE_TRACKING'),
(8, 'XTERM256'),
(4, 'UTF-8'),
(2, 'VT100'),
(1, 'ANSI')]
MTTS = [
(128, "PROXY"),
(64, "SCREENREADER"),
(32, "OSC_COLOR_PALETTE"),
(16, "MOUSE_TRACKING"),
(8, "XTERM256"),
(4, "UTF-8"),
(2, "VT100"),
(1, "ANSI"),
]
class Ttype(object):
@ -51,9 +53,9 @@ class Ttype(object):
self.protocol = protocol
# we set FORCEDENDLINE for clients not supporting ttype
self.protocol.protocol_flags["FORCEDENDLINE"] = True
self.protocol.protocol_flags['TTYPE'] = False
self.protocol.protocol_flags["TTYPE"] = False
# is it a safe bet to assume ANSI is always supported?
self.protocol.protocol_flags['ANSI'] = True
self.protocol.protocol_flags["ANSI"] = True
# setup protocol to handle ttype initialization and negotiation
self.protocol.negotiationMap[TTYPE] = self.will_ttype
# ask if client will ttype, connect callback if it does.
@ -67,7 +69,7 @@ class Ttype(object):
option (Option): Not used.
"""
self.protocol.protocol_flags['TTYPE'] = False
self.protocol.protocol_flags["TTYPE"] = False
self.protocol.handshake_done()
def will_ttype(self, option):
@ -86,7 +88,7 @@ class Ttype(object):
"""
options = self.protocol.protocol_flags
if options and options.get('TTYPE', False) or self.ttype_step > 3:
if options and options.get("TTYPE", False) or self.ttype_step > 3:
return
try:
@ -121,25 +123,28 @@ class Ttype(object):
if clientname.startswith("TINTIN++"):
self.protocol.protocol_flags["FORCEDENDLINE"] = True
if (clientname.startswith("XTERM") or
clientname.endswith("-256COLOR") or
clientname in (
"ATLANTIS", # > 0.9.9.0 (aug 2009)
"CMUD", # > 3.04 (mar 2009)
"KILDCLIENT", # > 2.2.0 (sep 2005)
"MUDLET", # > beta 15 (sep 2009)
"MUSHCLIENT", # > 4.02 (apr 2007)
"PUTTY", # > 0.58 (apr 2005)
"BEIP", # > 2.00.206 (late 2009) (BeipMu)
"POTATO", # > 2.00 (maybe earlier)
"TINYFUGUE" # > 4.x (maybe earlier)
)):
xterm256 = True
if (
clientname.startswith("XTERM")
or clientname.endswith("-256COLOR")
or clientname
in (
"ATLANTIS", # > 0.9.9.0 (aug 2009)
"CMUD", # > 3.04 (mar 2009)
"KILDCLIENT", # > 2.2.0 (sep 2005)
"MUDLET", # > beta 15 (sep 2009)
"MUSHCLIENT", # > 4.02 (apr 2007)
"PUTTY", # > 0.58 (apr 2005)
"BEIP", # > 2.00.206 (late 2009) (BeipMu)
"POTATO", # > 2.00 (maybe earlier)
"TINYFUGUE", # > 4.x (maybe earlier)
)
):
xterm256 = True
# all clients supporting TTYPE at all seem to support ANSI
self.protocol.protocol_flags['ANSI'] = True
self.protocol.protocol_flags['XTERM256'] = xterm256
self.protocol.protocol_flags['CLIENTNAME'] = clientname
self.protocol.protocol_flags["ANSI"] = True
self.protocol.protocol_flags["XTERM256"] = xterm256
self.protocol.protocol_flags["CLIENTNAME"] = clientname
self.protocol.requestNegotiation(TTYPE, SEND)
elif self.ttype_step == 2:
@ -147,13 +152,15 @@ class Ttype(object):
term = option
tupper = term.upper()
# identify xterm256 based on flag
xterm256 = (tupper.endswith("-256COLOR") or # Apple Terminal, old Tintin
tupper.endswith("XTERM") and # old Tintin, Putty
not tupper.endswith("-COLOR"))
xterm256 = (
tupper.endswith("-256COLOR")
or tupper.endswith("XTERM") # Apple Terminal, old Tintin
and not tupper.endswith("-COLOR") # old Tintin, Putty
)
if xterm256:
self.protocol.protocol_flags['ANSI'] = True
self.protocol.protocol_flags['XTERM256'] = xterm256
self.protocol.protocol_flags['TERM'] = term
self.protocol.protocol_flags["ANSI"] = True
self.protocol.protocol_flags["XTERM256"] = xterm256
self.protocol.protocol_flags["TERM"] = term
# request next information
self.protocol.requestNegotiation(TTYPE, SEND)
@ -164,13 +171,15 @@ class Ttype(object):
if option.isdigit():
# a number - determine the actual capabilities
option = int(option)
support = dict((capability, True) for bitval, capability in MTTS if option & bitval > 0)
support = dict(
(capability, True) for bitval, capability in MTTS if option & bitval > 0
)
self.protocol.protocol_flags.update(support)
else:
# some clients send erroneous MTTS as a string. Add directly.
self.protocol.protocol_flags[option.upper()] = True
self.protocol.protocol_flags['TTYPE'] = True
self.protocol.protocol_flags["TTYPE"] = True
# we must sync ttype once it'd done
self.protocol.handshake_done()
self.ttype_step += 1

View file

@ -25,7 +25,9 @@ from evennia.utils.ansi import parse_ansi
from evennia.utils.text2html import parse_html
from autobahn.twisted.websocket import WebSocketServerProtocol
_RE_SCREENREADER_REGEX = re.compile(r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE)
_RE_SCREENREADER_REGEX = re.compile(
r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE
)
_CLIENT_SESSIONS = mod_import(settings.SESSION_ENGINE).SessionStore
@ -36,6 +38,7 @@ class WebSocketClient(WebSocketServerProtocol, Session):
"""
Implements the server-side of the Websocket connection.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.protocol_key = "webclient/websocket"
@ -58,6 +61,7 @@ class WebSocketClient(WebSocketServerProtocol, Session):
return None
except AttributeError:
from evennia.utils import logger
self.csessid = None
logger.log_trace(str(self))
return None
@ -82,8 +86,10 @@ class WebSocketClient(WebSocketServerProtocol, Session):
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 (
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)
@ -143,7 +149,7 @@ class WebSocketClient(WebSocketServerProtocol, Session):
UTF-8 encoded text.
"""
cmdarray = json.loads(str(payload, 'utf-8'))
cmdarray = json.loads(str(payload, "utf-8"))
if cmdarray:
self.data_in(**{cmdarray[0]: [cmdarray[1], cmdarray[2]]})

View file

@ -32,7 +32,9 @@ from evennia.utils.text2html import parse_html
from evennia.server import session
_CLIENT_SESSIONS = utils.mod_import(settings.SESSION_ENGINE).SessionStore
_RE_SCREENREADER_REGEX = re.compile(r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE)
_RE_SCREENREADER_REGEX = re.compile(
r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE
)
_SERVERNAME = settings.SERVERNAME
_KEEPALIVE = 30 # how often to check keepalive
@ -42,6 +44,7 @@ _KEEPALIVE = 30 # how often to check keepalive
# extend this if one wants to send more
# complex database objects too.
class LazyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Promise):
@ -58,13 +61,15 @@ def jsonify(obj):
# using POST requests to /webclientdata.
#
class AjaxWebClient(resource.Resource):
"""
An ajax/comet long-polling transport
"""
isLeaf = True
allowedMethods = ('POST',)
allowedMethods = ("POST",)
def __init__(self):
self.requests = {}
@ -87,8 +92,11 @@ class AjaxWebClient(resource.Resource):
"""
now = time.time()
to_remove = []
keep_alives = ((csessid, remove) for csessid, (t, remove)
in self.last_alive.items() if now - t > _KEEPALIVE)
keep_alives = (
(csessid, remove)
for csessid, (t, remove) in self.last_alive.items()
if now - t > _KEEPALIVE
)
for csessid, remove in keep_alives:
if remove:
# keepalive timeout. Line is dead.
@ -118,7 +126,7 @@ class AjaxWebClient(resource.Resource):
csessid (int): The client-session id.
"""
return html.escape(request.args[b'csessid'][0].decode("utf-8"))
return html.escape(request.args[b"csessid"][0].decode("utf-8"))
def at_login(self):
"""
@ -174,9 +182,11 @@ class AjaxWebClient(resource.Resource):
csessid = self.get_client_sessid(request)
remote_addr = request.getClientIP()
host_string = "%s (%s:%s)" % (_SERVERNAME,
request.getRequestHostname(),
request.getHost().port)
host_string = "%s (%s:%s)" % (
_SERVERNAME,
request.getRequestHostname(),
request.getHost().port,
)
sess = AjaxWebClientSession()
sess.client = self
@ -200,7 +210,7 @@ class AjaxWebClient(resource.Resource):
# actually do the connection
sess.sessionhandler.connect(sess)
return jsonify({'msg': host_string, 'csessid': csessid})
return jsonify({"msg": host_string, "csessid": csessid})
def mode_keepalive(self, request):
@ -223,7 +233,7 @@ class AjaxWebClient(resource.Resource):
"""
csessid = self.get_client_sessid(request)
self.last_alive[csessid] = (time.time(), False)
cmdarray = json.loads(request.args.get(b'data')[0])
cmdarray = json.loads(request.args.get(b"data")[0])
for sess in self.sessionhandler.sessions_from_csessid(csessid):
sess.data_in(**{cmdarray[0]: [cmdarray[1], cmdarray[2]]})
return b'""'
@ -239,7 +249,7 @@ class AjaxWebClient(resource.Resource):
request (Request): Incoming request.
"""
csessid = html.escape(request.args[b'csessid'][0].decode("utf-8"))
csessid = html.escape(request.args[b"csessid"][0].decode("utf-8"))
self.last_alive[csessid] = (time.time(), False)
dataentries = self.databuffer.get(csessid)
@ -286,21 +296,21 @@ class AjaxWebClient(resource.Resource):
request (Request): Incoming request.
"""
dmode = request.args.get(b'mode', [b'None'])[0].decode("utf-8")
dmode = request.args.get(b"mode", [b"None"])[0].decode("utf-8")
if dmode == 'init':
if dmode == "init":
# startup. Setup the server.
return self.mode_init(request)
elif dmode == 'input':
elif dmode == "input":
# input from the client to the server
return self.mode_input(request)
elif dmode == 'receive':
elif dmode == "receive":
# the client is waiting to receive data.
return self.mode_receive(request)
elif dmode == 'close':
elif dmode == "close":
# the client is closing
return self.mode_close(request)
elif dmode == 'keepalive':
elif dmode == "keepalive":
# A reply to our keepalive request - all is well
return self.mode_keepalive(request)
else:
@ -313,6 +323,7 @@ class AjaxWebClient(resource.Resource):
# web client interface.
#
class AjaxWebClientSession(session.Session):
"""
This represents a session running in an AjaxWebclient.
@ -405,8 +416,8 @@ class AjaxWebClientSession(session.Session):
options = kwargs.pop("options", {})
raw = options.get("raw", flags.get("RAW", False))
xterm256 = options.get("xterm256", flags.get('XTERM256', True))
useansi = options.get("ansi", flags.get('ANSI', True))
xterm256 = options.get("xterm256", flags.get("XTERM256", True))
useansi = options.get("ansi", flags.get("ANSI", True))
nocolor = options.get("nocolor", flags.get("NOCOLOR") or not (xterm256 or useansi))
screenreader = options.get("screenreader", flags.get("SCREENREADER", False))
prompt = options.get("send_prompt", False)

View file

@ -47,8 +47,10 @@ from evennia.utils import mod_import, time_format
DUMMYRUNNER_SETTINGS = mod_import(settings.DUMMYRUNNER_SETTINGS_MODULE)
if not DUMMYRUNNER_SETTINGS:
raise IOError("Error: Dummyrunner could not find settings file at %s" %
settings.DUMMYRUNNER_SETTINGS_MODULE)
raise IOError(
"Error: Dummyrunner could not find settings file at %s"
% settings.DUMMYRUNNER_SETTINGS_MODULE
)
DATESTRING = "%Y%m%d%H%M%S"
@ -75,8 +77,7 @@ NLOGGED_IN = 0
# Messages
INFO_STARTING = \
"""
INFO_STARTING = """
Dummyrunner starting using {N} dummy account(s). If you don't see
any connection messages, make sure that the Evennia server is
running.
@ -84,8 +85,7 @@ INFO_STARTING = \
Use Ctrl-C to stop/disconnect clients.
"""
ERROR_NO_MIXIN = \
"""
ERROR_NO_MIXIN = """
Error: Evennia is not set up for dummyrunner. Before starting the
server, make sure to include the following at *the end* of your
settings file (remove when not using dummyrunner!):
@ -108,8 +108,7 @@ ERROR_NO_MIXIN = \
"""
ERROR_FEW_ACTIONS = \
"""
ERROR_FEW_ACTIONS = """
Dummyrunner settings error: The ACTIONS tuple is too short: it must
contain at least login- and logout functions.
"""
@ -169,9 +168,9 @@ until you see the initial login slows things too much.
"""
#------------------------------------------------------------
# ------------------------------------------------------------
# Helper functions
#------------------------------------------------------------
# ------------------------------------------------------------
ICOUNT = 0
@ -216,11 +215,12 @@ def makeiter(obj):
Returns:
iterable (iterable): An iterable object.
"""
return obj if hasattr(obj, '__iter__') else [obj]
return obj if hasattr(obj, "__iter__") else [obj]
#------------------------------------------------------------
# ------------------------------------------------------------
# Client classes
#------------------------------------------------------------
# ------------------------------------------------------------
class DummyClient(telnet.StatefulTelnetProtocol):
@ -253,7 +253,7 @@ class DummyClient(telnet.StatefulTelnetProtocol):
self._logout = self.factory.actions[1]
self._actions = self.factory.actions[2:]
reactor.addSystemEventTrigger('before', 'shutdown', self.logout)
reactor.addSystemEventTrigger("before", "shutdown", self.logout)
def dataReceived(self, data):
"""
@ -358,10 +358,11 @@ class DummyFactory(protocol.ClientFactory):
"Setup the factory base (shared by all clients)"
self.actions = actions
#------------------------------------------------------------
# ------------------------------------------------------------
# Access method:
# Starts clients and connects them to a running server.
#------------------------------------------------------------
# ------------------------------------------------------------
def start_all_dummy_clients(nclients):
@ -382,7 +383,12 @@ def start_all_dummy_clients(nclients):
# make sure the probabilities add up to 1
pratio = 1.0 / sum(tup[0] for tup in actions[2:])
flogin, flogout, probs, cfuncs = actions[0], actions[1], [tup[0] * pratio for tup in actions[2:]], [tup[1] for tup in actions[2:]]
flogin, flogout, probs, cfuncs = (
actions[0],
actions[1],
[tup[0] * pratio for tup in actions[2:]],
[tup[1] for tup in actions[2:]],
)
# create cumulative probabilies for the random actions
cprobs = [sum(v for i, v in enumerate(probs) if i <= k) for k in range(len(probs))]
# rebuild a new, optimized action structure
@ -395,12 +401,13 @@ def start_all_dummy_clients(nclients):
# start reactor
reactor.run()
#------------------------------------------------------------
# ------------------------------------------------------------
# Command line interface
#------------------------------------------------------------
# ------------------------------------------------------------
if __name__ == '__main__':
if __name__ == "__main__":
try:
settings.DUMMYRUNNER_MIXIN
@ -410,8 +417,9 @@ if __name__ == '__main__':
# parsing command line with default vals
parser = ArgumentParser(description=HELPTEXT)
parser.add_argument("-N", nargs=1, default=1, dest="nclients",
help="Number of clients to start")
parser.add_argument(
"-N", nargs=1, default=1, dest="nclients", help="Number of clients to start"
)
args = parser.parse_args()

View file

@ -89,6 +89,7 @@ TOBJ_TYPECLASS = "contrib.tutorial_examples.red_button.RedButton"
# login/logout
def c_login(client):
"logins to the game"
# we always use a new client name
@ -102,12 +103,13 @@ def c_login(client):
exitname2 = EXIT_TEMPLATE % client.counter()
client.exits.extend([exitname1, exitname2])
cmds = ('create %s %s' % (cname, cpwd),
'connect %s %s' % (cname, cpwd),
'@dig %s' % START_ROOM % client.gid,
'@teleport %s' % START_ROOM % client.gid,
'@dig %s = %s, %s' % (roomname, exitname1, exitname2)
)
cmds = (
"create %s %s" % (cname, cpwd),
"connect %s %s" % (cname, cpwd),
"@dig %s" % START_ROOM % client.gid,
"@teleport %s" % START_ROOM % client.gid,
"@dig %s = %s, %s" % (roomname, exitname1, exitname2),
)
return cmds
@ -116,8 +118,7 @@ def c_login_nodig(client):
cname = DUMMY_NAME % client.gid
cpwd = DUMMY_PWD % client.gid
cmds = ('create %s %s' % (cname, cpwd),
'connect %s %s' % (cname, cpwd))
cmds = ("create %s %s" % (cname, cpwd), "connect %s %s" % (cname, cpwd))
return cmds
@ -125,6 +126,7 @@ def c_logout(client):
"logouts of the game"
return "@quit"
# random commands
@ -150,17 +152,13 @@ def c_examines(client):
def c_idles(client):
"idles"
cmds = ('idle', 'idle')
cmds = ("idle", "idle")
return cmds
def c_help(client):
"reads help files"
cmds = ('help',
'help @teleport',
'help look',
'help @tunnel',
'help @dig')
cmds = ("help", "help @teleport", "help look", "help @tunnel", "help @dig")
return cmds
@ -170,17 +168,19 @@ def c_digs(client):
exitname1 = EXIT_TEMPLATE % client.counter()
exitname2 = EXIT_TEMPLATE % client.counter()
client.exits.extend([exitname1, exitname2])
return '@dig/tel %s = %s, %s' % (roomname, exitname1, exitname2)
return "@dig/tel %s = %s, %s" % (roomname, exitname1, exitname2)
def c_creates_obj(client):
"creates normal objects, storing their name on client"
objname = OBJ_TEMPLATE % client.counter()
client.objs.append(objname)
cmds = ('@create %s' % objname,
'@desc %s = "this is a test object' % objname,
'@set %s/testattr = this is a test attribute value.' % objname,
'@set %s/testattr2 = this is a second test attribute.' % objname)
cmds = (
"@create %s" % objname,
'@desc %s = "this is a test object' % objname,
"@set %s/testattr = this is a test attribute value." % objname,
"@set %s/testattr2 = this is a second test attribute." % objname,
)
return cmds
@ -188,18 +188,19 @@ def c_creates_button(client):
"creates example button, storing name on client"
objname = TOBJ_TEMPLATE % client.counter()
client.objs.append(objname)
cmds = ('@create %s:%s' % (objname, TOBJ_TYPECLASS),
'@desc %s = test red button!' % objname)
cmds = ("@create %s:%s" % (objname, TOBJ_TYPECLASS), "@desc %s = test red button!" % objname)
return cmds
def c_socialize(client):
"socializechats on channel"
cmds = ('ooc Hello!',
'ooc Testing ...',
'ooc Testing ... times 2',
'say Yo!',
'emote stands looking around.')
cmds = (
"ooc Hello!",
"ooc Testing ...",
"ooc Testing ... times 2",
"say Yo!",
"emote stands looking around.",
)
return cmds
@ -218,6 +219,7 @@ def c_moves_s(client):
"move through south exit if available"
return "south"
# Action tuple (required)
#
# This is a tuple of client action functions. The first element is the
@ -263,12 +265,7 @@ def c_moves_s(client):
# c_logout,
# (1.0, c_idles))
# "normal account" definition
ACTIONS = (c_login,
c_logout,
(0.01, c_digs),
(0.39, c_looks),
(0.2, c_help),
(0.4, c_moves))
ACTIONS = (c_login, c_logout, (0.01, c_digs), (0.39, c_looks), (0.2, c_help), (0.4, c_moves))
# walking tester. This requires a pre-made
# "loop" of multiple rooms that ties back
# to limbo (using @tunnel and @open)

View file

@ -10,9 +10,10 @@ Call this module directly to plot the log (requires matplotlib and numpy).
import os
import sys
import time
# TODO!
#sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
#os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings'
# sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
# os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings'
import evennia
from evennia.utils.idmapper import models as _idmapper
@ -38,8 +39,12 @@ class Memplot(evennia.DefaultScript):
def at_repeat(self):
"Regularly save memory statistics."
pid = os.getpid()
rmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "rss")).read()) / 1000.0 # resident memory
vmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "vsz")).read()) / 1000.0 # virtual memory
rmem = (
float(os.popen("ps -p %d -o %s | tail -1" % (pid, "rss")).read()) / 1000.0
) # resident memory
vmem = (
float(os.popen("ps -p %d -o %s | tail -1" % (pid, "vsz")).read()) / 1000.0
) # virtual memory
total_num, cachedict = _idmapper.cache_size()
t0 = (time.time() - self.db.starttime) / 60.0 # save in minutes
@ -61,8 +66,8 @@ if __name__ == "__main__":
nobj = data[:, 3]
# calculate derivative of obj creation
#oderiv = (0.5*(nobj[2:] - nobj[:-2]) / (secs[2:] - secs[:-2])).copy()
#oderiv = (0.5*(rmem[2:] - rmem[:-2]) / (secs[2:] - secs[:-2])).copy()
# oderiv = (0.5*(nobj[2:] - nobj[:-2]) / (secs[2:] - secs[:-2])).copy()
# oderiv = (0.5*(rmem[2:] - rmem[:-2]) / (secs[2:] - secs[:-2])).copy()
fig = pp.figure()
ax1 = fig.add_subplot(111)
@ -75,37 +80,37 @@ if __name__ == "__main__":
ax2 = ax1.twinx()
ax2.plot(secs, nobj, "g--", label="objs in cache", lw=2)
#ax2.plot(secs[:-2], oderiv/60.0, "g--", label="Objs/second", lw=2)
#ax2.plot(secs[:-2], oderiv, "g--", label="Objs/second", lw=2)
# ax2.plot(secs[:-2], oderiv/60.0, "g--", label="Objs/second", lw=2)
# ax2.plot(secs[:-2], oderiv, "g--", label="Objs/second", lw=2)
ax2.set_ylabel("Number of objects")
ax2.legend(loc="lower right")
ax2.annotate("First 500 bots\nconnecting", xy=(10, 4000))
ax2.annotate("Next 500 bots\nconnecting", xy=(350, 10000))
#ax2.annotate("@reload", xy=(185,600))
# ax2.annotate("@reload", xy=(185,600))
# # plot mem vs cachesize
# nobj, rmem, vmem = nobj[:262].copy(), rmem[:262].copy(), vmem[:262].copy()
#
# fig = pp.figure()
# ax1 = fig.add_subplot(111)
# ax1.set_title("Memory usage per cache size")
# ax1.set_xlabel("Cache size (number of objects)")
# ax1.set_ylabel("Memory usage (MB)")
# ax1.plot(nobj, rmem, "r", label="RMEM", lw=2)
# ax1.plot(nobj, vmem, "b", label="VMEM", lw=2)
#
# # plot mem vs cachesize
# nobj, rmem, vmem = nobj[:262].copy(), rmem[:262].copy(), vmem[:262].copy()
#
# fig = pp.figure()
# ax1 = fig.add_subplot(111)
# ax1.set_title("Memory usage per cache size")
# ax1.set_xlabel("Cache size (number of objects)")
# ax1.set_ylabel("Memory usage (MB)")
# ax1.plot(nobj, rmem, "r", label="RMEM", lw=2)
# ax1.plot(nobj, vmem, "b", label="VMEM", lw=2)
#
# empirical estimate of memory usage: rmem = 35.0 + 0.0157 * Ncache
# Ncache = int((rmem - 35.0) / 0.0157) (rmem in MB)
#
# rderiv_aver = 0.0157
# fig = pp.figure()
# ax1 = fig.add_subplot(111)
# ax1.set_title("Relation between memory and cache size")
# ax1.set_xlabel("Memory usage (MB)")
# ax1.set_ylabel("Idmapper Cache Size (number of objects)")
# rmem = numpy.linspace(35, 2000, 2000)
# nobjs = numpy.array([int((mem - 35.0) / 0.0157) for mem in rmem])
# ax1.plot(rmem, nobjs, "r", lw=2)
# empirical estimate of memory usage: rmem = 35.0 + 0.0157 * Ncache
# Ncache = int((rmem - 35.0) / 0.0157) (rmem in MB)
#
# rderiv_aver = 0.0157
# fig = pp.figure()
# ax1 = fig.add_subplot(111)
# ax1.set_title("Relation between memory and cache size")
# ax1.set_xlabel("Memory usage (MB)")
# ax1.set_ylabel("Idmapper Cache Size (number of objects)")
# rmem = numpy.linspace(35, 2000, 2000)
# nobjs = numpy.array([int((mem - 35.0) / 0.0157) for mem in rmem])
# ax1.plot(rmem, nobjs, "r", lw=2)
pp.show()

View file

@ -14,8 +14,6 @@ DUMMYRUNNER_MIXIN = True
# a faster password hasher suitable for multiple simultaneous
# account creations. The default one is safer but deliberately
# very slow to make cracking harder.
PASSWORD_HASHERS = (
'django.contrib.auth.hashers.MD5PasswordHasher',
)
PASSWORD_HASHERS = ("django.contrib.auth.hashers.MD5PasswordHasher",)
# make dummy clients able to test all commands
PERMISSION_ACCOUNT_DEFAULT = "Developer"

View file

@ -6,8 +6,9 @@ query as well as count them for optimization testing.
import sys
import os
#sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
#os.environ["DJANGO_SETTINGS_MODULE"] = "game.settings"
# sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
# os.environ["DJANGO_SETTINGS_MODULE"] = "game.settings"
from django.db import connection
@ -23,7 +24,7 @@ def count_queries(exec_string, setup_string):
exec(exec_string)
nqueries = len(connection.queries) - num_queries_old
for query in connection.queries[-nqueries if nqueries else 1:]:
for query in connection.queries[-nqueries if nqueries else 1 :]:
print(query["time"], query["sql"])
print("Number of queries: %s" % nqueries)
@ -32,13 +33,11 @@ if __name__ == "__main__":
# setup tests here
setup_string = \
"""
setup_string = """
from evennia.objects.models import ObjectDB
g = ObjectDB.objects.get(db_key="Griatch")
"""
exec_string = \
"""
exec_string = """
g.tags.all()
"""
count_queries(exec_string, setup_string)

View file

@ -1,7 +1,21 @@
from django.test import TestCase
from mock import Mock, patch, mock_open
from .dummyrunner_settings import (c_creates_button, c_creates_obj, c_digs, c_examines, c_help, c_idles, c_login,
c_login_nodig, c_logout, c_looks, c_moves, c_moves_n, c_moves_s, c_socialize)
from .dummyrunner_settings import (
c_creates_button,
c_creates_obj,
c_digs,
c_examines,
c_help,
c_idles,
c_login,
c_login_nodig,
c_logout,
c_looks,
c_moves,
c_moves_n,
c_moves_s,
c_socialize,
)
try:
import memplot
@ -26,15 +40,25 @@ class TestDummyrunnerSettings(TestCase):
self.client.exits = []
def test_c_login(self):
self.assertEqual(c_login(self.client), ('create %s %s' % (self.client.name, self.client.password),
'connect %s %s' % (self.client.name, self.client.password),
'@dig %s' % self.client.start_room,
'@teleport %s' % self.client.start_room,
"@dig testing_room_1 = exit_1, exit_1"))
self.assertEqual(
c_login(self.client),
(
"create %s %s" % (self.client.name, self.client.password),
"connect %s %s" % (self.client.name, self.client.password),
"@dig %s" % self.client.start_room,
"@teleport %s" % self.client.start_room,
"@dig testing_room_1 = exit_1, exit_1",
),
)
def test_c_login_no_dig(self):
self.assertEqual(c_login_nodig(self.client), ('create %s %s' % (self.client.name, self.client.password),
'connect %s %s' % (self.client.name, self.client.password)))
self.assertEqual(
c_login_nodig(self.client),
(
"create %s %s" % (self.client.name, self.client.password),
"connect %s %s" % (self.client.name, self.client.password),
),
)
def test_c_logout(self):
self.assertEqual(c_logout(self.client), "@quit")
@ -54,36 +78,54 @@ class TestDummyrunnerSettings(TestCase):
self.perception_method_tests(c_examines, "examine", " me")
def test_idles(self):
self.assertEqual(c_idles(self.client), ('idle', 'idle'))
self.assertEqual(c_idles(self.client), ("idle", "idle"))
def test_c_help(self):
self.assertEqual(c_help(self.client), ('help', 'help @teleport', 'help look', 'help @tunnel', 'help @dig'))
self.assertEqual(
c_help(self.client),
("help", "help @teleport", "help look", "help @tunnel", "help @dig"),
)
def test_c_digs(self):
self.assertEqual(c_digs(self.client), ('@dig/tel testing_room_1 = exit_1, exit_1'))
self.assertEqual(self.client.exits, ['exit_1', 'exit_1'])
self.assertEqual(c_digs(self.client), ("@dig/tel testing_room_1 = exit_1, exit_1"))
self.assertEqual(self.client.exits, ["exit_1", "exit_1"])
self.clear_client_lists()
def test_c_creates_obj(self):
objname = "testing_obj_1"
self.assertEqual(c_creates_obj(self.client), ('@create %s' % objname,
'@desc %s = "this is a test object' % objname,
'@set %s/testattr = this is a test attribute value.' % objname,
'@set %s/testattr2 = this is a second test attribute.' % objname))
self.assertEqual(
c_creates_obj(self.client),
(
"@create %s" % objname,
'@desc %s = "this is a test object' % objname,
"@set %s/testattr = this is a test attribute value." % objname,
"@set %s/testattr2 = this is a second test attribute." % objname,
),
)
self.assertEqual(self.client.objs, [objname])
self.clear_client_lists()
def test_c_creates_button(self):
objname = "testing_button_1"
typeclass_name = "contrib.tutorial_examples.red_button.RedButton"
self.assertEqual(c_creates_button(self.client), ('@create %s:%s' % (objname, typeclass_name),
'@desc %s = test red button!' % objname))
self.assertEqual(
c_creates_button(self.client),
("@create %s:%s" % (objname, typeclass_name), "@desc %s = test red button!" % objname),
)
self.assertEqual(self.client.objs, [objname])
self.clear_client_lists()
def test_c_socialize(self):
self.assertEqual(c_socialize(self.client), ('ooc Hello!', 'ooc Testing ...', 'ooc Testing ... times 2',
'say Yo!', 'emote stands looking around.'))
self.assertEqual(
c_socialize(self.client),
(
"ooc Hello!",
"ooc Testing ...",
"ooc Testing ... times 2",
"say Yo!",
"emote stands looking around.",
),
)
def test_c_moves(self):
self.assertEqual(c_moves(self.client), "look")
@ -108,6 +150,7 @@ class TestMemPlot(TestCase):
if isinstance(memplot, Mock):
return
from evennia.utils.create import create_script
mocked_idmapper.cache_size.return_value = (9, 5000)
mock_time.time = Mock(return_value=6000.0)
script = create_script(memplot.Memplot)
@ -115,5 +158,5 @@ class TestMemPlot(TestCase):
mocked_os.popen.read.return_value = 5000.0
script.at_repeat()
handle = mocked_open()
handle.write.assert_called_with('100.0, 0.001, 0.001, 9\n')
handle.write.assert_called_with("100.0, 0.001, 0.001, 9\n")
script.stop()

View file

@ -18,9 +18,11 @@ from twisted.internet.task import LoopingCall
from twisted.python.log import ILogObserver
import django
django.setup()
import evennia
evennia._init()
from django.db import connection
@ -41,7 +43,7 @@ from django.utils.translation import ugettext as _
_SA = object.__setattr__
# a file with a flag telling the server to restart after shutdown or not.
SERVER_RESTART = os.path.join(settings.GAME_DIR, "server", 'server.restart')
SERVER_RESTART = os.path.join(settings.GAME_DIR, "server", "server.restart")
# module containing hook methods called during start_stop
SERVER_STARTSTOP_MODULE = mod_import(settings.AT_SERVER_STARTSTOP_MODULE)
@ -75,16 +77,24 @@ GRAPEVINE_ENABLED = settings.GRAPEVINE_ENABLED
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
GAME_INDEX_ENABLED = settings.GAME_INDEX_ENABLED
INFO_DICT = {"servername": SERVERNAME, "version": VERSION,
"amp": "", "errors": "", "info": "", "webserver": "", "irc_rss": ""}
INFO_DICT = {
"servername": SERVERNAME,
"version": VERSION,
"amp": "",
"errors": "",
"info": "",
"webserver": "",
"irc_rss": "",
}
try:
WEB_PLUGINS_MODULE = mod_import(settings.WEB_PLUGINS_MODULE)
except ImportError:
WEB_PLUGINS_MODULE = None
INFO_DICT["errors"] = (
"WARNING: settings.WEB_PLUGINS_MODULE not found - "
"copy 'evennia/game_template/server/conf/web_plugins.py to mygame/server/conf.")
"WARNING: settings.WEB_PLUGINS_MODULE not found - "
"copy 'evennia/game_template/server/conf/web_plugins.py to mygame/server/conf."
)
# Maintenance function - this is called repeatedly by the server
@ -138,16 +148,18 @@ def _server_maintenance():
# handle idle timeouts
if _IDLE_TIMEOUT > 0:
reason = _("idle timeout exceeded")
for session in (sess for sess in SESSIONS.values()
if (now - sess.cmd_last) > _IDLE_TIMEOUT):
if not session.account or not \
session.account.access(session.account, "noidletimeout", default=False):
for session in (
sess for sess in SESSIONS.values() if (now - sess.cmd_last) > _IDLE_TIMEOUT
):
if not session.account or not session.account.access(
session.account, "noidletimeout", default=False
):
SESSIONS.disconnect(session, reason=reason)
#------------------------------------------------------------
# ------------------------------------------------------------
# Evennia Main Server object
#------------------------------------------------------------
# ------------------------------------------------------------
class Evennia(object):
@ -165,7 +177,7 @@ class Evennia(object):
application - an instantiated Twisted application
"""
sys.path.insert(1, '.')
sys.path.insert(1, ".")
# create a store of services
self.services = service.MultiService()
@ -189,6 +201,7 @@ class Evennia(object):
# (see https://github.com/evennia/evennia/issues/1128)
def _wrap_sigint_handler(*args):
from twisted.internet.defer import Deferred
if hasattr(self, "web_root"):
d = self.web_root.empty_threadpool()
d.addCallback(lambda _: self.shutdown("reload", _reactor_stopping=True))
@ -196,8 +209,8 @@ class Evennia(object):
d = Deferred(lambda _: self.shutdown("reload", _reactor_stopping=True))
d.addCallback(lambda _: reactor.stop())
reactor.callLater(1, d.callback, None)
reactor.sigInt = _wrap_sigint_handler
reactor.sigInt = _wrap_sigint_handler
# Server startup methods
@ -206,11 +219,14 @@ class Evennia(object):
Optimize some SQLite stuff at startup since we
can't save it to the database.
"""
if ((".".join(str(i) for i in django.VERSION) < "1.2" and
settings.DATABASES.get('default', {}).get('ENGINE') == "sqlite3") or
(hasattr(settings, 'DATABASES') and
settings.DATABASES.get("default", {}).get('ENGINE', None) ==
'django.db.backends.sqlite3')):
if (
".".join(str(i) for i in django.VERSION) < "1.2"
and settings.DATABASES.get("default", {}).get("ENGINE") == "sqlite3"
) or (
hasattr(settings, "DATABASES")
and settings.DATABASES.get("default", {}).get("ENGINE", None)
== "django.db.backends.sqlite3"
):
cursor = connection.cursor()
cursor.execute("PRAGMA cache_size=10000")
cursor.execute("PRAGMA synchronous=OFF")
@ -228,36 +244,68 @@ class Evennia(object):
global INFO_DICT
# setting names
settings_names = ("CMDSET_CHARACTER", "CMDSET_ACCOUNT",
"BASE_ACCOUNT_TYPECLASS", "BASE_OBJECT_TYPECLASS",
"BASE_CHARACTER_TYPECLASS", "BASE_ROOM_TYPECLASS",
"BASE_EXIT_TYPECLASS", "BASE_SCRIPT_TYPECLASS",
"BASE_CHANNEL_TYPECLASS")
settings_names = (
"CMDSET_CHARACTER",
"CMDSET_ACCOUNT",
"BASE_ACCOUNT_TYPECLASS",
"BASE_OBJECT_TYPECLASS",
"BASE_CHARACTER_TYPECLASS",
"BASE_ROOM_TYPECLASS",
"BASE_EXIT_TYPECLASS",
"BASE_SCRIPT_TYPECLASS",
"BASE_CHANNEL_TYPECLASS",
)
# get previous and current settings so they can be compared
settings_compare = list(zip([ServerConfig.objects.conf(name) for name in settings_names],
[settings.__getattr__(name) for name in settings_names]))
mismatches = [i for i, tup in enumerate(settings_compare) if tup[0] and tup[1] and tup[0] != tup[1]]
if len(mismatches): # can't use any() since mismatches may be [0] which reads as False for any()
settings_compare = list(
zip(
[ServerConfig.objects.conf(name) for name in settings_names],
[settings.__getattr__(name) for name in settings_names],
)
)
mismatches = [
i for i, tup in enumerate(settings_compare) if tup[0] and tup[1] and tup[0] != tup[1]
]
if len(
mismatches
): # can't use any() since mismatches may be [0] which reads as False for any()
# we have a changed default. Import relevant objects and
# run the update
from evennia.objects.models import ObjectDB
from evennia.comms.models import ChannelDB
#from evennia.accounts.models import AccountDB
for i, prev, curr in ((i, tup[0], tup[1]) for i, tup in enumerate(settings_compare) if i in mismatches):
# from evennia.accounts.models import AccountDB
for i, prev, curr in (
(i, tup[0], tup[1]) for i, tup in enumerate(settings_compare) if i in mismatches
):
# update the database
INFO_DICT['info'] = " %s:\n '%s' changed to '%s'. Updating unchanged entries in database ..." % (settings_names[i], prev, curr)
INFO_DICT["info"] = (
" %s:\n '%s' changed to '%s'. Updating unchanged entries in database ..."
% (settings_names[i], prev, curr)
)
if i == 0:
ObjectDB.objects.filter(db_cmdset_storage__exact=prev).update(db_cmdset_storage=curr)
ObjectDB.objects.filter(db_cmdset_storage__exact=prev).update(
db_cmdset_storage=curr
)
if i == 1:
AccountDB.objects.filter(db_cmdset_storage__exact=prev).update(db_cmdset_storage=curr)
AccountDB.objects.filter(db_cmdset_storage__exact=prev).update(
db_cmdset_storage=curr
)
if i == 2:
AccountDB.objects.filter(db_typeclass_path__exact=prev).update(db_typeclass_path=curr)
AccountDB.objects.filter(db_typeclass_path__exact=prev).update(
db_typeclass_path=curr
)
if i in (3, 4, 5, 6):
ObjectDB.objects.filter(db_typeclass_path__exact=prev).update(db_typeclass_path=curr)
ObjectDB.objects.filter(db_typeclass_path__exact=prev).update(
db_typeclass_path=curr
)
if i == 7:
ScriptDB.objects.filter(db_typeclass_path__exact=prev).update(db_typeclass_path=curr)
ScriptDB.objects.filter(db_typeclass_path__exact=prev).update(
db_typeclass_path=curr
)
if i == 8:
ChannelDB.objects.filter(db_typeclass_path__exact=prev).update(db_typeclass_path=curr)
ChannelDB.objects.filter(db_typeclass_path__exact=prev).update(
db_typeclass_path=curr
)
# store the new default and clean caches
ServerConfig.objects.conf(settings_names[i], curr)
ObjectDB.flush_instance_cache()
@ -266,8 +314,11 @@ class Evennia(object):
ChannelDB.flush_instance_cache()
# if this is the first start we might not have a "previous"
# setup saved. Store it now.
[ServerConfig.objects.conf(settings_names[i], tup[1])
for i, tup in enumerate(settings_compare) if not tup[0]]
[
ServerConfig.objects.conf(settings_names[i], tup[1])
for i, tup in enumerate(settings_compare)
if not tup[0]
]
def run_initial_setup(self):
"""
@ -278,19 +329,20 @@ class Evennia(object):
Once finished the last_initial_setup_step is set to -1.
"""
global INFO_DICT
last_initial_setup_step = ServerConfig.objects.conf('last_initial_setup_step')
last_initial_setup_step = ServerConfig.objects.conf("last_initial_setup_step")
if not last_initial_setup_step:
# None is only returned if the config does not exist,
# i.e. this is an empty DB that needs populating.
INFO_DICT['info'] = ' Server started for the first time. Setting defaults.'
INFO_DICT["info"] = " Server started for the first time. Setting defaults."
initial_setup.handle_setup(0)
elif int(last_initial_setup_step) >= 0:
# a positive value means the setup crashed on one of its
# modules and setup will resume from this step, retrying
# the last failed module. When all are finished, the step
# is set to -1 to show it does not need to be run again.
INFO_DICT['info'] = ' Resuming initial setup from step {last}.'.format(
last=last_initial_setup_step)
INFO_DICT["info"] = " Resuming initial setup from step {last}.".format(
last=last_initial_setup_step
)
initial_setup.handle_setup(int(last_initial_setup_step))
def run_init_hooks(self, mode):
@ -314,14 +366,14 @@ class Evennia(object):
[p.at_init() for p in AccountDB.get_all_cached_instances()]
# call correct server hook based on start file value
if mode == 'reload':
if mode == "reload":
logger.log_msg("Server successfully reloaded.")
self.at_server_reload_start()
elif mode == 'reset':
elif mode == "reset":
# only run hook, don't purge sessions
self.at_server_cold_start()
logger.log_msg("Evennia Server successfully restarted in 'reset' mode.")
elif mode == 'shutdown':
elif mode == "shutdown":
self.at_server_cold_start()
# clear eventual lingering session storages
ObjectDB.objects.clear_all_sessids()
@ -331,7 +383,7 @@ class Evennia(object):
self.at_server_start()
@defer.inlineCallbacks
def shutdown(self, mode='reload', _reactor_stopping=False):
def shutdown(self, mode="reload", _reactor_stopping=False):
"""
Shuts down the server from inside it.
@ -356,20 +408,24 @@ class Evennia(object):
from evennia.server.models import ServerConfig
from evennia.utils import gametime as _GAMETIME_MODULE
if mode == 'reload':
if mode == "reload":
# call restart hooks
ServerConfig.objects.conf("server_restart_mode", "reload")
yield [o.at_server_reload() for o in ObjectDB.get_all_cached_instances()]
yield [p.at_server_reload() for p in AccountDB.get_all_cached_instances()]
yield [(s.pause(manual_pause=False), s.at_server_reload())
for s in ScriptDB.get_all_cached_instances() if s.is_active or s.attributes.has("_manual_pause")]
yield [
(s.pause(manual_pause=False), s.at_server_reload())
for s in ScriptDB.get_all_cached_instances()
if s.is_active or s.attributes.has("_manual_pause")
]
yield self.sessions.all_sessions_portal_sync()
self.at_server_reload_stop()
# only save monitor state on reload, not on shutdown/reset
from evennia.scripts.monitorhandler import MONITOR_HANDLER
MONITOR_HANDLER.save()
else:
if mode == 'reset':
if mode == "reset":
# like shutdown but don't unset the is_connected flag and don't disconnect sessions
yield [o.at_server_shutdown() for o in ObjectDB.get_all_cached_instances()]
yield [p.at_server_shutdown() for p in AccountDB.get_all_cached_instances()]
@ -378,16 +434,24 @@ class Evennia(object):
else: # shutdown
yield [_SA(p, "is_connected", False) for p in AccountDB.get_all_cached_instances()]
yield [o.at_server_shutdown() for o in ObjectDB.get_all_cached_instances()]
yield [(p.unpuppet_all(), p.at_server_shutdown())
for p in AccountDB.get_all_cached_instances()]
yield [
(p.unpuppet_all(), p.at_server_shutdown())
for p in AccountDB.get_all_cached_instances()
]
yield ObjectDB.objects.clear_all_sessids()
yield [(s.pause(manual_pause=s.attributes.get("_manual_pause", False)),
s.at_server_shutdown()) for s in ScriptDB.get_all_cached_instances()]
yield [
(
s.pause(manual_pause=s.attributes.get("_manual_pause", False)),
s.at_server_shutdown(),
)
for s in ScriptDB.get_all_cached_instances()
]
ServerConfig.objects.conf("server_restart_mode", "reset")
self.at_server_cold_stop()
# tickerhandler state should always be saved.
from evennia.scripts.tickerhandler import TICKER_HANDLER
TICKER_HANDLER.save()
# always called, also for a reload
@ -444,10 +508,12 @@ class Evennia(object):
"""
from evennia.scripts.monitorhandler import MONITOR_HANDLER
MONITOR_HANDLER.restore(mode == 'reload')
MONITOR_HANDLER.restore(mode == "reload")
from evennia.scripts.tickerhandler import TICKER_HANDLER
TICKER_HANDLER.restore(mode == 'reload')
TICKER_HANDLER.restore(mode == "reload")
# after sync is complete we force-validate all scripts
# (this also starts any that didn't yet start)
@ -455,6 +521,7 @@ class Evennia(object):
# start the task handler
from evennia.scripts.taskhandler import TASK_HANDLER
TASK_HANDLER.load()
TASK_HANDLER.create_delays()
@ -468,17 +535,17 @@ class Evennia(object):
mudinfo_chan = settings.CHANNEL_MUDINFO
if not mudinfo_chan:
raise RuntimeError("settings.CHANNEL_MUDINFO must be defined.")
if not ChannelDB.objects.filter(db_key=mudinfo_chan['key']):
if not ChannelDB.objects.filter(db_key=mudinfo_chan["key"]):
channel = create_channel(**mudinfo_chan)
channel.connect(god_account)
# connectinfo
connectinfo_chan = settings.CHANNEL_MUDINFO
if connectinfo_chan:
if not ChannelDB.objects.filter(db_key=mudinfo_chan['key']):
if not ChannelDB.objects.filter(db_key=mudinfo_chan["key"]):
channel = create_channel(**connectinfo_chan)
# default channels
for chan_info in settings.DEFAULT_CHANNELS:
if not ChannelDB.objects.filter(db_key=chan_info['key']):
if not ChannelDB.objects.filter(db_key=chan_info["key"]):
channel = create_channel(**chan_info)
channel.connect(god_account)
@ -500,15 +567,19 @@ class Evennia(object):
# We need to do this just in case the server was killed in a way where
# the normal cleanup operations did not have time to run.
from evennia.objects.models import ObjectDB
ObjectDB.objects.clear_all_sessids()
# Remove non-persistent scripts
from evennia.scripts.models import ScriptDB
for script in ScriptDB.objects.filter(db_persistent=False):
script.stop()
if GUEST_ENABLED:
for guest in AccountDB.objects.all().filter(db_typeclass_path=settings.BASE_GUEST_TYPECLASS):
for guest in AccountDB.objects.all().filter(
db_typeclass_path=settings.BASE_GUEST_TYPECLASS
):
for character in guest.db._playable_characters:
if character:
character.delete()
@ -523,11 +594,12 @@ class Evennia(object):
if SERVER_STARTSTOP_MODULE:
SERVER_STARTSTOP_MODULE.at_server_cold_stop()
#------------------------------------------------------------
# ------------------------------------------------------------
#
# Start the Evennia game server and add all active services
#
#------------------------------------------------------------
# ------------------------------------------------------------
# Tell the system the server is starting up; some things are not available yet
@ -535,12 +607,13 @@ ServerConfig.objects.conf("server_starting_mode", True)
# twistd requires us to define the variable 'application' so it knows
# what to execute from.
application = service.Application('Evennia')
application = service.Application("Evennia")
if "--nodaemon" not in sys.argv:
# custom logging, but only if we are not running in interactive mode
logfile = logger.WeeklyLogFile(os.path.basename(settings.SERVER_LOG_FILE),
os.path.dirname(settings.SERVER_LOG_FILE))
logfile = logger.WeeklyLogFile(
os.path.basename(settings.SERVER_LOG_FILE), os.path.dirname(settings.SERVER_LOG_FILE)
)
application.setComponent(ILogObserver, logger.ServerLogObserver(logfile).emit)
# The main evennia server program. This sets up the database
@ -554,28 +627,36 @@ if AMP_ENABLED:
# it would be during testing and debugging.
ifacestr = ""
if AMP_INTERFACE != '127.0.0.1':
if AMP_INTERFACE != "127.0.0.1":
ifacestr = "-%s" % AMP_INTERFACE
INFO_DICT["amp"] = 'amp %s: %s' % (ifacestr, AMP_PORT)
INFO_DICT["amp"] = "amp %s: %s" % (ifacestr, AMP_PORT)
from evennia.server import amp_client
factory = amp_client.AMPClientFactory(EVENNIA)
amp_service = internet.TCPClient(AMP_HOST, AMP_PORT, factory)
amp_service.setName('ServerAMPClient')
amp_service.setName("ServerAMPClient")
EVENNIA.services.addService(amp_service)
if WEBSERVER_ENABLED:
# Start a django-compatible webserver.
from evennia.server.webserver import DjangoWebRoot, WSGIWebServer, Website, LockableThreadPool, PrivateStaticRoot
from evennia.server.webserver import (
DjangoWebRoot,
WSGIWebServer,
Website,
LockableThreadPool,
PrivateStaticRoot,
)
# start a thread pool and define the root url (/) as a wsgi resource
# recognized by Django
threads = LockableThreadPool(minthreads=max(1, settings.WEBSERVER_THREADPOOL_LIMITS[0]),
maxthreads=max(1, settings.WEBSERVER_THREADPOOL_LIMITS[1]))
threads = LockableThreadPool(
minthreads=max(1, settings.WEBSERVER_THREADPOOL_LIMITS[0]),
maxthreads=max(1, settings.WEBSERVER_THREADPOOL_LIMITS[1]),
)
web_root = DjangoWebRoot(threads)
# point our media resources to url /media
@ -594,8 +675,8 @@ if WEBSERVER_ENABLED:
INFO_DICT["webserver"] = ""
for proxyport, serverport in WEBSERVER_PORTS:
# create the webserver (we only need the port for this)
webserver = WSGIWebServer(threads, serverport, web_site, interface='127.0.0.1')
webserver.setName('EvenniaWebServer%s' % serverport)
webserver = WSGIWebServer(threads, serverport, web_site, interface="127.0.0.1")
webserver.setName("EvenniaWebServer%s" % serverport)
EVENNIA.services.addService(webserver)
INFO_DICT["webserver"] += "webserver: %s" % serverport
@ -603,18 +684,19 @@ if WEBSERVER_ENABLED:
ENABLED = []
if IRC_ENABLED:
# IRC channel connections
ENABLED.append('irc')
ENABLED.append("irc")
if RSS_ENABLED:
# RSS feed channel connections
ENABLED.append('rss')
ENABLED.append("rss")
if GRAPEVINE_ENABLED:
# Grapevine channel connections
ENABLED.append('grapevine')
ENABLED.append("grapevine")
if GAME_INDEX_ENABLED:
from evennia.server.game_index_client.service import EvenniaGameIndexService
egi_service = EvenniaGameIndexService()
EVENNIA.services.addService(egi_service)

View file

@ -31,25 +31,26 @@ from django.utils.translation import ugettext as _
class NDbHolder(object):
"""Holder for allowing property access of attributes"""
def __init__(self, obj, name, manager_name='attributes'):
def __init__(self, obj, name, manager_name="attributes"):
_SA(self, name, _GA(obj, manager_name))
_SA(self, 'name', name)
_SA(self, "name", name)
def __getattribute__(self, attrname):
if attrname == 'all':
if attrname == "all":
# we allow to overload our default .all
attr = _GA(self, _GA(self, 'name')).get("all")
attr = _GA(self, _GA(self, "name")).get("all")
return attr if attr else _GA(self, "all")
return _GA(self, _GA(self, 'name')).get(attrname)
return _GA(self, _GA(self, "name")).get(attrname)
def __setattr__(self, attrname, value):
_GA(self, _GA(self, 'name')).add(attrname, value)
_GA(self, _GA(self, "name")).add(attrname, value)
def __delattr__(self, attrname):
_GA(self, _GA(self, 'name')).remove(attrname)
_GA(self, _GA(self, "name")).remove(attrname)
def get_all(self):
return _GA(self, _GA(self, 'name')).all()
return _GA(self, _GA(self, "name")).all()
all = property(get_all)
@ -147,6 +148,7 @@ class NAttributeHandler(object):
# Server Session
# -------------------------------------------------------------
class ServerSession(Session):
"""
This class represents an account's session and is a template for
@ -166,10 +168,11 @@ class ServerSession(Session):
self.cmdset = CmdSetHandler(self, True)
def __cmdset_storage_get(self):
return [path.strip() for path in self.cmdset_storage_string.split(',')]
return [path.strip() for path in self.cmdset_storage_string.split(",")]
def __cmdset_storage_set(self, value):
self.cmdset_storage_string = ",".join(str(val).strip() for val in make_iter(value))
cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set)
def at_sync(self):
@ -252,8 +255,7 @@ class ServerSession(Session):
account.at_post_disconnect()
# remove any webclient settings monitors associated with this
# session
MONITOR_HANDLER.remove(account, "_saved_webclient_options",
self.sessid)
MONITOR_HANDLER.remove(account, "_saved_webclient_options", self.sessid)
def get_account(self):
"""
@ -274,6 +276,7 @@ class ServerSession(Session):
"""
return self.logged_in and self.puppet
get_character = get_puppet
def get_puppet_or_account(self):
@ -302,7 +305,7 @@ class ServerSession(Session):
cchan = channel and settings.CHANNEL_CONNECTINFO
if cchan:
try:
cchan = ChannelDB.objects.get_channel(cchan['key'])
cchan = ChannelDB.objects.get_channel(cchan["key"])
cchan.msg("[%s]: %s" % (cchan.key, message))
except Exception:
logger.log_trace()
@ -316,8 +319,8 @@ class ServerSession(Session):
"""
flags = self.protocol_flags
width = flags.get('SCREENWIDTH', {}).get(0, settings.CLIENT_DEFAULT_WIDTH)
height = flags.get('SCREENHEIGHT', {}).get(0, settings.CLIENT_DEFAULT_HEIGHT)
width = flags.get("SCREENWIDTH", {}).get(0, settings.CLIENT_DEFAULT_WIDTH)
height = flags.get("SCREENHEIGHT", {}).get(0, settings.CLIENT_DEFAULT_HEIGHT)
return width, height
def update_session_counters(self, idle=False):
@ -461,7 +464,7 @@ class ServerSession(Session):
if self.logged_in and hasattr(self, "account") and self.account:
symbol = "(#%s)" % self.account.id
try:
if hasattr(self.address, '__iter__'):
if hasattr(self.address, "__iter__"):
address = ":".join([str(part) for part in self.address])
else:
address = self.address
@ -525,6 +528,7 @@ class ServerSession(Session):
def ndb_del(self):
"""Stop accidental deletion."""
raise Exception("Cannot delete the ndb object!")
ndb = property(ndb_get, ndb_set, ndb_del)
db = property(ndb_get, ndb_set, ndb_del)

View file

@ -36,10 +36,24 @@ class Session(object):
"""
# names of attributes that should be affected by syncing.
_attrs_to_sync = ('protocol_key', 'address', 'suid', 'sessid', 'uid', 'csessid',
'uname', 'logged_in', 'puid',
'conn_time', 'cmd_last', 'cmd_last_visible', 'cmd_total',
'protocol_flags', 'server_data', "cmdset_storage_string")
_attrs_to_sync = (
"protocol_key",
"address",
"suid",
"sessid",
"uid",
"csessid",
"uname",
"logged_in",
"puid",
"conn_time",
"cmd_last",
"cmd_last_visible",
"cmd_total",
"protocol_flags",
"server_data",
"cmdset_storage_string",
)
def init_session(self, protocol_key, address, sessionhandler):
"""
@ -82,11 +96,13 @@ class Session(object):
self.cmd_last = self.conn_time
self.cmd_total = 0
self.protocol_flags = {"ENCODING": "utf-8",
"SCREENREADER": False,
"INPUTDEBUG": False,
"RAW": False,
"NOCOLOR": False}
self.protocol_flags = {
"ENCODING": "utf-8",
"SCREENREADER": False,
"INPUTDEBUG": False,
"RAW": False,
"NOCOLOR": False,
}
self.server_data = {}
# map of input data to session methods
@ -105,8 +121,9 @@ class Session(object):
the keys given by self._attrs_to_sync.
"""
return dict((key, value) for key, value in self.__dict__.items()
if key in self._attrs_to_sync)
return dict(
(key, value) for key, value in self.__dict__.items() if key in self._attrs_to_sync
)
def load_sync_data(self, sessdata):
"""
@ -118,9 +135,12 @@ class Session(object):
"""
for propname, value in sessdata.items():
if (propname == "protocol_flags" and isinstance(value, dict) and
hasattr(self, "protocol_flags") and
isinstance(self.protocol_flags, dict)):
if (
propname == "protocol_flags"
and isinstance(value, dict)
and hasattr(self, "protocol_flags")
and isinstance(self.protocol_flags, dict)
):
# special handling to allow partial update of protocol flags
self.protocol_flags.update(value)
else:
@ -134,7 +154,9 @@ class Session(object):
"""
if self.account:
self.protocol_flags.update(self.account.attributes.get("_saved_protocol_flags", None) or {})
self.protocol_flags.update(
self.account.attributes.get("_saved_protocol_flags", None) or {}
)
# access hooks

View file

@ -17,8 +17,13 @@ import time
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,
make_iter, delay, callables_from_module)
from evennia.utils.utils import (
variable_from_module,
is_iter,
make_iter,
delay,
callables_from_module,
)
from evennia.server.signals import SIGNAL_ACCOUNT_POST_LOGIN, SIGNAL_ACCOUNT_POST_LOGOUT
from evennia.server.signals import SIGNAL_ACCOUNT_POST_FIRST_LOGIN, SIGNAL_ACCOUNT_POST_LAST_LOGOUT
from evennia.utils.inlinefuncs import parse_inlinefunc
@ -33,7 +38,7 @@ _ServerConfig = None
_ScriptDB = None
_OOB_HANDLER = None
_ERR_BAD_UTF8 = 'Your client sent an incorrect UTF-8 sequence.'
_ERR_BAD_UTF8 = "Your client sent an incorrect UTF-8 sequence."
class DummySession(object):
@ -43,23 +48,23 @@ class DummySession(object):
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
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
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
SRELOAD = chr(14) # server reloading (have portal start a new server)
SSTART = chr(15) # server start (portal must already be running anyway)
PSHUTD = chr(16) # portal (+server) shutdown
SSHUTD = chr(17) # server shutdown
PSTATUS = chr(18) # ping server or portal status
SRESET = chr(19) # server shutdown in reset mode
SRELOAD = chr(14) # server reloading (have portal start a new server)
SSTART = chr(15) # server start (portal must already be running anyway)
PSHUTD = chr(16) # portal (+server) shutdown
SSHUTD = chr(17) # server shutdown
PSTATUS = chr(18) # ping server or portal status
SRESET = chr(19) # server shutdown in reset mode
# i18n
from django.utils.translation import ugettext as _
@ -96,15 +101,16 @@ def delayed_import():
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)
assert _ServerSession
assert _AccountDB
assert _ServerConfig
assert _ScriptDB
#-----------------------------------------------------------
# -----------------------------------------------------------
# SessionHandler base class
#------------------------------------------------------------
# ------------------------------------------------------------
class SessionHandler(dict):
"""
@ -199,7 +205,7 @@ class SessionHandler(dict):
except UnicodeDecodeError:
# incorrect unicode sequence
session.sendLine(_ERR_BAD_UTF8)
data = ''
data = ""
return data
@ -220,8 +226,11 @@ class SessionHandler(dict):
data = parse_inlinefunc(data, strip=strip_inlinefunc, session=session)
return str(data)
elif hasattr(data, "id") and hasattr(data, "db_date_created") \
and hasattr(data, '__dbclass__'):
elif (
hasattr(data, "id")
and hasattr(data, "db_date_created")
and hasattr(data, "__dbclass__")
):
# convert database-object to their string representation.
return _validate(str(data))
else:
@ -259,6 +268,7 @@ class SessionHandler(dict):
# Server-SessionHandler class
# ------------------------------------------------------------
class ServerSessionHandler(SessionHandler):
"""
This object holds the stack of sessions active in the game at
@ -382,7 +392,7 @@ class ServerSessionHandler(SessionHandler):
self[sessid] = sess
sess.at_sync()
mode = 'reload'
mode = "reload"
# tell the server hook we synced
self.server.at_post_portal_sync(mode)
@ -440,9 +450,9 @@ class ServerSessionHandler(SessionHandler):
the Server.
"""
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION, operation=SCONN,
protocol_path=protocol_path,
config=configdict)
self.server.amp_protocol.send_AdminServer2Portal(
DUMMYSESSION, operation=SCONN, protocol_path=protocol_path, config=configdict
)
def portal_restart_server(self):
"""
@ -464,8 +474,7 @@ class ServerSessionHandler(SessionHandler):
itself down)
"""
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION,
operation=PSHUTD)
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION, operation=PSHUTD)
def login(self, session, account, force=False, testmode=False):
"""
@ -512,10 +521,9 @@ class ServerSessionHandler(SessionHandler):
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})
self.server.amp_protocol.send_AdminServer2Portal(
session, operation=SLOGIN, sessiondata={"logged_in": True, "uid": session.uid}
)
account.at_post_login(session=session)
if nsess < 2:
SIGNAL_ACCOUNT_POST_FIRST_LOGIN.send(sender=account, session=session)
@ -543,8 +551,9 @@ 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:
@ -557,9 +566,9 @@ class ServerSessionHandler(SessionHandler):
del self[sessid]
if sync_portal:
# inform portal that session should be closed.
self.server.amp_protocol.send_AdminServer2Portal(session,
operation=SDISCONN,
reason=reason)
self.server.amp_protocol.send_AdminServer2Portal(
session, operation=SDISCONN, reason=reason
)
def all_sessions_portal_sync(self):
"""
@ -568,9 +577,9 @@ class ServerSessionHandler(SessionHandler):
"""
sessdata = self.get_all_sync_data()
return self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION,
operation=SSYNC,
sessiondata=sessdata)
return self.server.amp_protocol.send_AdminServer2Portal(
DUMMYSESSION, operation=SSYNC, sessiondata=sessdata
)
def session_portal_sync(self, session):
"""
@ -579,10 +588,9 @@ class ServerSessionHandler(SessionHandler):
"""
sessdata = {session.sessid: session.get_sync_data()}
return self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION,
operation=SSYNC,
sessiondata=sessdata,
clean=False)
return self.server.amp_protocol.send_AdminServer2Portal(
DUMMYSESSION, operation=SSYNC, sessiondata=sessdata, clean=False
)
def session_portal_partial_sync(self, session_data):
"""
@ -593,10 +601,9 @@ class ServerSessionHandler(SessionHandler):
more sessions in detail.
"""
return self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION,
operation=SSYNC,
sessiondata=session_data,
clean=False)
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."):
"""
@ -610,12 +617,13 @@ class ServerSessionHandler(SessionHandler):
for session in self:
del session
# tell portal to disconnect all sessions
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION,
operation=SDISCONNALL,
reason=reason)
self.server.amp_protocol.send_AdminServer2Portal(
DUMMYSESSION, operation=SDISCONNALL, reason=reason
)
def disconnect_duplicate_sessions(self, curr_session,
reason=_("Logged in from elsewhere. Disconnecting.")):
def disconnect_duplicate_sessions(
self, curr_session, reason=_("Logged in from elsewhere. Disconnecting.")
):
"""
Disconnects any existing sessions with the same user.
@ -628,10 +636,9 @@ class ServerSessionHandler(SessionHandler):
# we can't compare sessions directly since this will compare addresses and
# mean connecting from the same host would not catch duplicates
sid = id(curr_session)
doublet_sessions = [sess for sess in self.values()
if sess.logged_in and
sess.uid == uid and
id(sess) != sid]
doublet_sessions = [
sess for sess in self.values() if sess.logged_in and sess.uid == uid and id(sess) != sid
]
for session in doublet_sessions:
self.disconnect(session, reason)
@ -644,9 +651,13 @@ class ServerSessionHandler(SessionHandler):
"""
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):
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):
@ -670,8 +681,13 @@ 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):
"""
@ -702,8 +718,11 @@ class ServerSessionHandler(SessionHandler):
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]
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):
@ -734,6 +753,7 @@ class ServerSessionHandler(SessionHandler):
"""
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):
@ -749,8 +769,9 @@ class ServerSessionHandler(SessionHandler):
"""
if csessid:
return []
return [session for session in self.values()
if session.csessid and session.csessid == csessid]
return [
session for session in self.values() if session.csessid and session.csessid == csessid
]
def announce_all(self, message):
"""
@ -779,8 +800,7 @@ class ServerSessionHandler(SessionHandler):
kwargs = self.clean_senddata(session, kwargs)
# send across AMP
self.server.amp_protocol.send_MsgServer2Portal(session,
**kwargs)
self.server.amp_protocol.send_MsgServer2Portal(session, **kwargs)
def get_inputfuncs(self):
"""

View file

@ -24,45 +24,45 @@ from django.dispatch import Signal
# Account.create() after the Account is created. Note that this will *not* fire
# if calling create.create_account alone, since going through the Account.create()
# is the most expected route.
SIGNAL_ACCOUNT_POST_CREATE = Signal(providing_args=['ip', ])
SIGNAL_ACCOUNT_POST_CREATE = Signal(providing_args=["ip"])
# The Sender is the renamed Account. This is triggered by the username setter in AccountDB.
SIGNAL_ACCOUNT_POST_RENAME = Signal(providing_args=['old_name', 'new_name'])
SIGNAL_ACCOUNT_POST_RENAME = Signal(providing_args=["old_name", "new_name"])
# The Sender is the connecting Account. This is triggered when an Account connects cold;
# that is, it had no other sessions connected.
SIGNAL_ACCOUNT_POST_FIRST_LOGIN = Signal(providing_args=['session', ])
SIGNAL_ACCOUNT_POST_FIRST_LOGIN = Signal(providing_args=["session"])
# The sender is the connecting Account. This is triggered whenever a session authenticates
# to an Account regardless of existing sessions. It then firest after FIRST_LOGIN signal
SIGNAL_ACCOUNT_POST_LOGIN = Signal(providing_args=['session', ])
SIGNAL_ACCOUNT_POST_LOGIN = Signal(providing_args=["session"])
# The Sender is the Account attempting to authenticate. This is triggered whenever a
# session tries to login to an Account but fails.
SIGNAL_ACCOUNT_POST_LOGIN_FAIL = Signal(providing_args=['session', ])
SIGNAL_ACCOUNT_POST_LOGIN_FAIL = Signal(providing_args=["session"])
# The sender is the disconnecting Account. This is triggered whenever a session disconnects
# from the account, regardless of how many it started with or remain.
SIGNAL_ACCOUNT_POST_LOGOUT = Signal(providing_args=['session', ])
SIGNAL_ACCOUNT_POST_LOGOUT = Signal(providing_args=["session"])
# The sender is the Account. This is triggered when an Account's final session disconnects.
SIGNAL_ACCOUNT_POST_LAST_LOGOUT = Signal(providing_args=['session', ])
SIGNAL_ACCOUNT_POST_LAST_LOGOUT = Signal(providing_args=["session"])
# The sender is an Object. This is triggered when Object has been created, after all hooks.
SIGNAL_OBJECT_POST_CREATE = Signal()
# The sender is the Object being puppeted. This is triggered after all puppeting hooks have
# been called. The Object has already been puppeted by this point.
SIGNAL_OBJECT_POST_PUPPET = Signal(providing_args=['session', 'account'])
SIGNAL_OBJECT_POST_PUPPET = Signal(providing_args=["session", "account"])
# The sender is the Object being released. This is triggered after all hooks are called.
# The Object is no longer puppeted by this point.
SIGNAL_OBJECT_POST_UNPUPPET = Signal(providing_args=['session', 'account'])
SIGNAL_OBJECT_POST_UNPUPPET = Signal(providing_args=["session", "account"])
# The sender is the Typed Object being renamed. This isn't necessarily an Object;
# it could be a script. It fires whenever the value of the Typed object's 'key'
# changes. Will need to use isinstance() or other filtering on things that use this.
SIGNAL_TYPED_OBJECT_POST_RENAME = Signal(providing_args=['old_key', 'new_key'])
SIGNAL_TYPED_OBJECT_POST_RENAME = Signal(providing_args=["old_key", "new_key"])
# The sender is the created Script. This is called after the Script was first created,
# after all hooks.

View file

@ -17,12 +17,12 @@ from evennia.server import serversession, session
from evennia.utils import create
from twisted.internet.base import DelayedCall
DelayedCall.debug = True
# @patch("evennia.server.initial_setup.get_god_account",
# MagicMock(return_value=create.account("TestAMPAccount", "test@test.com", "testpassword")))
class _TestAMP(TwistedTestCase):
def setUp(self):
super(_TestAMP, self).setUp()
self.account = mommy.make("accounts.AccountDB", id=1)
@ -90,9 +90,9 @@ class TestAMPClientSend(_TestAMP):
def test_adminserver2portal(self, mocktransport):
self._connect_client(mocktransport)
self.amp_client.send_AdminServer2Portal(self.session,
operation=amp.PSYNC,
info_dict={}, spid=None)
self.amp_client.send_AdminServer2Portal(
self.session, operation=amp.PSYNC, info_dict={}, spid=None
)
wire_data = self._catch_wire_read(mocktransport)[0]
self._connect_server(mocktransport)
@ -117,8 +117,7 @@ class TestAMPClientRecv(_TestAMP):
def test_adminportal2server(self, mocktransport):
self._connect_server(mocktransport)
self.amp_server.send_AdminPortal2Server(self.session,
operation=amp.PDISCONNALL)
self.amp_server.send_AdminPortal2Server(self.session, operation=amp.PDISCONNALL)
wire_data = self._catch_wire_read(mocktransport)[0]
self._connect_client(mocktransport)

View file

@ -11,7 +11,6 @@ from evennia.server import initial_setup
class TestInitialSetup(TestCase):
@patch("evennia.server.initial_setup.AccountDB")
def test_get_god_account(self, mocked_accountdb):
mocked_accountdb.objects.get = MagicMock(return_value=1)

View file

@ -13,22 +13,25 @@ from evennia.server import evennia_launcher
from evennia.server.portal import amp
from twisted.internet.base import DelayedCall
DelayedCall.debug = True
@patch("evennia.server.evennia_launcher.Popen", new=MagicMock())
class TestLauncher(TwistedTestCase):
def test_is_windows(self):
self.assertEqual(evennia_launcher._is_windows(), os.name == 'nt')
self.assertEqual(evennia_launcher._is_windows(), os.name == "nt")
def test_file_compact(self):
self.assertEqual(evennia_launcher._file_names_compact(
"foo/bar/test1", "foo/bar/test2"),
"foo/bar/test1 and test2")
self.assertEqual(
evennia_launcher._file_names_compact("foo/bar/test1", "foo/bar/test2"),
"foo/bar/test1 and test2",
)
self.assertEqual(evennia_launcher._file_names_compact(
"foo/test1", "foo/bar/test2"),
"foo/test1 and foo/bar/test2")
self.assertEqual(
evennia_launcher._file_names_compact("foo/test1", "foo/bar/test2"),
"foo/test1 and foo/bar/test2",
)
@patch("evennia.server.evennia_launcher.print")
def test_print_info(self, mockprint):
@ -41,7 +44,7 @@ class TestLauncher(TwistedTestCase):
"webserver_proxy": 1234,
"webclient": 1234,
"webserver_internal": 1234,
"amp": 1234
"amp": 1234,
}
server_dict = {
"servername": "testserver",
@ -50,7 +53,7 @@ class TestLauncher(TwistedTestCase):
"amp": 1234,
"irc_rss": "irc.test",
"info": "testing mode",
"errors": ""
"errors": "",
}
evennia_launcher._print_info(portal_dict, server_dict)
@ -129,14 +132,14 @@ class TestLauncher(TwistedTestCase):
@patch("evennia.server.evennia_launcher.print")
def test_query_status_run(self, mprint):
evennia_launcher.query_status()
mprint.assert_called_with('Portal: RUNNING (pid 100)\nServer: RUNNING (pid 100)')
mprint.assert_called_with("Portal: RUNNING (pid 100)\nServer: RUNNING (pid 100)")
@patch("evennia.server.evennia_launcher.send_instruction", _msend_status_err)
@patch("evennia.server.evennia_launcher.NO_REACTOR_STOP", True)
@patch("evennia.server.evennia_launcher.print")
def test_query_status_not_run(self, mprint):
evennia_launcher.query_status()
mprint.assert_called_with('Portal: NOT RUNNING\nServer: NOT RUNNING')
mprint.assert_called_with("Portal: NOT RUNNING\nServer: NOT RUNNING")
@patch("evennia.server.evennia_launcher.send_instruction", _msend_status_ok)
@patch("evennia.server.evennia_launcher.NO_REACTOR_STOP", True)
@ -144,7 +147,7 @@ class TestLauncher(TwistedTestCase):
mprint = MagicMock()
def testcall(response):
resp = pickle.loads(response['status'])
resp = pickle.loads(response["status"])
mprint(resp)
evennia_launcher.query_status(callback=testcall)
@ -173,10 +176,8 @@ class TestLauncher(TwistedTestCase):
mcall = MagicMock()
merr = MagicMock()
evennia_launcher.wait_for_status(
portal_running=True,
server_running=True,
callback=mcall,
errback=merr)
portal_running=True, server_running=True, callback=mcall, errback=merr
)
mcall.assert_called_with(True, True)
merr.assert_not_called()
@ -187,10 +188,8 @@ class TestLauncher(TwistedTestCase):
mcall = MagicMock()
merr = MagicMock()
evennia_launcher.wait_for_status(
portal_running=True,
server_running=True,
callback=mcall,
errback=merr)
portal_running=True, server_running=True, callback=mcall, errback=merr
)
mcall.assert_not_called()
merr.assert_not_called()

View file

@ -22,6 +22,7 @@ class MockSettings(object):
Class for simulating django.conf.settings. Created with a single value, and then sets the required
WEBSERVER_ENABLED setting to True or False depending if we're testing WEBSERVER_PORTS.
"""
def __init__(self, setting, value=None):
setattr(self, setting, value)
if setting == "WEBSERVER_PORTS":
@ -34,12 +35,26 @@ class TestDeprecations(TestCase):
"""
Class for testing deprecations.check_errors.
"""
deprecated_settings = (
"CMDSET_DEFAULT", "CMDSET_OOC", "BASE_COMM_TYPECLASS", "COMM_TYPECLASS_PATHS",
"CHARACTER_DEFAULT_HOME", "OBJECT_TYPECLASS_PATHS", "SCRIPT_TYPECLASS_PATHS",
"ACCOUNT_TYPECLASS_PATHS", "CHANNEL_TYPECLASS_PATHS", "SEARCH_MULTIMATCH_SEPARATOR",
"TIME_SEC_PER_MIN", "TIME_MIN_PER_HOUR", "TIME_HOUR_PER_DAY", "TIME_DAY_PER_WEEK",
"TIME_WEEK_PER_MONTH", "TIME_MONTH_PER_YEAR", "GAME_DIRECTORY_LISTING")
"CMDSET_DEFAULT",
"CMDSET_OOC",
"BASE_COMM_TYPECLASS",
"COMM_TYPECLASS_PATHS",
"CHARACTER_DEFAULT_HOME",
"OBJECT_TYPECLASS_PATHS",
"SCRIPT_TYPECLASS_PATHS",
"ACCOUNT_TYPECLASS_PATHS",
"CHANNEL_TYPECLASS_PATHS",
"SEARCH_MULTIMATCH_SEPARATOR",
"TIME_SEC_PER_MIN",
"TIME_MIN_PER_HOUR",
"TIME_HOUR_PER_DAY",
"TIME_DAY_PER_WEEK",
"TIME_WEEK_PER_MONTH",
"TIME_MONTH_PER_YEAR",
"GAME_DIRECTORY_LISTING",
)
def test_check_errors(self):
"""
@ -51,34 +66,32 @@ class TestDeprecations(TestCase):
self.assertRaises(DeprecationWarning, check_errors, MockSettings(setting))
# test check for WEBSERVER_PORTS having correct value
self.assertRaises(
DeprecationWarning,
check_errors, MockSettings("WEBSERVER_PORTS", value=["not a tuple"]))
DeprecationWarning, check_errors, MockSettings("WEBSERVER_PORTS", value=["not a tuple"])
)
class ValidatorTest(EvenniaTest):
def test_validator(self):
# Validator returns None on success and ValidationError on failure.
validator = EvenniaPasswordValidator()
# This password should meet Evennia standards.
self.assertFalse(validator.validate('testpassword', user=self.account))
self.assertFalse(validator.validate("testpassword", user=self.account))
# This password contains illegal characters and should raise an Exception.
from django.core.exceptions import ValidationError
self.assertRaises(ValidationError, validator.validate, '(#)[#]<>', user=self.account)
self.assertRaises(ValidationError, validator.validate, "(#)[#]<>", user=self.account)
class ThrottleTest(EvenniaTest):
"""
Class for testing the connection/IP throttle.
"""
def test_throttle(self):
ips = ('94.100.176.153', '45.56.148.77', '5.196.1.129')
kwargs = {
'limit': 5,
'timeout': 15 * 60
}
ips = ("94.100.176.153", "45.56.148.77", "5.196.1.129")
kwargs = {"limit": 5, "timeout": 15 * 60}
throttle = Throttle(**kwargs)

View file

@ -18,97 +18,110 @@ class TestServer(TestCase):
def setUp(self):
from evennia.server import server
self.server = server
def test__server_maintenance_reset(self):
with patch.multiple("evennia.server.server",
LoopingCall=DEFAULT,
Evennia=DEFAULT,
_FLUSH_CACHE=DEFAULT,
connection=DEFAULT,
_IDMAPPER_CACHE_MAXSIZE=1000,
_MAINTENANCE_COUNT=0,
ServerConfig=DEFAULT) as mocks:
mocks['connection'].close = MagicMock()
mocks['ServerConfig'].objects.conf = MagicMock(return_value=456)
with patch.multiple(
"evennia.server.server",
LoopingCall=DEFAULT,
Evennia=DEFAULT,
_FLUSH_CACHE=DEFAULT,
connection=DEFAULT,
_IDMAPPER_CACHE_MAXSIZE=1000,
_MAINTENANCE_COUNT=0,
ServerConfig=DEFAULT,
) as mocks:
mocks["connection"].close = MagicMock()
mocks["ServerConfig"].objects.conf = MagicMock(return_value=456)
# flush cache
self.server._server_maintenance()
mocks['ServerConfig'].objects.conf.assert_called_with('runtime', 456)
mocks["ServerConfig"].objects.conf.assert_called_with("runtime", 456)
def test__server_maintenance_flush(self):
with patch.multiple("evennia.server.server",
LoopingCall=DEFAULT,
Evennia=DEFAULT,
_FLUSH_CACHE=DEFAULT,
connection=DEFAULT,
_IDMAPPER_CACHE_MAXSIZE=1000,
_MAINTENANCE_COUNT=600 - 1,
ServerConfig=DEFAULT) as mocks:
mocks['connection'].close = MagicMock()
mocks['ServerConfig'].objects.conf = MagicMock(return_value=100)
with patch.multiple(
"evennia.server.server",
LoopingCall=DEFAULT,
Evennia=DEFAULT,
_FLUSH_CACHE=DEFAULT,
connection=DEFAULT,
_IDMAPPER_CACHE_MAXSIZE=1000,
_MAINTENANCE_COUNT=600 - 1,
ServerConfig=DEFAULT,
) as mocks:
mocks["connection"].close = MagicMock()
mocks["ServerConfig"].objects.conf = MagicMock(return_value=100)
# flush cache
self.server._server_maintenance()
mocks['_FLUSH_CACHE'].assert_called_with(1000)
mocks["_FLUSH_CACHE"].assert_called_with(1000)
def test__server_maintenance_validate_scripts(self):
with patch.multiple("evennia.server.server",
LoopingCall=DEFAULT,
Evennia=DEFAULT,
_FLUSH_CACHE=DEFAULT,
connection=DEFAULT,
_IDMAPPER_CACHE_MAXSIZE=1000,
_MAINTENANCE_COUNT=3600 - 1,
ServerConfig=DEFAULT) as mocks:
mocks['connection'].close = MagicMock()
mocks['ServerConfig'].objects.conf = MagicMock(return_value=100)
with patch.multiple(
"evennia.server.server",
LoopingCall=DEFAULT,
Evennia=DEFAULT,
_FLUSH_CACHE=DEFAULT,
connection=DEFAULT,
_IDMAPPER_CACHE_MAXSIZE=1000,
_MAINTENANCE_COUNT=3600 - 1,
ServerConfig=DEFAULT,
) as mocks:
mocks["connection"].close = MagicMock()
mocks["ServerConfig"].objects.conf = MagicMock(return_value=100)
with patch("evennia.server.server.evennia.ScriptDB.objects.validate") as mock:
self.server._server_maintenance()
mocks['_FLUSH_CACHE'].assert_called_with(1000)
mocks["_FLUSH_CACHE"].assert_called_with(1000)
mock.assert_called()
def test__server_maintenance_channel_handler_update(self):
with patch.multiple("evennia.server.server",
LoopingCall=DEFAULT,
Evennia=DEFAULT,
_FLUSH_CACHE=DEFAULT,
connection=DEFAULT,
_IDMAPPER_CACHE_MAXSIZE=1000,
_MAINTENANCE_COUNT=3700 - 1,
ServerConfig=DEFAULT) as mocks:
mocks['connection'].close = MagicMock()
mocks['ServerConfig'].objects.conf = MagicMock(return_value=100)
with patch.multiple(
"evennia.server.server",
LoopingCall=DEFAULT,
Evennia=DEFAULT,
_FLUSH_CACHE=DEFAULT,
connection=DEFAULT,
_IDMAPPER_CACHE_MAXSIZE=1000,
_MAINTENANCE_COUNT=3700 - 1,
ServerConfig=DEFAULT,
) as mocks:
mocks["connection"].close = MagicMock()
mocks["ServerConfig"].objects.conf = MagicMock(return_value=100)
with patch("evennia.server.server.evennia.CHANNEL_HANDLER.update") as mock:
self.server._server_maintenance()
mock.assert_called()
def test__server_maintenance_close_connection(self):
with patch.multiple("evennia.server.server",
LoopingCall=DEFAULT,
Evennia=DEFAULT,
_FLUSH_CACHE=DEFAULT,
connection=DEFAULT,
_IDMAPPER_CACHE_MAXSIZE=1000,
_MAINTENANCE_COUNT=(3600 * 7) - 1,
ServerConfig=DEFAULT) as mocks:
mocks['connection'].close = MagicMock()
mocks['ServerConfig'].objects.conf = MagicMock(return_value=100)
with patch.multiple(
"evennia.server.server",
LoopingCall=DEFAULT,
Evennia=DEFAULT,
_FLUSH_CACHE=DEFAULT,
connection=DEFAULT,
_IDMAPPER_CACHE_MAXSIZE=1000,
_MAINTENANCE_COUNT=(3600 * 7) - 1,
ServerConfig=DEFAULT,
) as mocks:
mocks["connection"].close = MagicMock()
mocks["ServerConfig"].objects.conf = MagicMock(return_value=100)
self.server._server_maintenance()
mocks['connection'].close.assert_called()
mocks["connection"].close.assert_called()
def test__server_maintenance_idle_time(self):
with patch.multiple("evennia.server.server",
LoopingCall=DEFAULT,
Evennia=DEFAULT,
_FLUSH_CACHE=DEFAULT,
connection=DEFAULT,
_IDMAPPER_CACHE_MAXSIZE=1000,
_MAINTENANCE_COUNT=(3600 * 7) - 1,
SESSIONS=DEFAULT,
_IDLE_TIMEOUT=10,
time=DEFAULT,
ServerConfig=DEFAULT) as mocks:
with patch.multiple(
"evennia.server.server",
LoopingCall=DEFAULT,
Evennia=DEFAULT,
_FLUSH_CACHE=DEFAULT,
connection=DEFAULT,
_IDMAPPER_CACHE_MAXSIZE=1000,
_MAINTENANCE_COUNT=(3600 * 7) - 1,
SESSIONS=DEFAULT,
_IDLE_TIMEOUT=10,
time=DEFAULT,
ServerConfig=DEFAULT,
) as mocks:
sess1 = MagicMock()
sess2 = MagicMock()
sess3 = MagicMock()
@ -123,9 +136,9 @@ class TestServer(TestCase):
sess3.account = MagicMock()
sess4.account.access = MagicMock(return_value=False)
mocks['time'].time = MagicMock(return_value=1000)
mocks["time"].time = MagicMock(return_value=1000)
mocks['ServerConfig'].objects.conf = MagicMock(return_value=100)
mocks["ServerConfig"].objects.conf = MagicMock(return_value=100)
mocks["SESSIONS"].values = MagicMock(return_value=[sess1, sess2, sess3, sess4])
mocks["SESSIONS"].disconnect = MagicMock()
@ -135,11 +148,9 @@ class TestServer(TestCase):
mocks["SESSIONS"].disconnect.assert_has_calls(calls, any_order=True)
def test_evennia_start(self):
with patch.multiple("evennia.server.server",
time=DEFAULT,
service=DEFAULT) as mocks:
with patch.multiple("evennia.server.server", time=DEFAULT, service=DEFAULT) as mocks:
mocks['time'].time = MagicMock(return_value=1000)
mocks["time"].time = MagicMock(return_value=1000)
evennia = self.server.Evennia(MagicMock())
self.assertEqual(evennia.start_time, 1000)
@ -148,45 +159,49 @@ class TestServer(TestCase):
@patch("evennia.server.server.ScriptDB")
@patch("evennia.comms.models.ChannelDB")
def test_update_defaults(self, mockchan, mockscript, mockacct, mockobj):
with patch.multiple("evennia.server.server",
ServerConfig=DEFAULT) as mocks:
with patch.multiple("evennia.server.server", ServerConfig=DEFAULT) as mocks:
mockchan.objects.filter = MagicMock()
mockscript.objects.filter = MagicMock()
mockacct.objects.filter = MagicMock()
mockobj.objects.filter = MagicMock()
mockchan.objects.filter = MagicMock()
mockscript.objects.filter = MagicMock()
mockacct.objects.filter = MagicMock()
mockobj.objects.filter = MagicMock()
# fake mismatches
settings_names = ("CMDSET_CHARACTER", "CMDSET_ACCOUNT",
"BASE_ACCOUNT_TYPECLASS", "BASE_OBJECT_TYPECLASS",
"BASE_CHARACTER_TYPECLASS", "BASE_ROOM_TYPECLASS",
"BASE_EXIT_TYPECLASS", "BASE_SCRIPT_TYPECLASS",
"BASE_CHANNEL_TYPECLASS")
fakes = {name: "Dummy.path" for name in settings_names}
# fake mismatches
settings_names = (
"CMDSET_CHARACTER",
"CMDSET_ACCOUNT",
"BASE_ACCOUNT_TYPECLASS",
"BASE_OBJECT_TYPECLASS",
"BASE_CHARACTER_TYPECLASS",
"BASE_ROOM_TYPECLASS",
"BASE_EXIT_TYPECLASS",
"BASE_SCRIPT_TYPECLASS",
"BASE_CHANNEL_TYPECLASS",
)
fakes = {name: "Dummy.path" for name in settings_names}
def _mock_conf(key, *args):
return fakes[key]
def _mock_conf(key, *args):
return fakes[key]
mocks['ServerConfig'].objects.conf = _mock_conf
mocks["ServerConfig"].objects.conf = _mock_conf
evennia = self.server.Evennia(MagicMock())
evennia.update_defaults()
evennia = self.server.Evennia(MagicMock())
evennia.update_defaults()
mockchan.objects.filter.assert_called()
mockscript.objects.filter.assert_called()
mockacct.objects.filter.assert_called()
mockobj.objects.filter.assert_called()
mockchan.objects.filter.assert_called()
mockscript.objects.filter.assert_called()
mockacct.objects.filter.assert_called()
mockobj.objects.filter.assert_called()
def test_initial_setup(self):
from evennia.utils.create import create_account
acct = create_account("TestSuperuser", "test@test.com", "testpassword",
is_superuser=True)
acct = create_account("TestSuperuser", "test@test.com", "testpassword", is_superuser=True)
with patch.multiple("evennia.server.initial_setup",
reset_server=DEFAULT,
AccountDB=DEFAULT) as mocks:
mocks['AccountDB'].objects.get = MagicMock(return_value=acct)
with patch.multiple(
"evennia.server.initial_setup", reset_server=DEFAULT, AccountDB=DEFAULT
) as mocks:
mocks["AccountDB"].objects.get = MagicMock(return_value=acct)
evennia = self.server.Evennia(MagicMock())
evennia.run_initial_setup()
acct.delete()
@ -194,16 +209,17 @@ class TestServer(TestCase):
def test_initial_setup_retry(self):
from evennia.utils.create import create_account
acct = create_account("TestSuperuser2", "test@test.com", "testpassword",
is_superuser=True)
acct = create_account("TestSuperuser2", "test@test.com", "testpassword", is_superuser=True)
with patch.multiple("evennia.server.initial_setup",
ServerConfig=DEFAULT,
reset_server=DEFAULT,
AccountDB=DEFAULT) as mocks:
mocks['AccountDB'].objects.get = MagicMock(return_value=acct)
with patch.multiple(
"evennia.server.initial_setup",
ServerConfig=DEFAULT,
reset_server=DEFAULT,
AccountDB=DEFAULT,
) as mocks:
mocks["AccountDB"].objects.get = MagicMock(return_value=acct)
# a last_initial_setup_step > 0
mocks['ServerConfig'].objects.conf = MagicMock(return_value=4)
mocks["ServerConfig"].objects.conf = MagicMock(return_value=4)
evennia = self.server.Evennia(MagicMock())
evennia.run_initial_setup()
acct.delete()
@ -211,6 +227,7 @@ class TestServer(TestCase):
@override_settings(DEFAULT_HOME="#1")
def test_run_init_hooks(self):
from evennia.utils import create
obj1 = create.object(key="HookTestObj1")
obj2 = create.object(key="HookTestObj2")
acct1 = create.account("HookAcct1", "hooktest1@test.com", "testpasswd")
@ -224,9 +241,9 @@ class TestServer(TestCase):
mockobj.objects.clear_all_sessids = MagicMock()
evennia = self.server.Evennia(MagicMock())
evennia.run_init_hooks('reload')
evennia.run_init_hooks('reset')
evennia.run_init_hooks('shutdown')
evennia.run_init_hooks("reload")
evennia.run_init_hooks("reset")
evennia.run_init_hooks("shutdown")
mockacct.get_all_cached_instances.assert_called()
mockobj.get_all_cached_instances.assert_called()
@ -236,7 +253,7 @@ class TestServer(TestCase):
acct1.delete()
acct2.delete()
@patch('evennia.server.server.INFO_DICT', {"test": "foo"})
@patch("evennia.server.server.INFO_DICT", {"test": "foo"})
def test_get_info_dict(self):
evennia = self.server.Evennia(MagicMock())
self.assertEqual(evennia.get_info_dict(), {"test": "foo"})

View file

@ -22,6 +22,8 @@ class EvenniaTestSuiteRunner(DiscoverRunner):
If not given, a subset of settings.INSTALLED_APPS will be used.
"""
import evennia
evennia._init()
return super(EvenniaTestSuiteRunner, self).build_suite(
test_labels, extra_tests=extra_tests, **kwargs)
test_labels, extra_tests=extra_tests, **kwargs
)

View file

@ -16,7 +16,7 @@ class Throttle(object):
no recent failures have been recorded.
"""
error_msg = 'Too many failed attempts; you must wait a few minutes before trying again.'
error_msg = "Too many failed attempts; you must wait a few minutes before trying again."
def __init__(self, **kwargs):
"""
@ -31,8 +31,8 @@ class Throttle(object):
the throttle is imposed!
"""
self.storage = defaultdict(deque)
self.cache_size = self.limit = kwargs.get('limit', 5)
self.timeout = kwargs.get('timeout', 5 * 60)
self.cache_size = self.limit = kwargs.get("limit", 5)
self.timeout = kwargs.get("timeout", 5 * 60)
def get(self, ip=None):
"""
@ -49,10 +49,12 @@ class Throttle(object):
timestamps of recent failures only for that IP.
"""
if ip: return self.storage.get(ip, deque(maxlen=self.cache_size))
else: return self.storage
if ip:
return self.storage.get(ip, deque(maxlen=self.cache_size))
else:
return self.storage
def update(self, ip, failmsg='Exceeded threshold.'):
def update(self, ip, failmsg="Exceeded threshold."):
"""
Store the time of the latest failure.
@ -78,8 +80,11 @@ class Throttle(object):
currently_throttled = self.check(ip)
# If this makes it engage, log a single activation event
if (not previously_throttled and currently_throttled):
logger.log_sec('Throttle Activated: %s (IP: %s, %i hits in %i seconds.)' % (failmsg, ip, self.limit, self.timeout))
if not previously_throttled and currently_throttled:
logger.log_sec(
"Throttle Activated: %s (IP: %s, %i hits in %i seconds.)"
% (failmsg, ip, self.limit, self.timeout)
)
def check(self, ip):
"""
@ -107,7 +112,7 @@ class Throttle(object):
return True
else:
# timeout has passed. clear faillist
del(self.storage[ip])
del self.storage[ip]
return False
else:
return False

View file

@ -24,26 +24,28 @@ class EvenniaUsernameAvailabilityValidator:
"""
# Check guest list
if (settings.GUEST_LIST and username.lower() in (guest.lower() for guest in settings.GUEST_LIST)):
if settings.GUEST_LIST and username.lower() in (
guest.lower() for guest in settings.GUEST_LIST
):
raise ValidationError(
_('Sorry, that username is reserved.'),
code='evennia_username_reserved',
_("Sorry, that username is reserved."), code="evennia_username_reserved"
)
# Check database
exists = AccountDB.objects.filter(username__iexact=username).exists()
if exists:
raise ValidationError(
_('Sorry, that username is already taken.'),
code='evennia_username_taken',
_("Sorry, that username is already taken."), code="evennia_username_taken"
)
class EvenniaPasswordValidator:
def __init__(self, regex=r"^[\w. @+\-',]+$",
policy="Password should contain a mix of letters, "
"spaces, digits and @/./+/-/_/'/, only."):
def __init__(
self,
regex=r"^[\w. @+\-',]+$",
policy="Password should contain a mix of letters, "
"spaces, digits and @/./+/-/_/'/, only.",
):
"""
Constructs a standard Django password validator.
@ -71,10 +73,7 @@ class EvenniaPasswordValidator:
"""
# Check complexity
if not re.findall(self.regex, password):
raise ValidationError(
_(self.policy),
code='evennia_password_policy',
)
raise ValidationError(_(self.policy), code="evennia_password_policy")
def get_help_text(self):
"""

View file

@ -58,6 +58,7 @@ class LockableThreadPool(threadpool.ThreadPool):
# X-Forwarded-For Handler
#
class HTTPChannelWithXForwardedFor(http.HTTPChannel):
"""
HTTP xforward class
@ -73,9 +74,9 @@ class HTTPChannelWithXForwardedFor(http.HTTPChannel):
http.HTTPChannel.allHeadersReceived(self)
req = self.requests[-1]
client_ip, port = self.transport.client
proxy_chain = req.getHeader('X-FORWARDED-FOR')
proxy_chain = req.getHeader("X-FORWARDED-FOR")
if proxy_chain and client_ip in _UPSTREAM_IPS:
forwarded = proxy_chain.split(', ', 1)[CLIENT]
forwarded = proxy_chain.split(", ", 1)[CLIENT]
self.transport.client = (forwarded, port)
@ -100,10 +101,11 @@ class EvenniaReverseProxyResource(ReverseProxyResource):
"""
request.notifyFinish().addErrback(
lambda f: logger.log_trace("%s\nCaught errback in webserver.py:75." % f))
lambda f: logger.log_trace("%s\nCaught errback in webserver.py:75." % f)
)
return EvenniaReverseProxyResource(
self.host, self.port, self.path + '/' + urlquote(path, safe=""),
self.reactor)
self.host, self.port, self.path + "/" + urlquote(path, safe=""), self.reactor
)
def render(self, request):
"""
@ -121,18 +123,24 @@ class EvenniaReverseProxyResource(ReverseProxyResource):
request.content.seek(0, 0)
qs = urllib.parse.urlparse(request.uri)[4]
if qs:
rest = self.path + '?' + qs.decode()
rest = self.path + "?" + qs.decode()
else:
rest = self.path
rest = rest.encode()
clientFactory = self.proxyClientFactoryClass(
request.method, rest, request.clientproto,
request.getAllHeaders(), request.content.read(), request)
request.method,
rest,
request.clientproto,
request.getAllHeaders(),
request.content.read(),
request,
)
clientFactory.noisy = False
self.reactor.connectTCP(self.host, self.port, clientFactory)
# don't trigger traceback if connection is lost before request finish.
request.notifyFinish().addErrback(
lambda f: logger.log_trace("%s\nCaught errback in webserver.py:75." % f))
lambda f: logger.log_trace("%s\nCaught errback in webserver.py:75." % f)
)
return NOT_DONE_YET
@ -178,7 +186,7 @@ class DjangoWebRoot(resource.Resource):
return defer.DeferredList(self._pending_requests, consumeErrors=True)
def _decrement_requests(self, *args, **kwargs):
self._pending_requests.pop(kwargs.get('deferred', None), None)
self._pending_requests.pop(kwargs.get("deferred", None), None)
def getChild(self, path, request):
"""
@ -209,10 +217,12 @@ class DjangoWebRoot(resource.Resource):
# Site with deactivateable logging
#
class Website(server.Site):
"""
This class will only log http requests if settings.DEBUG is True.
"""
noisy = False
def logPrefix(self):
@ -231,6 +241,7 @@ class Website(server.Site):
# Threaded Webserver
#
class WSGIWebServer(internet.TCPServer):
"""
This is a WSGI webserver. It makes sure to start
@ -277,5 +288,6 @@ class PrivateStaticRoot(static.File):
won't see an index of all static/media files on the server).
"""
def directoryListing(self):
return resource.ForbiddenResource()