Evennia now runs on its own Twisted webserver (no need for testserver or Apache if you don't want to). Evennia now also has an ajax long-polling web client running from Twisted. The web client requires no extra dependencies beyond jQuery which is included. The src/server structure has been r
cleaned up and rewritten to make it easier to add new protocols in the future - all new protocols need to inherit from server.session.Session, whi ch implements a set of hooks that Evennia uses to communicate. The current web client protocol is functional but does not implement any of rcaskey 's suggestions as of yet - it uses a separate data object passed through msg() to communicate between the server and the various protocols. Also the client itself could probably need cleanup and 'prettification'. The fact that the system runs a hybrid of Django and Twisted, getting the best of both worlds should allow for many possibilities in the future. /Griatch
This commit is contained in:
parent
ecefbfac01
commit
251f94aa7a
118 changed files with 9049 additions and 593 deletions
|
|
@ -107,6 +107,15 @@ def cycle_logfile():
|
||||||
os.remove(logfile_old)
|
os.remove(logfile_old)
|
||||||
os.rename(logfile, logfile_old)
|
os.rename(logfile, logfile_old)
|
||||||
|
|
||||||
|
logfile = settings.HTTP_LOG_FILE.strip()
|
||||||
|
logfile_old = logfile + '.old'
|
||||||
|
if os.path.exists(logfile):
|
||||||
|
# Cycle the old logfiles to *.old
|
||||||
|
if os.path.exists(logfile_old):
|
||||||
|
# E.g. Windows don't support rename-replace
|
||||||
|
os.remove(logfile_old)
|
||||||
|
os.rename(logfile, logfile_old)
|
||||||
|
|
||||||
def start_daemon(parser, options, args):
|
def start_daemon(parser, options, args):
|
||||||
"""
|
"""
|
||||||
Start the server in daemon mode. This means that all logging output will
|
Start the server in daemon mode. This means that all logging output will
|
||||||
|
|
@ -136,6 +145,9 @@ def start_interactive(parser, options, args):
|
||||||
print '\nStarting Evennia server in interactive mode (stop with keyboard interrupt) ...'
|
print '\nStarting Evennia server in interactive mode (stop with keyboard interrupt) ...'
|
||||||
print 'Logging to: Standard output.'
|
print 'Logging to: Standard output.'
|
||||||
|
|
||||||
|
# we cycle logfiles (this will at most put all files to *.old)
|
||||||
|
# to handle html request logging files.
|
||||||
|
cycle_logfile()
|
||||||
try:
|
try:
|
||||||
call([TWISTED_BINARY,
|
call([TWISTED_BINARY,
|
||||||
'-n',
|
'-n',
|
||||||
|
|
|
||||||
|
|
@ -318,7 +318,7 @@ class CmdQuit(MuxCommand):
|
||||||
sessions = self.caller.sessions
|
sessions = self.caller.sessions
|
||||||
for session in sessions:
|
for session in sessions:
|
||||||
session.msg("Quitting. Hope to see you soon again.")
|
session.msg("Quitting. Hope to see you soon again.")
|
||||||
session.handle_close()
|
session.at_disconnect()
|
||||||
|
|
||||||
class CmdWho(MuxCommand):
|
class CmdWho(MuxCommand):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -29,27 +29,20 @@ from src.server import session, sessionhandler
|
||||||
# print all feedback from test commands (can become very verbose!)
|
# print all feedback from test commands (can become very verbose!)
|
||||||
VERBOSE = False
|
VERBOSE = False
|
||||||
|
|
||||||
class FakeSession(session.SessionProtocol):
|
class FakeSession(session.Session):
|
||||||
"""
|
"""
|
||||||
A fake session that
|
A fake session that
|
||||||
implements dummy versions of the real thing; this is needed to
|
implements dummy versions of the real thing; this is needed to
|
||||||
mimic a logged-in player.
|
mimic a logged-in player.
|
||||||
"""
|
"""
|
||||||
|
protocol_key = "TestProtocol"
|
||||||
def connectionMade(self):
|
def connectionMade(self):
|
||||||
self.prep_session()
|
self.session_connect('0,0,0,0')
|
||||||
sessionhandler.add_session(self)
|
|
||||||
def prep_session(self):
|
|
||||||
self.server, self.address = None, "0.0.0.0"
|
|
||||||
self.name, self.uid = None, None
|
|
||||||
self.logged_in = False
|
|
||||||
self.encoding = "utf-8"
|
|
||||||
self.cmd_last, self.cmd_last_visible, self.cmd_conn_time = time.time(), time.time(), time.time()
|
|
||||||
self.cmd_total = 0
|
|
||||||
def disconnectClient(self):
|
def disconnectClient(self):
|
||||||
pass
|
pass
|
||||||
def lineReceived(self, raw_string):
|
def lineReceived(self, raw_string):
|
||||||
pass
|
pass
|
||||||
def msg(self, message, markup=True):
|
def msg(self, message, data=None):
|
||||||
if VERBOSE:
|
if VERBOSE:
|
||||||
print message
|
print message
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,13 @@ import traceback
|
||||||
#from django.contrib.auth.models import User
|
#from django.contrib.auth.models import User
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from src.server import sessionhandler
|
||||||
from src.players.models import PlayerDB
|
from src.players.models import PlayerDB
|
||||||
from src.objects.models import ObjectDB
|
from src.objects.models import ObjectDB
|
||||||
from src.config.models import ConfigValue
|
from src.config.models import ConfigValue, ConnectScreen
|
||||||
from src.comms.models import Channel
|
from src.comms.models import Channel
|
||||||
from src.utils import create, logger, utils
|
|
||||||
|
from src.utils import create, logger, utils, ansi
|
||||||
from src.commands.default.muxcommand import MuxCommand
|
from src.commands.default.muxcommand import MuxCommand
|
||||||
|
|
||||||
class CmdConnect(MuxCommand):
|
class CmdConnect(MuxCommand):
|
||||||
|
|
@ -94,7 +96,7 @@ class CmdConnect(MuxCommand):
|
||||||
player.at_pre_login()
|
player.at_pre_login()
|
||||||
character.at_pre_login()
|
character.at_pre_login()
|
||||||
|
|
||||||
session.login(player)
|
session.session_login(player)
|
||||||
|
|
||||||
player.at_post_login()
|
player.at_post_login()
|
||||||
character.at_post_login()
|
character.at_post_login()
|
||||||
|
|
@ -230,7 +232,7 @@ class CmdQuit(MuxCommand):
|
||||||
"Simply close the connection."
|
"Simply close the connection."
|
||||||
session = self.caller
|
session = self.caller
|
||||||
session.msg("Good bye! Disconnecting ...")
|
session.msg("Good bye! Disconnecting ...")
|
||||||
session.handle_close()
|
session.at_disconnect()
|
||||||
|
|
||||||
class CmdUnconnectedLook(MuxCommand):
|
class CmdUnconnectedLook(MuxCommand):
|
||||||
"""
|
"""
|
||||||
|
|
@ -243,8 +245,11 @@ class CmdUnconnectedLook(MuxCommand):
|
||||||
def func(self):
|
def func(self):
|
||||||
"Show the connect screen."
|
"Show the connect screen."
|
||||||
try:
|
try:
|
||||||
self.caller.game_connect_screen()
|
screen = ConnectScreen.objects.get_random_connect_screen()
|
||||||
except Exception:
|
string = ansi.parse_ansi(screen.text)
|
||||||
|
self.caller.msg(string)
|
||||||
|
except Exception, e:
|
||||||
|
self.caller.msg(e)
|
||||||
self.caller.msg("Connect screen not found. Enter 'help' for aid.")
|
self.caller.msg("Connect screen not found. Enter 'help' for aid.")
|
||||||
|
|
||||||
class CmdUnconnectedHelp(MuxCommand):
|
class CmdUnconnectedHelp(MuxCommand):
|
||||||
|
|
@ -271,23 +276,19 @@ Commands available at this point:
|
||||||
To login to the system, you need to do one of the following:
|
To login to the system, you need to do one of the following:
|
||||||
|
|
||||||
1) If you have no previous account, you need to use the 'create'
|
1) If you have no previous account, you need to use the 'create'
|
||||||
command followed by your desired character name (in quotes), your
|
command like this:
|
||||||
e-mail address and finally a password of your choice. Like
|
|
||||||
this:
|
|
||||||
|
|
||||||
> create "Anna the Barbarian" anna@myemail.com tuK3221mP
|
> create "Anna the Barbarian" anna@myemail.com c67jHL8p
|
||||||
|
|
||||||
It's always a good idea (not only here, but everywhere on the net)
|
It's always a good idea (not only here, but everywhere on the net)
|
||||||
to not use a regular word for your password. Make it longer than
|
to not use a regular word for your password. Make it longer than
|
||||||
3 characters (ideally 6 or more) and mix numbers and capitalization
|
3 characters (ideally 6 or more) and mix numbers and capitalization
|
||||||
into it. Now proceed to 2).
|
into it.
|
||||||
|
|
||||||
2) If you have an account already, either because you just created
|
2) If you have an account already, either because you just created
|
||||||
one in 1) above, or you are returning, use the 'connect' command
|
one in 1) above or you are returning, use the 'connect' command:
|
||||||
followed by the e-mail and password you previously set.
|
|
||||||
Example:
|
|
||||||
|
|
||||||
> connect anna@myemail.com tuK3221mP
|
> connect anna@myemail.com c67jHL8p
|
||||||
|
|
||||||
This should log you in. Run 'help' again once you're logged in
|
This should log you in. Run 'help' again once you're logged in
|
||||||
to get more aid. Hope you enjoy your stay!
|
to get more aid. Hope you enjoy your stay!
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ These managers handles the
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from src.players.models import PlayerDB
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from src.utils.utils import is_iter
|
from src.utils.utils import is_iter
|
||||||
|
|
||||||
|
|
@ -21,6 +20,7 @@ def to_object(inp, objtype='player'):
|
||||||
inp - the input object/string
|
inp - the input object/string
|
||||||
objtype - 'player' or 'channel'
|
objtype - 'player' or 'channel'
|
||||||
"""
|
"""
|
||||||
|
from src.players.models import PlayerDB
|
||||||
if objtype == 'player':
|
if objtype == 'player':
|
||||||
if type(inp) == PlayerDB:
|
if type(inp) == PlayerDB:
|
||||||
return inp
|
return inp
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ be able to delete connections on the fly).
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from src.utils.idmapper.models import SharedMemoryModel
|
from src.utils.idmapper.models import SharedMemoryModel
|
||||||
from src.players.models import PlayerDB
|
from src.players.models import PlayerDB
|
||||||
from src.comms import managers
|
|
||||||
from src.server import sessionhandler
|
from src.server import sessionhandler
|
||||||
|
from src.comms import managers
|
||||||
from src.permissions.permissions import has_perm
|
from src.permissions.permissions import has_perm
|
||||||
from src.utils.utils import is_iter
|
from src.utils.utils import is_iter
|
||||||
from src.utils.utils import dbref as is_dbref
|
from src.utils.utils import dbref as is_dbref
|
||||||
|
|
@ -81,7 +81,7 @@ class Msg(SharedMemoryModel):
|
||||||
permissions - perm strings
|
permissions - perm strings
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from src.players.models import PlayerDB
|
||||||
#
|
#
|
||||||
# Msg database model setup
|
# Msg database model setup
|
||||||
#
|
#
|
||||||
|
|
@ -509,7 +509,7 @@ class Channel(SharedMemoryModel):
|
||||||
# send message to all connected players
|
# send message to all connected players
|
||||||
for conn in conns:
|
for conn in conns:
|
||||||
for session in \
|
for session in \
|
||||||
sessionhandler.find_sessions_from_username(conn.player.name):
|
sessionhandler.SESSIONS.sessions_from_player(conn.player):
|
||||||
session.msg(msg)
|
session.msg(msg)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -539,6 +539,7 @@ class ChannelConnection(SharedMemoryModel):
|
||||||
The advantage of making it like this is that one can easily
|
The advantage of making it like this is that one can easily
|
||||||
break the connection just by deleting this object.
|
break the connection just by deleting this object.
|
||||||
"""
|
"""
|
||||||
|
from src.players.models import PlayerDB
|
||||||
# Player connected to a channel
|
# Player connected to a channel
|
||||||
db_player = models.ForeignKey(PlayerDB)
|
db_player = models.ForeignKey(PlayerDB)
|
||||||
# Channel the player is connected to
|
# Channel the player is connected to
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ class ConfigValueManager(models.Manager):
|
||||||
new_conf.db_value = db_value
|
new_conf.db_value = db_value
|
||||||
new_conf.save()
|
new_conf.save()
|
||||||
|
|
||||||
def get_configvalue(self, config_key):
|
def get_configvalue(self, config_key, default=None):
|
||||||
"""
|
"""
|
||||||
Retrieve a configuration value.
|
Retrieve a configuration value.
|
||||||
|
|
||||||
|
|
@ -35,7 +35,7 @@ class ConfigValueManager(models.Manager):
|
||||||
try:
|
try:
|
||||||
return self.get(db_key__iexact=config_key).db_value
|
return self.get(db_key__iexact=config_key).db_value
|
||||||
except self.model.DoesNotExist:
|
except self.model.DoesNotExist:
|
||||||
return None
|
return default
|
||||||
|
|
||||||
# a simple wrapper for consistent naming in utils.search
|
# a simple wrapper for consistent naming in utils.search
|
||||||
def config_search(self, ostring):
|
def config_search(self, ostring):
|
||||||
|
|
@ -46,7 +46,7 @@ class ConfigValueManager(models.Manager):
|
||||||
"""
|
"""
|
||||||
return self.get_configvalue(ostring)
|
return self.get_configvalue(ostring)
|
||||||
|
|
||||||
def conf(self, db_key=None, db_value=None, delete=False):
|
def conf(self, db_key=None, db_value=None, delete=False, default=None):
|
||||||
"""
|
"""
|
||||||
Wrapper to access the Config database.
|
Wrapper to access the Config database.
|
||||||
This will act as a get/setter, lister or deleter
|
This will act as a get/setter, lister or deleter
|
||||||
|
|
@ -64,7 +64,7 @@ class ConfigValueManager(models.Manager):
|
||||||
elif db_value != None:
|
elif db_value != None:
|
||||||
self.set_configvalue(db_key, db_value)
|
self.set_configvalue(db_key, db_value)
|
||||||
else:
|
else:
|
||||||
return self.get_configvalue(db_key)
|
return self.get_configvalue(db_key, default=default)
|
||||||
|
|
||||||
|
|
||||||
class ConnectScreenManager(models.Manager):
|
class ConnectScreenManager(models.Manager):
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ class HelpEntry(SharedMemoryModel):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# HelpEntry Database Model setup
|
# HelpEntry Database Model setup
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -111,10 +111,8 @@ class ObjectDB(TypedObject):
|
||||||
has_player - bool if an active player is currently connected
|
has_player - bool if an active player is currently connected
|
||||||
contents - other objects having this object as location
|
contents - other objects having this object as location
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# ObjectDB Database model setup
|
# ObjectDB Database model setup
|
||||||
#
|
#
|
||||||
|
|
@ -354,8 +352,6 @@ class ObjectDB(TypedObject):
|
||||||
return ObjectDB.objects.get_contents(self)
|
return ObjectDB.objects.get_contents(self)
|
||||||
contents = property(contents_get)
|
contents = property(contents_get)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Nicks - custom nicknames
|
# Nicks - custom nicknames
|
||||||
#
|
#
|
||||||
|
|
@ -379,7 +375,6 @@ class ObjectDB(TypedObject):
|
||||||
# game).
|
# game).
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
def set_nick(self, nick, realname=None):
|
def set_nick(self, nick, realname=None):
|
||||||
"""
|
"""
|
||||||
Map a nick to a realname. Be careful if mapping an
|
Map a nick to a realname. Be careful if mapping an
|
||||||
|
|
@ -514,33 +509,30 @@ class ObjectDB(TypedObject):
|
||||||
break
|
break
|
||||||
cmdhandler.cmdhandler(self.typeclass(self), raw_string)
|
cmdhandler.cmdhandler(self.typeclass(self), raw_string)
|
||||||
|
|
||||||
def msg(self, message, from_obj=None, markup=True):
|
def msg(self, message, from_obj=None, data=None):
|
||||||
"""
|
"""
|
||||||
Emits something to any sessions attached to the object.
|
Emits something to any sessions attached to the object.
|
||||||
|
|
||||||
message (str): The message to send
|
message (str): The message to send
|
||||||
from_obj (obj): object that is sending.
|
from_obj (obj): object that is sending.
|
||||||
markup (bool): Markup. Determines if the message is parsed
|
data (object): an optional data object that may or may not
|
||||||
for special markup, such as ansi colors. If
|
be used by the protocol.
|
||||||
false, all markup will be cleaned from the
|
|
||||||
message in the session.msg() and message
|
|
||||||
passed on as raw text.
|
|
||||||
"""
|
"""
|
||||||
# This is an important function that must always work.
|
# This is an important function that must always work.
|
||||||
# we use a different __getattribute__ to avoid recursive loops.
|
# we use a different __getattribute__ to avoid recursive loops.
|
||||||
|
|
||||||
if object.__getattribute__(self, 'player'):
|
if object.__getattribute__(self, 'player'):
|
||||||
object.__getattribute__(self, 'player').msg(message, markup)
|
object.__getattribute__(self, 'player').msg(message, data)
|
||||||
|
|
||||||
def emit_to(self, message, from_obj=None):
|
def emit_to(self, message, from_obj=None, data=None):
|
||||||
"Deprecated. Alias for msg"
|
"Deprecated. Alias for msg"
|
||||||
self.msg(message, from_obj)
|
self.msg(message, from_obj, data)
|
||||||
|
|
||||||
def msg_contents(self, message, exclude=None):
|
def msg_contents(self, message, exclude=None, from_obj=None, data=None):
|
||||||
"""
|
"""
|
||||||
Emits something to all objects inside an object.
|
Emits something to all objects inside an object.
|
||||||
|
|
||||||
exclude is a list of objects not to send to.
|
exclude is a list of objects not to send to. See self.msg() for more info.
|
||||||
"""
|
"""
|
||||||
contents = self.contents
|
contents = self.contents
|
||||||
if exclude:
|
if exclude:
|
||||||
|
|
@ -549,11 +541,11 @@ class ObjectDB(TypedObject):
|
||||||
contents = [obj for obj in contents
|
contents = [obj for obj in contents
|
||||||
if (obj not in exclude and obj not in exclude)]
|
if (obj not in exclude and obj not in exclude)]
|
||||||
for obj in contents:
|
for obj in contents:
|
||||||
obj.msg(message)
|
obj.msg(message, from_obj=from_obj, data=data)
|
||||||
|
|
||||||
def emit_to_contents(self, message, exclude=None):
|
def emit_to_contents(self, message, exclude=None, from_obj=None, data=None):
|
||||||
"Deprecated. Alias for msg_contents"
|
"Deprecated. Alias for msg_contents"
|
||||||
self.msg_contents(message, exclude)
|
self.msg_contents(message, exclude=exclude, from_obj=from_obj, data=data)
|
||||||
|
|
||||||
def move_to(self, destination, quiet=False,
|
def move_to(self, destination, quiet=False,
|
||||||
emit_to_obj=None):
|
emit_to_obj=None):
|
||||||
|
|
@ -736,12 +728,3 @@ class ObjectDB(TypedObject):
|
||||||
|
|
||||||
# Deferred import to avoid circular import errors.
|
# Deferred import to avoid circular import errors.
|
||||||
from src.commands import cmdhandler
|
from src.commands import cmdhandler
|
||||||
|
|
||||||
|
|
||||||
# from src.typeclasses import idmap
|
|
||||||
# class CachedObj(models.Model):
|
|
||||||
# key = models.CharField(max_length=255, null=True, blank=True)
|
|
||||||
# test = models.BooleanField(default=False)
|
|
||||||
# objects = idmap.CachingManager()
|
|
||||||
# def id(self):
|
|
||||||
# return id(self)
|
|
||||||
|
|
|
||||||
|
|
@ -216,11 +216,11 @@ class PlayerDB(TypedObject):
|
||||||
name = property(name_get, name_set, name_del)
|
name = property(name_get, name_set, name_del)
|
||||||
key = property(name_get, name_set, name_del)
|
key = property(name_get, name_set, name_del)
|
||||||
|
|
||||||
# sessions property (wraps sessionhandler)
|
# sessions property
|
||||||
#@property
|
#@property
|
||||||
def sessions_get(self):
|
def sessions_get(self):
|
||||||
"Getter. Retrieve sessions related to this player/user"
|
"Getter. Retrieve sessions related to this player/user"
|
||||||
return sessionhandler.find_sessions_from_username(self.name)
|
return sessionhandler.SESSIONS.sessions_from_player(self)
|
||||||
#@sessions.setter
|
#@sessions.setter
|
||||||
def sessions_set(self, value):
|
def sessions_set(self, value):
|
||||||
"Setter. Protects the sessions property from adding things"
|
"Setter. Protects the sessions property from adding things"
|
||||||
|
|
@ -251,26 +251,20 @@ class PlayerDB(TypedObject):
|
||||||
# PlayerDB class access methods
|
# PlayerDB class access methods
|
||||||
#
|
#
|
||||||
|
|
||||||
def msg(self, message, from_obj=None, markup=True):
|
def msg(self, outgoing_string, from_obj=None, data=None):
|
||||||
"""
|
"""
|
||||||
This is the main route for sending data to the user.
|
Evennia -> User
|
||||||
|
This is the main route for sending data back to the user from the server.
|
||||||
"""
|
"""
|
||||||
if from_obj:
|
if from_obj:
|
||||||
try:
|
try:
|
||||||
from_obj.at_msg_send(message, self)
|
from_obj.at_msg_send(outgoing_string, self)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if self.character:
|
if self.character:
|
||||||
if self.character.at_msg_receive(message, from_obj):
|
if self.character.at_msg_receive(outgoing_string, from_obj):
|
||||||
for session in object.__getattribute__(self, 'sessions'):
|
for session in object.__getattribute__(self, 'sessions'):
|
||||||
session.msg(message, markup)
|
session.msg(outgoing_string, data)
|
||||||
|
|
||||||
def emit_to(self, message, from_obj=None):
|
|
||||||
"""
|
|
||||||
Deprecated. Use msg instead.
|
|
||||||
"""
|
|
||||||
self.msg(message, from_obj)
|
|
||||||
|
|
||||||
|
|
||||||
def swap_character(self, new_character, delete_old_character=False):
|
def swap_character(self, new_character, delete_old_character=False):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ class Player(TypeClass):
|
||||||
them loose.
|
them loose.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
def at_disconnect(self):
|
def at_disconnect(self, reason=None):
|
||||||
"""
|
"""
|
||||||
Called just before user
|
Called just before user
|
||||||
is disconnected.
|
is disconnected.
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ Common examples of uses of Scripts:
|
||||||
"""
|
"""
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from src.objects.models import ObjectDB
|
|
||||||
from src.typeclasses.models import Attribute, TypedObject
|
from src.typeclasses.models import Attribute, TypedObject
|
||||||
from src.scripts.manager import ScriptManager
|
from src.scripts.manager import ScriptManager
|
||||||
|
|
||||||
|
|
@ -91,7 +90,7 @@ class ScriptDB(TypedObject):
|
||||||
# optional description.
|
# optional description.
|
||||||
db_desc = models.CharField(max_length=255, blank=True)
|
db_desc = models.CharField(max_length=255, blank=True)
|
||||||
# A reference to the database object affected by this Script, if any.
|
# A reference to the database object affected by this Script, if any.
|
||||||
db_obj = models.ForeignKey(ObjectDB, null=True, blank=True)
|
db_obj = models.ForeignKey("objects.ObjectDB", null=True, blank=True)
|
||||||
# how often to run Script (secs). -1 means there is no timer
|
# how often to run Script (secs). -1 means there is no timer
|
||||||
db_interval = models.IntegerField(default=-1)
|
db_interval = models.IntegerField(default=-1)
|
||||||
# start script right away or wait interval seconds first
|
# start script right away or wait interval seconds first
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,7 @@ class CheckSessions(Script):
|
||||||
"called every 60 seconds"
|
"called every 60 seconds"
|
||||||
#print "session check!"
|
#print "session check!"
|
||||||
#print "ValidateSessions run"
|
#print "ValidateSessions run"
|
||||||
sessionhandler.check_all_sessions()
|
sessionhandler.validate_sessions()
|
||||||
|
|
||||||
class ValidateScripts(Script):
|
class ValidateScripts(Script):
|
||||||
"Check script validation regularly"
|
"Check script validation regularly"
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,9 @@ Everything starts at handle_setup()
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core import management
|
from django.core import management
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from src.config.models import ConfigValue, ConnectScreen
|
from src.config.models import ConfigValue, ConnectScreen
|
||||||
from src.objects.models import ObjectDB
|
|
||||||
from src.comms.models import Channel, ChannelConnection
|
|
||||||
from src.players.models import PlayerDB
|
|
||||||
from src.help.models import HelpEntry
|
from src.help.models import HelpEntry
|
||||||
from src.scripts import scripts
|
|
||||||
from src.utils import create
|
from src.utils import create
|
||||||
from src.utils import gametime
|
|
||||||
|
|
||||||
def create_config_values():
|
def create_config_values():
|
||||||
"""
|
"""
|
||||||
|
|
@ -34,14 +28,14 @@ def create_connect_screens():
|
||||||
|
|
||||||
print " Creating startup screen(s) ..."
|
print " Creating startup screen(s) ..."
|
||||||
|
|
||||||
text = "%ch%cb==================================================================%cn"
|
text = "{b=================================================================={n"
|
||||||
text += "\r\n Welcome to %chEvennia%cn! Please type one of the following to begin:\r\n"
|
text += "\r\n Welcome to {wEvennia{n! Please type one of the following to begin:\r\n"
|
||||||
text += "\r\n If you have an existing account, connect to it by typing:\r\n "
|
text += "\r\n If you have an existing account, connect to it by typing:\r\n "
|
||||||
text += "%chconnect <email> <password>%cn\r\n If you need to create an account, "
|
text += "{wconnect <email> <password>{n\r\n If you need to create an account, "
|
||||||
text += "type (without the <>'s):\r\n "
|
text += "type (without the <>'s):\r\n "
|
||||||
text += "%chcreate \"<username>\" <email> <password>%cn\r\n"
|
text += "{wcreate \"<username>\" <email> <password>{n\r\n"
|
||||||
text += "\r\n Enter %chhelp%cn for more info. %chlook%cn will re-show this screen.\r\n"
|
text += "\r\n Enter {whelp{n for more info. {wlook{n will re-show this screen.\r\n"
|
||||||
text += "%ch%cb==================================================================%cn\r\n"
|
text += "{b=================================================================={n\r\n"
|
||||||
ConnectScreen(db_key="Default", db_text=text, db_is_active=True).save()
|
ConnectScreen(db_key="Default", db_text=text, db_is_active=True).save()
|
||||||
|
|
||||||
def get_god_user():
|
def get_god_user():
|
||||||
|
|
@ -116,6 +110,7 @@ def create_channels():
|
||||||
|
|
||||||
# connect the god user to all these channels by default.
|
# connect the god user to all these channels by default.
|
||||||
goduser = get_god_user()
|
goduser = get_god_user()
|
||||||
|
from src.comms.models import ChannelConnection
|
||||||
ChannelConnection.objects.create_connection(goduser, pchan)
|
ChannelConnection.objects.create_connection(goduser, pchan)
|
||||||
ChannelConnection.objects.create_connection(goduser, ichan)
|
ChannelConnection.objects.create_connection(goduser, ichan)
|
||||||
ChannelConnection.objects.create_connection(goduser, cchan)
|
ChannelConnection.objects.create_connection(goduser, cchan)
|
||||||
|
|
@ -164,6 +159,7 @@ def create_system_scripts():
|
||||||
Setup the system repeat scripts. They are automatically started
|
Setup the system repeat scripts. They are automatically started
|
||||||
by the create_script function.
|
by the create_script function.
|
||||||
"""
|
"""
|
||||||
|
from src.scripts import scripts
|
||||||
|
|
||||||
print " Creating and starting global scripts ..."
|
print " Creating and starting global scripts ..."
|
||||||
|
|
||||||
|
|
@ -184,7 +180,7 @@ def start_game_time():
|
||||||
(the uptime can also be found directly from the server though).
|
(the uptime can also be found directly from the server though).
|
||||||
"""
|
"""
|
||||||
print " Starting in-game time ..."
|
print " Starting in-game time ..."
|
||||||
|
from src.utils import gametime
|
||||||
gametime.init_gametime()
|
gametime.init_gametime()
|
||||||
|
|
||||||
def handle_setup(last_step):
|
def handle_setup(last_step):
|
||||||
|
|
@ -230,11 +226,16 @@ def handle_setup(last_step):
|
||||||
setup_func()
|
setup_func()
|
||||||
except Exception:
|
except Exception:
|
||||||
if last_step + num == 2:
|
if last_step + num == 2:
|
||||||
|
from src.players.models import PlayerDB
|
||||||
|
from src.objects.models import ObjectDB
|
||||||
|
|
||||||
for obj in ObjectDB.objects.all():
|
for obj in ObjectDB.objects.all():
|
||||||
obj.delete()
|
obj.delete()
|
||||||
for profile in PlayerDB.objects.all():
|
for profile in PlayerDB.objects.all():
|
||||||
profile.delete()
|
profile.delete()
|
||||||
elif last_step + num == 3:
|
elif last_step + num == 3:
|
||||||
|
from src.comms.models import Channel, ChannelConnection
|
||||||
|
|
||||||
for chan in Channel.objects.all():
|
for chan in Channel.objects.all():
|
||||||
chan.delete()
|
chan.delete()
|
||||||
for conn in ChannelConnection.objects.all():
|
for conn in ChannelConnection.objects.all():
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
"""
|
"""
|
||||||
This module implements the main Evennia
|
This module implements the main Evennia server process, the core of
|
||||||
server process, the core of the game engine.
|
the game engine. Only import this once!
|
||||||
|
|
||||||
|
This module should be started with the 'twistd' executable since it
|
||||||
|
sets up all the networking features. (this is done by
|
||||||
|
game/evennia.py).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -12,75 +17,82 @@ if os.name == 'nt':
|
||||||
|
|
||||||
from twisted.application import internet, service
|
from twisted.application import internet, service
|
||||||
from twisted.internet import protocol, reactor
|
from twisted.internet import protocol, reactor
|
||||||
|
from twisted.web import server, static
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from src.utils import reloads
|
||||||
from src.config.models import ConfigValue
|
from src.config.models import ConfigValue
|
||||||
from src.server.session import SessionProtocol
|
|
||||||
from src.server import sessionhandler
|
from src.server import sessionhandler
|
||||||
from src.server import initial_setup
|
from src.server import initial_setup
|
||||||
from src.utils import reloads
|
|
||||||
from src.utils.utils import get_evennia_version
|
from src.utils.utils import get_evennia_version
|
||||||
from src.comms import channelhandler
|
from src.comms import channelhandler
|
||||||
|
|
||||||
class EvenniaService(service.Service):
|
|
||||||
|
#------------------------------------------------------------
|
||||||
|
# Evennia Server settings
|
||||||
|
#------------------------------------------------------------
|
||||||
|
|
||||||
|
SERVERNAME = settings.SERVERNAME
|
||||||
|
VERSION = get_evennia_version()
|
||||||
|
|
||||||
|
TELNET_PORTS = settings.TELNET_PORTS
|
||||||
|
WEBSERVER_PORTS = settings.WEBSERVER_PORTS
|
||||||
|
|
||||||
|
TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS
|
||||||
|
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS
|
||||||
|
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
|
||||||
|
IMC2_ENABLED = settings.IMC2_ENABLED
|
||||||
|
IRC_ENABLED = settings.IRC_ENABLED
|
||||||
|
|
||||||
|
#------------------------------------------------------------
|
||||||
|
# Evennia Main Server object
|
||||||
|
#------------------------------------------------------------
|
||||||
|
class Evennia(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
The main server service task.
|
The main Evennia server handler. This object sets up the database and
|
||||||
|
tracks and interlinks all the twisted network services that make up
|
||||||
|
evennia.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, application):
|
||||||
|
"""
|
||||||
|
Setup the server.
|
||||||
|
|
||||||
|
application - an instantiated Twisted application
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
|
||||||
# Holds the TCP services.
|
|
||||||
self.service_collection = None
|
|
||||||
self.game_running = True
|
|
||||||
sys.path.append('.')
|
sys.path.append('.')
|
||||||
|
|
||||||
# Database-specific startup optimizations.
|
# create a store of services
|
||||||
if (settings.DATABASE_ENGINE == "sqlite3"
|
self.services = service.IServiceCollection(application)
|
||||||
or hasattr(settings, 'DATABASE')
|
|
||||||
and settings.DATABASE.get('ENGINE', None) == 'django.db.backends.sqlite3'):
|
|
||||||
# run sqlite3 preps
|
|
||||||
self.sqlite3_prep()
|
|
||||||
|
|
||||||
# Begin startup debug output.
|
|
||||||
print '\n' + '-'*50
|
print '\n' + '-'*50
|
||||||
|
|
||||||
last_initial_setup_step = \
|
# Database-specific startup optimizations.
|
||||||
ConfigValue.objects.conf('last_initial_setup_step')
|
self.sqlite3_prep()
|
||||||
|
|
||||||
if not last_initial_setup_step:
|
# Run the initial setup if needed
|
||||||
# None is only returned if the config does not exist,
|
self.run_initial_setup()
|
||||||
# i.e. this is an empty DB that needs populating.
|
|
||||||
print ' Server started for the first time. Setting defaults.'
|
|
||||||
initial_setup.handle_setup(0)
|
|
||||||
print '-'*50
|
|
||||||
|
|
||||||
elif int(last_initial_setup_step) >= 0:
|
|
||||||
# last_setup_step >= 0 means the setup crashed
|
|
||||||
# on one of its modules and setup will resume, 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.
|
|
||||||
print ' Resuming initial setup from step %s.' % \
|
|
||||||
last_initial_setup_step
|
|
||||||
initial_setup.handle_setup(int(last_initial_setup_step))
|
|
||||||
print '-'*50
|
|
||||||
|
|
||||||
# we have to null this here.
|
# we have to null this here.
|
||||||
sessionhandler.change_session_count(0)
|
sessionhandler.SESSIONS.session_count(0)
|
||||||
|
|
||||||
self.start_time = time.time()
|
self.start_time = time.time()
|
||||||
|
|
||||||
# initialize channelhandler
|
# initialize channelhandler
|
||||||
channelhandler.CHANNELHANDLER.update()
|
channelhandler.CHANNELHANDLER.update()
|
||||||
|
|
||||||
# init all global scripts
|
# init all global scripts
|
||||||
reloads.reload_scripts(init_mode=True)
|
reloads.reload_scripts(init_mode=True)
|
||||||
|
|
||||||
# Make output to the terminal.
|
# Make info output to the terminal.
|
||||||
print ' %s (%s) started on port(s):' % \
|
self.terminal_output()
|
||||||
(settings.SERVERNAME, get_evennia_version())
|
|
||||||
for port in settings.GAMEPORTS:
|
|
||||||
print ' * %s' % (port)
|
|
||||||
print '-'*50
|
print '-'*50
|
||||||
|
|
||||||
|
self.game_running = True
|
||||||
|
|
||||||
# Server startup methods
|
# Server startup methods
|
||||||
|
|
||||||
|
|
@ -89,14 +101,50 @@ class EvenniaService(service.Service):
|
||||||
Optimize some SQLite stuff at startup since we
|
Optimize some SQLite stuff at startup since we
|
||||||
can't save it to the database.
|
can't save it to the database.
|
||||||
"""
|
"""
|
||||||
|
if (settings.DATABASE_ENGINE == "sqlite3"
|
||||||
|
or hasattr(settings, 'DATABASE')
|
||||||
|
and settings.DATABASE.get('ENGINE', None)
|
||||||
|
== 'django.db.backends.sqlite3'):
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
cursor.execute("PRAGMA cache_size=10000")
|
cursor.execute("PRAGMA cache_size=10000")
|
||||||
cursor.execute("PRAGMA synchronous=OFF")
|
cursor.execute("PRAGMA synchronous=OFF")
|
||||||
cursor.execute("PRAGMA count_changes=OFF")
|
cursor.execute("PRAGMA count_changes=OFF")
|
||||||
cursor.execute("PRAGMA temp_store=2")
|
cursor.execute("PRAGMA temp_store=2")
|
||||||
|
|
||||||
|
def run_initial_setup(self):
|
||||||
|
"""
|
||||||
|
This attempts to run the initial_setup script of the server.
|
||||||
|
It returns if this is not the first time the server starts.
|
||||||
|
"""
|
||||||
|
last_initial_setup_step = ConfigValue.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.
|
||||||
|
print ' Server started for the first time. Setting defaults.'
|
||||||
|
initial_setup.handle_setup(0)
|
||||||
|
print '-'*50
|
||||||
|
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.
|
||||||
|
print ' Resuming initial setup from step %s.' % \
|
||||||
|
last_initial_setup_step
|
||||||
|
initial_setup.handle_setup(int(last_initial_setup_step))
|
||||||
|
print '-'*50
|
||||||
|
|
||||||
# General methods
|
def terminal_output(self):
|
||||||
|
"""
|
||||||
|
Outputs server startup info to the terminal.
|
||||||
|
"""
|
||||||
|
print ' %s (%s) started on port(s):' % (SERVERNAME, VERSION)
|
||||||
|
if TELNET_ENABLED:
|
||||||
|
print " telnet: " + ", ".join([str(port) for port in TELNET_PORTS])
|
||||||
|
if WEBSERVER_ENABLED:
|
||||||
|
clientstring = ""
|
||||||
|
if WEBCLIENT_ENABLED:
|
||||||
|
clientstring = '/client'
|
||||||
|
print " webserver%s: " % clientstring + ", ".join([str(port) for port in WEBSERVER_PORTS])
|
||||||
|
|
||||||
def shutdown(self, message=None):
|
def shutdown(self, message=None):
|
||||||
"""
|
"""
|
||||||
|
|
@ -104,29 +152,69 @@ class EvenniaService(service.Service):
|
||||||
"""
|
"""
|
||||||
if not message:
|
if not message:
|
||||||
message = 'The server has been shutdown. Please check back soon.'
|
message = 'The server has been shutdown. Please check back soon.'
|
||||||
sessionhandler.announce_all(message)
|
sessionhandler.SESSIONS.disconnect_all_sessions(reason=message)
|
||||||
sessionhandler.disconnect_all_sessions()
|
|
||||||
reactor.callLater(0, reactor.stop)
|
reactor.callLater(0, reactor.stop)
|
||||||
|
|
||||||
def getEvenniaServiceFactory(self):
|
|
||||||
"Retrieve instances of the server"
|
#------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Start the Evennia game server and add all active services
|
||||||
|
#
|
||||||
|
#------------------------------------------------------------
|
||||||
|
|
||||||
|
# twistd requires us to define the variable 'application' so it knows
|
||||||
|
# what to execute from.
|
||||||
|
application = service.Application('Evennia')
|
||||||
|
|
||||||
|
# The main evennia server program. This sets up the database
|
||||||
|
# and is where we store all the other services.
|
||||||
|
EVENNIA = Evennia(application)
|
||||||
|
|
||||||
|
# We group all the various services under the same twisted app.
|
||||||
|
# These will gradually be started as they are initialized below.
|
||||||
|
|
||||||
|
if TELNET_ENABLED:
|
||||||
|
|
||||||
|
# start telnet game connections
|
||||||
|
|
||||||
|
from src.server import telnet
|
||||||
|
|
||||||
|
for port in TELNET_PORTS:
|
||||||
factory = protocol.ServerFactory()
|
factory = protocol.ServerFactory()
|
||||||
factory.protocol = SessionProtocol
|
factory.protocol = telnet.TelnetProtocol
|
||||||
factory.server = self
|
telnet_service = internet.TCPServer(port, factory)
|
||||||
return factory
|
telnet_service.setName('Evennia%s' % port)
|
||||||
|
EVENNIA.services.addService(telnet_service)
|
||||||
|
|
||||||
def start_services(self, application):
|
if WEBSERVER_ENABLED:
|
||||||
"""
|
|
||||||
Starts all of the TCP services.
|
# a django-compatible webserver.
|
||||||
"""
|
|
||||||
self.service_collection = service.IServiceCollection(application)
|
from src.server.webserver import DjangoWebRoot
|
||||||
for port in settings.GAMEPORTS:
|
|
||||||
evennia_server = \
|
# define the root url (/) as a wsgi resource recognized by Django
|
||||||
internet.TCPServer(port, self.getEvenniaServiceFactory())
|
web_root = DjangoWebRoot()
|
||||||
evennia_server.setName('Evennia%s' %port)
|
# point our media resources to url /media
|
||||||
evennia_server.setServiceParent(self.service_collection)
|
media_dir = os.path.join(settings.SRC_DIR, 'web', settings.MEDIA_URL.lstrip('/')) #TODO: Could be made cleaner?
|
||||||
|
web_root.putChild("media", static.File(media_dir))
|
||||||
|
|
||||||
|
if WEBCLIENT_ENABLED:
|
||||||
|
# create ajax client processes at /webclientdata
|
||||||
|
from src.server.webclient import WebClient
|
||||||
|
web_root.putChild("webclientdata", WebClient())
|
||||||
|
|
||||||
|
web_site = server.Site(web_root, logPath=settings.HTTP_LOG_FILE)
|
||||||
|
for port in WEBSERVER_PORTS:
|
||||||
|
# create the webserver
|
||||||
|
webserver = internet.TCPServer(port, web_site)
|
||||||
|
webserver.setName('EvenniaWebServer%s' % port)
|
||||||
|
EVENNIA.services.addService(webserver)
|
||||||
|
|
||||||
|
|
||||||
|
if IMC2_ENABLED:
|
||||||
|
|
||||||
|
# IMC2 channel connections
|
||||||
|
|
||||||
if settings.IMC2_ENABLED:
|
|
||||||
from src.imc2.connection import IMC2ClientFactory
|
from src.imc2.connection import IMC2ClientFactory
|
||||||
from src.imc2 import events as imc2_events
|
from src.imc2 import events as imc2_events
|
||||||
imc2_factory = IMC2ClientFactory()
|
imc2_factory = IMC2ClientFactory()
|
||||||
|
|
@ -134,10 +222,13 @@ class EvenniaService(service.Service):
|
||||||
settings.IMC2_SERVER_PORT,
|
settings.IMC2_SERVER_PORT,
|
||||||
imc2_factory)
|
imc2_factory)
|
||||||
svc.setName('IMC2')
|
svc.setName('IMC2')
|
||||||
svc.setServiceParent(self.service_collection)
|
EVENNIA.services.addService(svc)
|
||||||
imc2_events.add_events()
|
imc2_events.add_events()
|
||||||
|
|
||||||
if settings.IRC_ENABLED:
|
if IRC_ENABLED:
|
||||||
|
|
||||||
|
# IRC channel connections
|
||||||
|
|
||||||
from src.irc.connection import IRC_BotFactory
|
from src.irc.connection import IRC_BotFactory
|
||||||
irc = internet.TCPClient(settings.IRC_NETWORK,
|
irc = internet.TCPClient(settings.IRC_NETWORK,
|
||||||
settings.IRC_PORT,
|
settings.IRC_PORT,
|
||||||
|
|
@ -145,11 +236,4 @@ class EvenniaService(service.Service):
|
||||||
settings.IRC_NETWORK,
|
settings.IRC_NETWORK,
|
||||||
settings.IRC_NICKNAME))
|
settings.IRC_NICKNAME))
|
||||||
irc.setName("%s:%s" % ("IRC", settings.IRC_CHANNEL))
|
irc.setName("%s:%s" % ("IRC", settings.IRC_CHANNEL))
|
||||||
irc.setServiceParent(self.service_collection)
|
EVENNIA.services.addService(irc)
|
||||||
|
|
||||||
|
|
||||||
# Twisted requires us to define an 'application' attribute.
|
|
||||||
application = service.Application('Evennia')
|
|
||||||
# The main mud service. Import this for access to the server methods.
|
|
||||||
mud_service = EvenniaService()
|
|
||||||
mud_service.start_services(application)
|
|
||||||
|
|
|
||||||
|
|
@ -1,214 +1,183 @@
|
||||||
"""
|
"""
|
||||||
This module contains classes related to Sessions. sessionhandler has the things
|
This defines a generic session class.
|
||||||
needed to manage them.
|
|
||||||
|
All protocols should implement this class and its hook methods.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from twisted.conch.telnet import StatefulTelnetProtocol
|
#from django.contrib.auth.models import User
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from src.server import sessionhandler
|
#from src.objects.models import ObjectDB
|
||||||
from src.objects.models import ObjectDB
|
|
||||||
from src.comms.models import Channel
|
from src.comms.models import Channel
|
||||||
from src.config.models import ConnectScreen
|
from src.utils import logger, reloads
|
||||||
from src.commands import cmdhandler
|
from src.commands import cmdhandler
|
||||||
from src.utils import ansi
|
from src.server import sessionhandler
|
||||||
from src.utils import reloads
|
|
||||||
from src.utils import logger
|
|
||||||
from src.utils import utils
|
|
||||||
|
|
||||||
ENCODINGS = settings.ENCODINGS
|
IDLE_TIMEOUT = settings.IDLE_TIMEOUT
|
||||||
|
IDLE_COMMAND = settings.IDLE_COMMAND
|
||||||
|
|
||||||
class SessionProtocol(StatefulTelnetProtocol):
|
|
||||||
|
|
||||||
|
class IOdata(object):
|
||||||
"""
|
"""
|
||||||
This class represents a player's session. Each player
|
A simple storage object that allows for storing
|
||||||
gets a session assigned to them whenever
|
new attributes on it at creation.
|
||||||
they connect to the game server. All communication
|
"""
|
||||||
between game and player goes through here.
|
def __init__(self, **kwargs):
|
||||||
|
"Give keyword arguments to store as new arguments on the object."
|
||||||
|
self.__dict__.update(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
#------------------------------------------------------------
|
||||||
|
# SessionBase class
|
||||||
|
#------------------------------------------------------------
|
||||||
|
|
||||||
|
class SessionBase(object):
|
||||||
|
"""
|
||||||
|
This class represents a player's session and is a template for
|
||||||
|
individual protocols to communicate with Evennia.
|
||||||
|
|
||||||
|
Each player gets a session assigned to them whenever they connect
|
||||||
|
to the game server. All communication between game and player goes
|
||||||
|
through their session.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __str__(self):
|
# use this to uniquely identify the protocol name, e.g. "telnet" or "comet"
|
||||||
"""
|
protocol_key = "BaseProtocol"
|
||||||
String representation of the user session class. We use
|
|
||||||
this a lot in the server logs and stuff.
|
|
||||||
"""
|
|
||||||
if self.logged_in:
|
|
||||||
symbol = '#'
|
|
||||||
else:
|
|
||||||
symbol = '?'
|
|
||||||
return "<%s> %s@%s" % (symbol, self.name, self.address,)
|
|
||||||
|
|
||||||
def connectionMade(self):
|
def session_connect(self, address, suid=None):
|
||||||
"""
|
"""
|
||||||
What to do when we get a connection.
|
The setup of the session. An address (usually an IP address) on any form is required.
|
||||||
"""
|
|
||||||
# setup the parameters
|
|
||||||
self.prep_session()
|
|
||||||
# send info
|
|
||||||
logger.log_infomsg('New connection: %s' % self)
|
|
||||||
# add this new session to handler
|
|
||||||
sessionhandler.add_session(self)
|
|
||||||
# show a connect screen
|
|
||||||
self.game_connect_screen()
|
|
||||||
|
|
||||||
def getClientAddress(self):
|
This should be called by the protocol at connection time.
|
||||||
"""
|
|
||||||
Returns the client's address and port in a tuple. For example
|
|
||||||
('127.0.0.1', 41917)
|
|
||||||
"""
|
|
||||||
return self.transport.client
|
|
||||||
|
|
||||||
def prep_session(self):
|
suid = this is a session id. Needed by some transport protocols.
|
||||||
"""
|
"""
|
||||||
This sets up the main parameters of
|
self.address = address
|
||||||
the session. The game will poll these
|
|
||||||
properties to check the status of the
|
|
||||||
connection and to be able to contact
|
|
||||||
the connected player.
|
|
||||||
"""
|
|
||||||
# main server properties
|
|
||||||
self.server = self.factory.server
|
|
||||||
self.address = self.getClientAddress()
|
|
||||||
|
|
||||||
# player setup
|
# user setup
|
||||||
self.name = None
|
self.name = None
|
||||||
self.uid = None
|
self.uid = None
|
||||||
|
self.suid = suid
|
||||||
self.logged_in = False
|
self.logged_in = False
|
||||||
self.encoding = "utf-8"
|
self.encoding = "utf-8"
|
||||||
|
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
# The time the user last issued a command.
|
# The time the user last issued a command.
|
||||||
self.cmd_last = time.time()
|
self.cmd_last = current_time
|
||||||
# Player-visible idle time, excluding the IDLE command.
|
# Player-visible idle time, excluding the IDLE command.
|
||||||
self.cmd_last_visible = time.time()
|
self.cmd_last_visible = current_time
|
||||||
|
# The time when the user connected.
|
||||||
|
self.conn_time = current_time
|
||||||
# Total number of commands issued.
|
# Total number of commands issued.
|
||||||
self.cmd_total = 0
|
self.cmd_total = 0
|
||||||
# The time when the user connected.
|
|
||||||
self.conn_time = time.time()
|
|
||||||
#self.channels_subscribed = {}
|
#self.channels_subscribed = {}
|
||||||
|
sessionhandler.SESSIONS.add_unloggedin_session(self)
|
||||||
|
# call hook method
|
||||||
|
self.at_connect()
|
||||||
|
|
||||||
def disconnectClient(self):
|
def session_login(self, player):
|
||||||
"""
|
"""
|
||||||
Manually disconnect the client.
|
Private startup mechanisms that need to run at login
|
||||||
"""
|
|
||||||
self.transport.loseConnection()
|
|
||||||
|
|
||||||
def connectionLost(self, reason):
|
player - the connected player
|
||||||
"""
|
"""
|
||||||
Execute this when a client abruplty loses their connection.
|
self.player = player
|
||||||
"""
|
self.user = player.user
|
||||||
logger.log_infomsg('Disconnected: %s' % self)
|
self.uid = self.user.id
|
||||||
self.cemit_info('Disconnected: %s.' % self)
|
self.name = self.user.username
|
||||||
self.handle_close()
|
self.logged_in = True
|
||||||
|
self.conn_time = time.time()
|
||||||
|
|
||||||
def lineReceived(self, raw_string):
|
# Update account's last login time.
|
||||||
"""
|
self.user.last_login = datetime.now()
|
||||||
Communication Player -> Evennia
|
self.user.save()
|
||||||
Any line return indicates a command for the purpose of the MUD.
|
self.log('Logged in: %s' % self)
|
||||||
So we take the user input and pass it to the Player and their currently
|
|
||||||
connected character.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.encoding:
|
# start (persistent) scripts on this object
|
||||||
try:
|
reloads.reload_scripts(obj=self.player.character, init_mode=True)
|
||||||
raw_string = utils.to_unicode(raw_string, encoding=self.encoding)
|
|
||||||
self.execute_cmd(raw_string)
|
|
||||||
return
|
|
||||||
except Exception, e:
|
|
||||||
err = str(e)
|
|
||||||
print err
|
|
||||||
pass
|
|
||||||
|
|
||||||
# malformed/wrong encoding defined on player-try some defaults
|
#add session to connected list
|
||||||
for encoding in ENCODINGS:
|
sessionhandler.SESSIONS.add_loggedin_session(self)
|
||||||
try:
|
|
||||||
raw_string = utils.to_unicode(raw_string, encoding=encoding)
|
#call hook
|
||||||
err = None
|
self.at_login()
|
||||||
break
|
|
||||||
except Exception, e:
|
def session_disconnect(self, reason=None):
|
||||||
err = str(e)
|
"""
|
||||||
continue
|
Clean up the session, removing it from the game and doing some
|
||||||
if err:
|
accounting. This method is used also for non-loggedin
|
||||||
self.sendLine(err)
|
accounts.
|
||||||
|
|
||||||
|
Note that this methods does not close the connection - this is protocol-dependent
|
||||||
|
and have to be done right after this function!
|
||||||
|
"""
|
||||||
|
if self.logged_in:
|
||||||
|
character = self.get_character()
|
||||||
|
if character:
|
||||||
|
character.player.at_disconnect(reason)
|
||||||
|
uaccount = character.player.user
|
||||||
|
uaccount.last_login = datetime.now()
|
||||||
|
uaccount.save()
|
||||||
|
self.logged_in = False
|
||||||
|
sessionhandler.SESSIONS.remove_session(self)
|
||||||
|
|
||||||
|
def session_validate(self):
|
||||||
|
"""
|
||||||
|
Validate the session to make sure they have not been idle for too long
|
||||||
|
"""
|
||||||
|
if IDLE_TIMEOUT > 0 and (time.time() - self.cmd_last) > IDLE_TIMEOUT:
|
||||||
|
self.msg("Idle timeout exceeded, disconnecting.")
|
||||||
|
self.session_disconnect()
|
||||||
|
|
||||||
|
def get_player(self):
|
||||||
|
"""
|
||||||
|
Get the player associated with this session
|
||||||
|
"""
|
||||||
|
if self.logged_in:
|
||||||
|
return self.player
|
||||||
else:
|
else:
|
||||||
self.execute_cmd(raw_string)
|
return None
|
||||||
|
|
||||||
def msg(self, message, markup=True):
|
# if self.logged_in:
|
||||||
"""
|
# character = ObjectDB.objects.get_object_with_user(self.uid)
|
||||||
Communication Evennia -> Player
|
# if not character:
|
||||||
Sends a message to the session.
|
# string = "No player match for session uid: %s" % self.uid
|
||||||
|
# logger.log_errmsg(string)
|
||||||
markup - determines if formatting markup should be
|
# return None
|
||||||
parsed or not. Currently this means ANSI
|
# return character.player
|
||||||
colors, but could also be html tags for
|
# return None
|
||||||
web connections etc.
|
|
||||||
"""
|
|
||||||
if self.encoding:
|
|
||||||
try:
|
|
||||||
message = utils.to_str(message, encoding=self.encoding)
|
|
||||||
self.sendLine(ansi.parse_ansi(message, strip_ansi=not markup))
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# malformed/wrong encoding defined on player - try some defaults
|
|
||||||
for encoding in ENCODINGS:
|
|
||||||
try:
|
|
||||||
message = utils.to_str(message, encoding=encoding)
|
|
||||||
err = None
|
|
||||||
break
|
|
||||||
except Exception, e:
|
|
||||||
err = str(e)
|
|
||||||
continue
|
|
||||||
if err:
|
|
||||||
self.sendLine(err)
|
|
||||||
else:
|
|
||||||
self.sendLine(ansi.parse_ansi(message, strip_ansi=not markup))
|
|
||||||
|
|
||||||
def get_character(self):
|
def get_character(self):
|
||||||
"""
|
"""
|
||||||
Returns the in-game character associated with a session.
|
Returns the in-game character associated with a session.
|
||||||
This returns the typeclass of the object.
|
This returns the typeclass of the object.
|
||||||
"""
|
"""
|
||||||
if self.logged_in:
|
player = self.get_player()
|
||||||
character = ObjectDB.objects.get_object_with_user(self.uid)
|
if player:
|
||||||
if not character:
|
return player.character
|
||||||
string = "No character match for session uid: %s" % self.uid
|
|
||||||
logger.log_errmsg(string)
|
|
||||||
else:
|
|
||||||
return character
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def execute_cmd(self, raw_string):
|
def log(self, message, channel=True):
|
||||||
"""
|
"""
|
||||||
Sends a command to this session's
|
Emits session info to the appropriate outputs and info channels.
|
||||||
character for processing.
|
|
||||||
|
|
||||||
'idle' is a special command that is
|
|
||||||
interrupted already here. It doesn't do
|
|
||||||
anything except silently updates the
|
|
||||||
last-active timer to avoid getting kicked
|
|
||||||
off for idleness.
|
|
||||||
"""
|
"""
|
||||||
# handle the 'idle' command
|
if channel:
|
||||||
if str(raw_string).strip() == 'idle':
|
try:
|
||||||
self.update_counters(idle=True)
|
cchan = settings.CHANNEL_CONNECTINFO
|
||||||
return
|
cchan = Channel.objects.get_channel(cchan[0])
|
||||||
|
cchan.msg("[%s]: %s" % (cchan.key, message))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
logger.log_infomsg(message)
|
||||||
|
|
||||||
# all other inputs, including empty inputs
|
def update_session_counters(self, idle=False):
|
||||||
character = self.get_character()
|
|
||||||
if character:
|
|
||||||
# normal operation.
|
|
||||||
character.execute_cmd(raw_string)
|
|
||||||
else:
|
|
||||||
# we are not logged in yet
|
|
||||||
cmdhandler.cmdhandler(self, raw_string, unloggedin=True)
|
|
||||||
# update our command counters and idle times.
|
|
||||||
self.update_counters()
|
|
||||||
|
|
||||||
def update_counters(self, idle=False):
|
|
||||||
"""
|
"""
|
||||||
Hit this when the user enters a command in order to update idle timers
|
Hit this when the user enters a command in order to update idle timers
|
||||||
and command counters. If silently is True, the public-facing idle time
|
and command counters.
|
||||||
is not updated.
|
|
||||||
"""
|
"""
|
||||||
# Store the timestamp of the user's last command.
|
# Store the timestamp of the user's last command.
|
||||||
self.cmd_last = time.time()
|
self.cmd_last = time.time()
|
||||||
|
|
@ -218,79 +187,124 @@ class SessionProtocol(StatefulTelnetProtocol):
|
||||||
# Player-visible idle time, not used in idle timeout calcs.
|
# Player-visible idle time, not used in idle timeout calcs.
|
||||||
self.cmd_last_visible = time.time()
|
self.cmd_last_visible = time.time()
|
||||||
|
|
||||||
def handle_close(self):
|
def execute_cmd(self, command_string):
|
||||||
"""
|
"""
|
||||||
Break the connection and do some accounting.
|
Execute a command string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# handle the 'idle' command
|
||||||
|
if str(command_string).strip() == IDLE_COMMAND:
|
||||||
|
self.update_session_counters(idle=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# all other inputs, including empty inputs
|
||||||
character = self.get_character()
|
character = self.get_character()
|
||||||
|
|
||||||
if character:
|
if character:
|
||||||
#call hook functions
|
#print "loggedin _execute_cmd: '%s' __ %s" % (command_string, character)
|
||||||
character.at_disconnect()
|
# normal operation.
|
||||||
character.player.at_disconnect()
|
character.execute_cmd(command_string)
|
||||||
uaccount = character.player.user
|
else:
|
||||||
uaccount.last_login = datetime.now()
|
#print "unloggedin _execute_cmd: '%s' __ %s" % (command_string, character)
|
||||||
uaccount.save()
|
# we are not logged in yet; call cmdhandler directly
|
||||||
self.disconnectClient()
|
cmdhandler.cmdhandler(self, command_string, unloggedin=True)
|
||||||
self.logged_in = False
|
|
||||||
sessionhandler.remove_session(self)
|
|
||||||
|
|
||||||
def game_connect_screen(self):
|
def get_data_obj(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Show the banner screen. Grab from the 'connect_screen'
|
Create a data object, storing keyword arguments on itself as arguments.
|
||||||
config directive. If more than one connect screen is
|
|
||||||
defined in the ConnectScreen attribute, it will be
|
|
||||||
random which screen is used.
|
|
||||||
"""
|
"""
|
||||||
screen = ConnectScreen.objects.get_random_connect_screen()
|
return IOdata(**kwargs)
|
||||||
string = ansi.parse_ansi(screen.text)
|
|
||||||
self.msg(string)
|
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.address == other.address
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""
|
||||||
|
String representation of the user session class. We use
|
||||||
|
this a lot in the server logs.
|
||||||
|
"""
|
||||||
|
if self.logged_in:
|
||||||
|
symbol = '#'
|
||||||
|
else:
|
||||||
|
symbol = '?'
|
||||||
|
return "<%s> %s@%s" % (symbol, self.name, self.address,)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
"""
|
||||||
|
Unicode representation
|
||||||
|
"""
|
||||||
|
return u"%s" % str(self)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#------------------------------------------------------------
|
||||||
|
# Session class - inherit from this
|
||||||
|
#------------------------------------------------------------
|
||||||
|
|
||||||
|
class Session(SessionBase):
|
||||||
|
"""
|
||||||
|
The main class to inherit from. Overload the methods here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# exchange this for a unique name you can use to identify the
|
||||||
|
# protocol type this session uses
|
||||||
|
protocol_key = "TemplateProtocol"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Hook methods
|
||||||
|
#
|
||||||
|
|
||||||
|
def at_connect(self):
|
||||||
|
"""
|
||||||
|
This method is called by the connection mechanic after
|
||||||
|
connection has been made. The session is added to the
|
||||||
|
sessionhandler and basic accounting has been made at this
|
||||||
|
point.
|
||||||
|
|
||||||
|
This is the place to put e.g. welcome screens specific to the
|
||||||
|
protocol.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def at_login(self, player):
|
||||||
|
"""
|
||||||
|
This method is called by the login mechanic whenever the user
|
||||||
|
has finished authenticating. The user has been moved to the
|
||||||
|
right sessionhandler list and basic book keeping has been
|
||||||
|
done at this point (so logged_in=True).
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def at_disconnect(self, reason=None):
|
||||||
|
"""
|
||||||
|
This method is called just before cleaning up the session
|
||||||
|
(so still logged_in=True at this point).
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def at_data_in(self, string="", data=None):
|
||||||
|
"""
|
||||||
|
Player -> Evennia
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def at_data_out(self, string="", data=None):
|
||||||
|
"""
|
||||||
|
Evennia -> Player
|
||||||
|
|
||||||
|
string - an string of any form to send to the player
|
||||||
|
data - a data structure of any form
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# easy-access functions
|
||||||
def login(self, player):
|
def login(self, player):
|
||||||
"""
|
"alias for at_login"
|
||||||
After the user has authenticated, this actually
|
self.at_login(player)
|
||||||
logs them in. At this point the session has
|
def logout(self):
|
||||||
a User account tied to it. User is an django
|
"alias for at_logout"
|
||||||
object that handles stuff like permissions and
|
self.at_disconnect()
|
||||||
access, it has no visible precense in the game.
|
def msg(self, string='', data=None):
|
||||||
This User object is in turn tied to a game
|
"alias for at_data_out"
|
||||||
Object, which represents whatever existence
|
self.at_data_out(string, data)
|
||||||
the player has in the game world. This is the
|
|
||||||
'character' referred to in this module.
|
|
||||||
"""
|
|
||||||
# set the session properties
|
|
||||||
|
|
||||||
user = player.user
|
|
||||||
self.uid = user.id
|
|
||||||
self.name = user.username
|
|
||||||
self.logged_in = True
|
|
||||||
self.conn_time = time.time()
|
|
||||||
if player.db.encoding:
|
|
||||||
self.encoding = player.db.encoding
|
|
||||||
|
|
||||||
if not settings.ALLOW_MULTISESSION:
|
|
||||||
# disconnect previous sessions.
|
|
||||||
sessionhandler.disconnect_duplicate_session(self)
|
|
||||||
|
|
||||||
# start (persistent) scripts on this object
|
|
||||||
reloads.reload_scripts(obj=self.get_character(), init_mode=True)
|
|
||||||
|
|
||||||
logger.log_infomsg("Logged in: %s" % self)
|
|
||||||
self.cemit_info('Logged in: %s' % self)
|
|
||||||
|
|
||||||
# Update their account's last login time.
|
|
||||||
user.last_login = datetime.now()
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
def cemit_info(self, message):
|
|
||||||
"""
|
|
||||||
Channel emits info to the appropriate info channel. By default, this
|
|
||||||
is MUDConnections.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
cchan = settings.CHANNEL_CONNECTINFO
|
|
||||||
cchan = Channel.objects.get_channel(cchan[0])
|
|
||||||
cchan.msg("[%s]: %s" % (cchan.key, message))
|
|
||||||
except Exception:
|
|
||||||
logger.log_infomsg(message)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,137 +1,184 @@
|
||||||
"""
|
"""
|
||||||
Sessionhandler, stores and handles
|
This module handles sessions of users connecting
|
||||||
a list of all player connections (sessions).
|
to the server.
|
||||||
|
|
||||||
|
Since Evennia supports several different connection
|
||||||
|
protocols, it is important to have a joint place
|
||||||
|
to store session info. It also makes it easier
|
||||||
|
to dispatch data.
|
||||||
|
|
||||||
|
Whereas server.py handles all setup of the server
|
||||||
|
and database itself, this file handles all that
|
||||||
|
comes after initial startup.
|
||||||
|
|
||||||
|
All new sessions (of whatever protocol) are responsible for
|
||||||
|
registering themselves with this module.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import time
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from src.config.models import ConfigValue
|
from src.config.models import ConfigValue
|
||||||
from src.utils import logger
|
|
||||||
|
|
||||||
# Our list of connected sessions.
|
ALLOW_MULTISESSION = settings.ALLOW_MULTISESSION
|
||||||
SESSIONS = []
|
|
||||||
|
|
||||||
def add_session(session):
|
#------------------------------------------------------------
|
||||||
"""
|
# SessionHandler class
|
||||||
Adds a session to the session list.
|
#------------------------------------------------------------
|
||||||
"""
|
|
||||||
SESSIONS.insert(0, session)
|
|
||||||
change_session_count(1)
|
|
||||||
logger.log_infomsg('Sessions active: %d' % (len(get_sessions(return_unlogged=True),)))
|
|
||||||
|
|
||||||
def get_sessions(return_unlogged=False):
|
class SessionHandler(object):
|
||||||
"""
|
"""
|
||||||
Lists the connected session objects.
|
This object holds the stack of sessions active in the game at
|
||||||
|
any time.
|
||||||
|
|
||||||
|
A session register with the handler in two steps, first by
|
||||||
|
registering itself with the connect() method. This indicates an
|
||||||
|
non-authenticated session. Whenever the session is authenticated
|
||||||
|
the session together with the related player is sent to the login()
|
||||||
|
method.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if return_unlogged:
|
|
||||||
return SESSIONS
|
def __init__(self):
|
||||||
|
"""
|
||||||
|
Init the handler. We track two types of sessions, those
|
||||||
|
who have just connected (unloggedin) and those who have
|
||||||
|
logged in (authenticated).
|
||||||
|
"""
|
||||||
|
self.unloggedin = []
|
||||||
|
self.loggedin = []
|
||||||
|
|
||||||
|
def add_unloggedin_session(self, session):
|
||||||
|
"""
|
||||||
|
Call at first connect. This adds a not-yet authenticated session.
|
||||||
|
"""
|
||||||
|
self.unloggedin.insert(0, session)
|
||||||
|
|
||||||
|
def add_loggedin_session(self, session):
|
||||||
|
"""
|
||||||
|
Log in the previously unloggedin session and the player we by
|
||||||
|
now should know is connected to it. After this point we
|
||||||
|
assume the session to be logged in one way or another.
|
||||||
|
"""
|
||||||
|
# prep the session with player/user info
|
||||||
|
|
||||||
|
|
||||||
|
if not ALLOW_MULTISESSION:
|
||||||
|
# disconnect previous sessions.
|
||||||
|
self.disconnect_duplicate_sessions(session)
|
||||||
|
|
||||||
|
# store/move the session to the right list
|
||||||
|
try:
|
||||||
|
self.unloggedin.remove(session)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
self.loggedin.insert(0, session)
|
||||||
|
self.session_count(1)
|
||||||
|
|
||||||
|
def remove_session(self, session):
|
||||||
|
"""
|
||||||
|
Remove session from the handler
|
||||||
|
"""
|
||||||
|
removed = False
|
||||||
|
try:
|
||||||
|
self.unloggedin.remove(session)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
self.loggedin.remove(session)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
self.session_count(-1)
|
||||||
|
|
||||||
|
def get_sessions(self, include_unloggedin=False):
|
||||||
|
"""
|
||||||
|
Returns the connected session objects.
|
||||||
|
"""
|
||||||
|
if include_unloggedin:
|
||||||
|
return self.loggedin + self.unloggedin
|
||||||
else:
|
else:
|
||||||
return [sess for sess in SESSIONS if sess.logged_in]
|
return self.loggedin
|
||||||
|
|
||||||
def get_session_id_list(return_unlogged=False):
|
def disconnect_all_sessions(self, reason=None):
|
||||||
"""
|
|
||||||
Lists the connected session object ids.
|
|
||||||
"""
|
|
||||||
if return_unlogged:
|
|
||||||
return SESSIONS
|
|
||||||
else:
|
|
||||||
return [sess.uid for sess in SESSIONS if sess.logged_in]
|
|
||||||
|
|
||||||
def disconnect_all_sessions():
|
|
||||||
"""
|
"""
|
||||||
Cleanly disconnect all of the connected sessions.
|
Cleanly disconnect all of the connected sessions.
|
||||||
"""
|
"""
|
||||||
for sess in get_sessions():
|
sessions = self.get_sessions(include_unloggedin=True)
|
||||||
sess.handle_close()
|
for session in sessions:
|
||||||
|
session.session_disconnect(reason)
|
||||||
|
self.session_count(0)
|
||||||
|
|
||||||
def disconnect_duplicate_session(session):
|
def disconnect_duplicate_sessions(self, session):
|
||||||
"""
|
"""
|
||||||
Disconnects any existing session under the same object. This is used in
|
Disconnects any existing sessions with the same game object. This is used in
|
||||||
connection recovery to help with record-keeping.
|
connection recovery to help with record-keeping.
|
||||||
"""
|
"""
|
||||||
SESSIONS = get_sessions()
|
reason = "Your account has been logged in from elsewhere. Disconnecting."
|
||||||
session_pobj = session.get_character()
|
sessions = self.get_sessions()
|
||||||
for other_session in SESSIONS:
|
session_character = self.get_character(session)
|
||||||
other_pobject = other_session.get_character()
|
logged_out = 0
|
||||||
if session_pobj == other_pobject and other_session != session:
|
for other_session in sessions:
|
||||||
other_session.msg("Your account has been logged in from elsewhere, disconnecting.")
|
other_character = self.get_character(other_session)
|
||||||
other_session.disconnectClient()
|
if session_character == other_character and other_session != session:
|
||||||
return True
|
self.remove_session(other_session, reason=reason)
|
||||||
return False
|
logged_out += 1
|
||||||
|
self.session_count(-logged_out)
|
||||||
|
return logged_out
|
||||||
|
|
||||||
def check_all_sessions():
|
def validate_sessions(self):
|
||||||
"""
|
"""
|
||||||
Check all currently connected sessions and see if any are dead.
|
Check all currently connected sessions (logged in and not)
|
||||||
|
and see if any are dead.
|
||||||
"""
|
"""
|
||||||
idle_timeout = int(ConfigValue.objects.conf('idle_timeout'))
|
for session in self.get_sessions(include_unloggedin=True):
|
||||||
|
session.session_validate()
|
||||||
|
|
||||||
if len(SESSIONS) <= 0:
|
def session_count(self, num=None):
|
||||||
return
|
|
||||||
|
|
||||||
if idle_timeout <= 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
for sess in get_sessions(return_unlogged=True):
|
|
||||||
if (time.time() - sess.cmd_last) > idle_timeout:
|
|
||||||
sess.msg("Idle timeout exceeded, disconnecting.")
|
|
||||||
sess.handle_close()
|
|
||||||
|
|
||||||
def change_session_count(num):
|
|
||||||
"""
|
"""
|
||||||
Count number of connected users by use of a config value
|
Count up/down the number of connected, authenticated users.
|
||||||
|
If num is None, the current number of sessions is returned.
|
||||||
|
|
||||||
num can be a positive or negative value. If 0, the counter
|
num can be a positive or negative value to be added to the current count.
|
||||||
will be reset to 0.
|
If 0, the counter will be reset to 0.
|
||||||
"""
|
"""
|
||||||
|
if num == None:
|
||||||
if num == 0:
|
# show the current value. This also syncs it.
|
||||||
# reset
|
return int(ConfigValue.objects.conf('nr_sessions', default=0))
|
||||||
|
elif num == 0:
|
||||||
|
# reset value to 0
|
||||||
ConfigValue.objects.conf('nr_sessions', 0)
|
ConfigValue.objects.conf('nr_sessions', 0)
|
||||||
|
|
||||||
nr = ConfigValue.objects.conf('nr_sessions')
|
|
||||||
if nr == None:
|
|
||||||
nr = 0
|
|
||||||
else:
|
else:
|
||||||
nr = int(nr)
|
# add/remove session count from value
|
||||||
nr += num
|
add = int(ConfigValue.objects.conf('nr_sessions', default=0))
|
||||||
ConfigValue.objects.conf('nr_sessions', str(nr))
|
num = max(0, num + add)
|
||||||
|
ConfigValue.objects.conf('nr_sessions', str(num))
|
||||||
|
|
||||||
|
def sessions_from_player(self, player):
|
||||||
def remove_session(session):
|
|
||||||
"""
|
"""
|
||||||
Removes a session from the session list.
|
Given a player, return any matching sessions.
|
||||||
"""
|
|
||||||
try:
|
|
||||||
SESSIONS.remove(session)
|
|
||||||
change_session_count(-1)
|
|
||||||
logger.log_infomsg('Sessions active: %d' % (len(get_sessions()),))
|
|
||||||
except ValueError:
|
|
||||||
# the session was already removed.
|
|
||||||
logger.log_errmsg("Unable to remove session: %s" % (session,))
|
|
||||||
return
|
|
||||||
|
|
||||||
def find_sessions_from_username(username):
|
|
||||||
"""
|
|
||||||
Given a username, return any matching sessions.
|
|
||||||
"""
|
"""
|
||||||
|
username = player.user.username
|
||||||
try:
|
try:
|
||||||
uobj = User.objects.get(username=username)
|
uobj = User.objects.get(username=username)
|
||||||
uid = uobj.id
|
|
||||||
return [session for session in SESSIONS if session.uid == uid]
|
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
uid = uobj.id
|
||||||
|
return [session for session in self.loggedin if session.uid == uid]
|
||||||
|
|
||||||
def sessions_from_object(targ_object):
|
def sessions_from_character(self, character):
|
||||||
"""
|
"""
|
||||||
Returns a list of matching session objects, or None if there are no matches.
|
Given a game character, return any matching sessions.
|
||||||
|
"""
|
||||||
|
player = character.player
|
||||||
|
if player:
|
||||||
|
return self.sessions_from_player(player)
|
||||||
|
return None
|
||||||
|
|
||||||
targobject: (Object) The object to match.
|
def session_from_suid(self, suid):
|
||||||
"""
|
"""
|
||||||
return [session for session in SESSIONS
|
Given a session id, retrieve the session (this is primarily
|
||||||
if session.get_character() == targ_object]
|
intended to be called by web clients)
|
||||||
|
"""
|
||||||
|
return [sess for sess in self.get_sessions(include_unloggedin=True) if sess.suid and sess.suid == suid]
|
||||||
|
|
||||||
|
SESSIONS = SessionHandler()
|
||||||
|
|
||||||
|
|
||||||
def announce_all(message):
|
|
||||||
"""
|
|
||||||
Announces something to all connected players.
|
|
||||||
"""
|
|
||||||
for session in get_sessions():
|
|
||||||
session.msg('%s' % message)
|
|
||||||
|
|
|
||||||
153
src/server/telnet.py
Normal file
153
src/server/telnet.py
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
"""
|
||||||
|
This module implements the telnet protocol.
|
||||||
|
|
||||||
|
This depends on a generic session module that implements
|
||||||
|
the actual login procedure of the game, tracks
|
||||||
|
sessions etc.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from twisted.conch.telnet import StatefulTelnetProtocol
|
||||||
|
from django.conf import settings
|
||||||
|
from src.config.models import ConnectScreen
|
||||||
|
from src.server import session
|
||||||
|
from src.utils import ansi, utils
|
||||||
|
|
||||||
|
ENCODINGS = settings.ENCODINGS
|
||||||
|
|
||||||
|
class TelnetProtocol(StatefulTelnetProtocol, session.Session):
|
||||||
|
"""
|
||||||
|
Each player connecting over telnet (ie using most traditional mud
|
||||||
|
clients) gets a telnet protocol instance assigned to them. All
|
||||||
|
communication between game and player goes through here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# identifier in case one needs to easily separate protocols at run-time.
|
||||||
|
protocol_key = "telnet"
|
||||||
|
|
||||||
|
# telnet-specific hooks
|
||||||
|
|
||||||
|
def connectionMade(self):
|
||||||
|
"""
|
||||||
|
This is called when the connection is first
|
||||||
|
established.
|
||||||
|
"""
|
||||||
|
# initialize the session
|
||||||
|
self.session_connect(self.getClientAddress())
|
||||||
|
|
||||||
|
def connectionLost(self, reason="Disconnecting. Goodbye for now."):
|
||||||
|
"""
|
||||||
|
This is executed when the connection is lost for
|
||||||
|
whatever reason. It should also be called from
|
||||||
|
self.at_disconnect() so one can close the connection
|
||||||
|
manually without having to know the name of this specific
|
||||||
|
method.
|
||||||
|
"""
|
||||||
|
self.session_disconnect(reason)
|
||||||
|
self.transport.loseConnection()
|
||||||
|
|
||||||
|
def getClientAddress(self):
|
||||||
|
"""
|
||||||
|
Returns the client's address and port in a tuple. For example
|
||||||
|
('127.0.0.1', 41917)
|
||||||
|
"""
|
||||||
|
return self.transport.client
|
||||||
|
|
||||||
|
def lineReceived(self, string):
|
||||||
|
"""
|
||||||
|
Communication Player -> Evennia. Any line return indicates a
|
||||||
|
command for the purpose of the MUD. So we take the user input
|
||||||
|
and pass it on to the game engine.
|
||||||
|
"""
|
||||||
|
self.at_data_in(string)
|
||||||
|
|
||||||
|
def lineSend(self, string):
|
||||||
|
"""
|
||||||
|
Communication Evennia -> Player
|
||||||
|
Any string sent should already have been
|
||||||
|
properly formatted and processed
|
||||||
|
before reaching this point.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.sendLine(string) #this is the telnet-specific method for sending
|
||||||
|
|
||||||
|
# session-general method hooks
|
||||||
|
|
||||||
|
def at_connect(self):
|
||||||
|
"""
|
||||||
|
Show the banner screen. Grab from the 'connect_screen'
|
||||||
|
config directive. If more than one connect screen is
|
||||||
|
defined in the ConnectScreen attribute, it will be
|
||||||
|
random which screen is used.
|
||||||
|
"""
|
||||||
|
self.telnet_markup = True
|
||||||
|
# show screen
|
||||||
|
screen = ConnectScreen.objects.get_random_connect_screen()
|
||||||
|
string = ansi.parse_ansi(screen.text)
|
||||||
|
self.lineSend(string)
|
||||||
|
|
||||||
|
def at_login(self):
|
||||||
|
"""
|
||||||
|
Called after authentication. self.logged_in=True at this point.
|
||||||
|
"""
|
||||||
|
if self.player.has_attribute('telnet_markup'):
|
||||||
|
self.telnet_markup = self.player.get_attribute("telnet_markup")
|
||||||
|
else:
|
||||||
|
self.telnet_markup = True
|
||||||
|
|
||||||
|
def at_disconnect(self, reason="Connection closed. Goodbye for now."):
|
||||||
|
"""
|
||||||
|
Disconnect from server
|
||||||
|
"""
|
||||||
|
if reason:
|
||||||
|
self.lineSend(reason)
|
||||||
|
self.connectionLost(reasoon)
|
||||||
|
|
||||||
|
def at_data_out(self, string, data=None):
|
||||||
|
"""
|
||||||
|
Data Evennia -> Player access hook. 'data' argument is ignored.
|
||||||
|
"""
|
||||||
|
if self.encoding:
|
||||||
|
try:
|
||||||
|
string = utils.to_str(string, encoding=self.encoding)
|
||||||
|
self.lineSend(ansi.parse_ansi(string, strip_ansi=not self.telnet_markup))
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# malformed/wrong encoding defined on player - try some defaults
|
||||||
|
for encoding in ENCODINGS:
|
||||||
|
try:
|
||||||
|
string = utils.to_str(string, encoding=encoding)
|
||||||
|
err = None
|
||||||
|
break
|
||||||
|
except Exception, e:
|
||||||
|
err = str(e)
|
||||||
|
continue
|
||||||
|
if err:
|
||||||
|
self.lineSend(err)
|
||||||
|
else:
|
||||||
|
self.lineSend(ansi.parse_ansi(string, strip_ansi=not self.telnet_markup))
|
||||||
|
|
||||||
|
def at_data_in(self, string, data=None):
|
||||||
|
"""
|
||||||
|
Line from Player -> Evennia. 'data' argument is not used.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.encoding:
|
||||||
|
try:
|
||||||
|
string = utils.to_unicode(string, encoding=self.encoding)
|
||||||
|
self.execute_cmd(string)
|
||||||
|
return
|
||||||
|
except Exception, e:
|
||||||
|
err = str(e)
|
||||||
|
print err
|
||||||
|
# malformed/wrong encoding defined on player - try some defaults
|
||||||
|
for encoding in ENCODINGS:
|
||||||
|
try:
|
||||||
|
string = utils.to_unicode(string, encoding=encoding)
|
||||||
|
err = None
|
||||||
|
break
|
||||||
|
except Exception, e:
|
||||||
|
err = str(e)
|
||||||
|
continue
|
||||||
|
self.execute_cmd(self, string)
|
||||||
286
src/server/webclient.py
Normal file
286
src/server/webclient.py
Normal file
|
|
@ -0,0 +1,286 @@
|
||||||
|
"""
|
||||||
|
Web client server resource.
|
||||||
|
|
||||||
|
The Evennia web client consists of two components running
|
||||||
|
on twisted and django. They are both a part of the Evennia
|
||||||
|
website url tree (so the testing website might be located
|
||||||
|
on http://localhost:8020/, whereas the webclient can be
|
||||||
|
found on http://localhost:8020/webclient.)
|
||||||
|
|
||||||
|
/webclient - this url is handled through django's template
|
||||||
|
system and serves the html page for the client
|
||||||
|
itself along with its javascript chat program.
|
||||||
|
/webclientdata - this url is called by the ajax chat using
|
||||||
|
POST requests (long-polling when necessary)
|
||||||
|
The WebClient resource in this module will
|
||||||
|
handle these requests and act as a gateway
|
||||||
|
to sessions connected over the webclient.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from twisted.web import server, resource
|
||||||
|
from twisted.internet import defer
|
||||||
|
|
||||||
|
from django.utils import simplejson
|
||||||
|
from django.utils.functional import Promise
|
||||||
|
from django.utils.encoding import force_unicode
|
||||||
|
from django.conf import settings
|
||||||
|
from src.utils import utils
|
||||||
|
from src.utils.ansi2html import parse_html
|
||||||
|
from src.config.models import ConnectScreen
|
||||||
|
from src.server import session, sessionhandler
|
||||||
|
|
||||||
|
SERVERNAME = settings.SERVERNAME
|
||||||
|
ENCODINGS = settings.ENCODINGS
|
||||||
|
|
||||||
|
# defining a simple json encoder for returning
|
||||||
|
# django data to the client. Might need to
|
||||||
|
# extend this if one wants to send more
|
||||||
|
# complex database objects too.
|
||||||
|
|
||||||
|
class LazyEncoder(simplejson.JSONEncoder):
|
||||||
|
def default(self, obj):
|
||||||
|
if isinstance(obj, Promise):
|
||||||
|
return force_unicode(obj)
|
||||||
|
return super(LazyEncoder, self).default(obj)
|
||||||
|
def jsonify(obj):
|
||||||
|
return simplejson.dumps(obj, ensure_ascii=False, cls=LazyEncoder)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# WebClient resource - this is called by the ajax client
|
||||||
|
# using POST requests to /webclientdata.
|
||||||
|
#
|
||||||
|
|
||||||
|
class WebClient(resource.Resource):
|
||||||
|
"""
|
||||||
|
An ajax/comet long-polling transport protocol for
|
||||||
|
"""
|
||||||
|
isLeaf = True
|
||||||
|
allowedMethods = ('POST',)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.requests = {}
|
||||||
|
self.databuffer = {}
|
||||||
|
|
||||||
|
def getChild(self, path, request):
|
||||||
|
"""
|
||||||
|
This is the place to put dynamic content.
|
||||||
|
"""
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _responseFailed(self, failure, suid, request):
|
||||||
|
"callback if a request is lost/timed out"
|
||||||
|
try:
|
||||||
|
self.requests.get(suid, []).remove(request)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def lineSend(self, suid, string, data=None):
|
||||||
|
"""
|
||||||
|
This adds the data to the buffer and/or sends it to
|
||||||
|
the client as soon as possible.
|
||||||
|
"""
|
||||||
|
requests = self.requests.get(suid, None)
|
||||||
|
if requests:
|
||||||
|
request = requests.pop(0)
|
||||||
|
# we have a request waiting. Return immediately.
|
||||||
|
request.write(jsonify({'msg':string, 'data':data}))
|
||||||
|
request.finish()
|
||||||
|
self.requests[suid] = requests
|
||||||
|
else:
|
||||||
|
# no waiting request. Store data in buffer
|
||||||
|
dataentries = self.databuffer.get(suid, [])
|
||||||
|
dataentries.append(jsonify({'msg':string, 'data':data}))
|
||||||
|
self.databuffer[suid] = dataentries
|
||||||
|
|
||||||
|
def disconnect(self, suid):
|
||||||
|
"Disconnect session"
|
||||||
|
sess = sessionhandler.SESSIONS.session_from_suid(suid)
|
||||||
|
if sess:
|
||||||
|
sess[0].session_disconnect()
|
||||||
|
if self.requests.has_key(suid):
|
||||||
|
for request in self.requests.get(suid, []):
|
||||||
|
request.finish()
|
||||||
|
del self.requests[suid]
|
||||||
|
if self.databuffer.has_key(suid):
|
||||||
|
del self.databuffer[suid]
|
||||||
|
|
||||||
|
def mode_init(self, request):
|
||||||
|
"""
|
||||||
|
This is called by render_POST when the client
|
||||||
|
requests an init mode operation (at startup)
|
||||||
|
"""
|
||||||
|
csess = request.getSession() # obs, this is a cookie, not an evennia session!
|
||||||
|
#csees.expireCallbacks.append(lambda : )
|
||||||
|
suid = csess.uid
|
||||||
|
remote_addr = request.getClientIP()
|
||||||
|
host_string = "%s (%s:%s)" % (SERVERNAME, request.getHost().host, request.getHost().port)
|
||||||
|
self.requests[suid] = []
|
||||||
|
self.databuffer[suid] = []
|
||||||
|
|
||||||
|
sess = sessionhandler.SESSIONS.session_from_suid(suid)
|
||||||
|
if not sess:
|
||||||
|
sess = WebClientSession()
|
||||||
|
sess.client = self
|
||||||
|
sess.session_connect(remote_addr, suid)
|
||||||
|
sessionhandler.SESSIONS.add_unloggedin_session(sess)
|
||||||
|
return jsonify({'msg':host_string})
|
||||||
|
|
||||||
|
def mode_input(self, request):
|
||||||
|
"""
|
||||||
|
This is called by render_POST when the client
|
||||||
|
is sending data to the server.
|
||||||
|
"""
|
||||||
|
string = request.args.get('msg', [''])[0]
|
||||||
|
data = request.args.get('data', [None])[0]
|
||||||
|
suid = request.getSession().uid
|
||||||
|
sess = sessionhandler.SESSIONS.session_from_suid(suid)
|
||||||
|
if sess:
|
||||||
|
sess[0].at_data_in(string, data)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def mode_receive(self, request):
|
||||||
|
"""
|
||||||
|
This is called by render_POST when the client is telling us
|
||||||
|
that it is ready to receive data as soon as it is
|
||||||
|
available. This is the basis of a long-polling (comet)
|
||||||
|
mechanism: the server will wait to reply until data is
|
||||||
|
available.
|
||||||
|
"""
|
||||||
|
suid = request.getSession().uid
|
||||||
|
dataentries = self.databuffer.get(suid, [])
|
||||||
|
if dataentries:
|
||||||
|
return dataentries.pop(0)
|
||||||
|
reqlist = self.requests.get(suid, [])
|
||||||
|
request.notifyFinish().addErrback(self._responseFailed, suid, request)
|
||||||
|
reqlist.append(request)
|
||||||
|
self.requests[suid] = reqlist
|
||||||
|
return server.NOT_DONE_YET
|
||||||
|
|
||||||
|
def render_POST(self, request):
|
||||||
|
"""
|
||||||
|
This function is what Twisted calls with POST requests coming
|
||||||
|
in from the ajax client. The requests should be tagged with
|
||||||
|
different modes depending on what needs to be done, such as
|
||||||
|
initializing or sending/receving data through the request. It
|
||||||
|
uses a long-polling mechanism to avoid sending data unless
|
||||||
|
there is actual data available.
|
||||||
|
"""
|
||||||
|
dmode = request.args.get('mode', [None])[0]
|
||||||
|
if dmode == 'init':
|
||||||
|
# startup. Setup the server.
|
||||||
|
return self.mode_init(request)
|
||||||
|
elif dmode == 'input':
|
||||||
|
# input from the client to the server
|
||||||
|
return self.mode_input(request)
|
||||||
|
elif dmode == 'receive':
|
||||||
|
# the client is waiting to receive data.
|
||||||
|
return self.mode_receive(request)
|
||||||
|
else:
|
||||||
|
# this should not happen if client sends valid data.
|
||||||
|
return ''
|
||||||
|
|
||||||
|
#
|
||||||
|
# A session type handling communication over the
|
||||||
|
# web client interface.
|
||||||
|
#
|
||||||
|
|
||||||
|
class WebClientSession(session.Session):
|
||||||
|
"""
|
||||||
|
This represents a session running in a webclient.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def at_connect(self):
|
||||||
|
"""
|
||||||
|
Show the banner screen. Grab from the 'connect_screen'
|
||||||
|
config directive. If more than one connect screen is
|
||||||
|
defined in the ConnectScreen attribute, it will be
|
||||||
|
random which screen is used.
|
||||||
|
"""
|
||||||
|
# show screen
|
||||||
|
screen = ConnectScreen.objects.get_random_connect_screen()
|
||||||
|
string = parse_html(screen.text)
|
||||||
|
self.client.lineSend(self.suid, string)
|
||||||
|
|
||||||
|
def at_login(self):
|
||||||
|
"""
|
||||||
|
Called after authentication. self.logged_in=True at this point.
|
||||||
|
"""
|
||||||
|
if self.player.has_attribute('telnet_markup'):
|
||||||
|
self.telnet_markup = self.player.get_attribute("telnet_markup")
|
||||||
|
else:
|
||||||
|
self.telnet_markup = True
|
||||||
|
|
||||||
|
def at_disconnect(self, reason=None):
|
||||||
|
"""
|
||||||
|
Disconnect from server
|
||||||
|
"""
|
||||||
|
if reason:
|
||||||
|
self.lineSend(self.suid, reason)
|
||||||
|
self.client.disconnect(self.suid)
|
||||||
|
|
||||||
|
def at_data_out(self, string='', data=None):
|
||||||
|
"""
|
||||||
|
Data Evennia -> Player access hook.
|
||||||
|
|
||||||
|
data argument may be used depending on
|
||||||
|
the client-server implementation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if data:
|
||||||
|
# treat data?
|
||||||
|
pass
|
||||||
|
|
||||||
|
# string handling is similar to telnet
|
||||||
|
if self.encoding:
|
||||||
|
try:
|
||||||
|
string = utils.to_str(string, encoding=self.encoding)
|
||||||
|
#self.client.lineSend(self.suid, ansi.parse_ansi(string, strip_ansi=True))
|
||||||
|
self.client.lineSend(self.suid, parse_html(string))
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# malformed/wrong encoding defined on player - try some defaults
|
||||||
|
for encoding in ENCODINGS:
|
||||||
|
try:
|
||||||
|
string = utils.to_str(string, encoding=encoding)
|
||||||
|
err = None
|
||||||
|
break
|
||||||
|
except Exception, e:
|
||||||
|
err = str(e)
|
||||||
|
continue
|
||||||
|
if err:
|
||||||
|
self.client.lineSend(self.suid, err)
|
||||||
|
else:
|
||||||
|
#self.client.lineSend(self.suid, ansi.parse_ansi(string, strip_ansi=True))
|
||||||
|
self.client.lineSend(self.suid, parse_html(string))
|
||||||
|
def at_data_in(self, string, data=None):
|
||||||
|
"""
|
||||||
|
Input from Player -> Evennia (called by client).
|
||||||
|
Use of 'data' is up to the client - server implementation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# treat data?
|
||||||
|
if data:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# the string part is identical to telnet
|
||||||
|
if self.encoding:
|
||||||
|
try:
|
||||||
|
string = utils.to_unicode(string, encoding=self.encoding)
|
||||||
|
self.execute_cmd(string)
|
||||||
|
return
|
||||||
|
except Exception, e:
|
||||||
|
err = str(e)
|
||||||
|
print err
|
||||||
|
# malformed/wrong encoding defined on player - try some defaults
|
||||||
|
for encoding in ENCODINGS:
|
||||||
|
try:
|
||||||
|
string = utils.to_unicode(string, encoding=encoding)
|
||||||
|
err = None
|
||||||
|
break
|
||||||
|
except Exception, e:
|
||||||
|
err = str(e)
|
||||||
|
continue
|
||||||
|
self.execute_cmd(string)
|
||||||
|
|
||||||
60
src/server/webserver.py
Normal file
60
src/server/webserver.py
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
"""
|
||||||
|
This implements resources for twisted webservers using the wsgi
|
||||||
|
interface of django. This alleviates the need of running e.g. an
|
||||||
|
apache server to serve Evennia's web presence (although you could do
|
||||||
|
that too if desired).
|
||||||
|
|
||||||
|
The actual servers are started inside server.py as part of the Evennia
|
||||||
|
application.
|
||||||
|
|
||||||
|
(Lots of thanks to http://githup.com/clemensha/twisted-wsgi-django for
|
||||||
|
a great example/aid on how to do this.)
|
||||||
|
|
||||||
|
"""
|
||||||
|
from twisted.web import resource
|
||||||
|
from twisted.python import threadpool
|
||||||
|
from twisted.internet import reactor
|
||||||
|
|
||||||
|
from twisted.web.wsgi import WSGIResource
|
||||||
|
from django.core.handlers.wsgi import WSGIHandler
|
||||||
|
|
||||||
|
#
|
||||||
|
# Website server resource
|
||||||
|
#
|
||||||
|
|
||||||
|
class DjangoWebRoot(resource.Resource):
|
||||||
|
"""
|
||||||
|
This creates a web root (/) that Django
|
||||||
|
understands by tweaking the way the
|
||||||
|
child instancee are recognized.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""
|
||||||
|
Setup the django+twisted resource
|
||||||
|
"""
|
||||||
|
resource.Resource.__init__(self)
|
||||||
|
self.wsgi_resource = self._wsgi_resource()
|
||||||
|
|
||||||
|
def _wsgi_resource(self):
|
||||||
|
"""
|
||||||
|
Sets up a threaded webserver resource by tying
|
||||||
|
django and twisted together.
|
||||||
|
"""
|
||||||
|
# Start the threading
|
||||||
|
pool = threadpool.ThreadPool()
|
||||||
|
pool.start()
|
||||||
|
# Set it up so the pool stops after e.g. Ctrl-C kills the server
|
||||||
|
reactor.addSystemEventTrigger('after', 'shutdown', pool.stop)
|
||||||
|
# combine twisted's wsgi resource with django's wsgi handler
|
||||||
|
wsgi_resource = WSGIResource(reactor, pool, WSGIHandler())
|
||||||
|
return wsgi_resource
|
||||||
|
|
||||||
|
def getChild(self, path, request):
|
||||||
|
"""
|
||||||
|
To make things work we nudge the
|
||||||
|
url tree to make this the root.
|
||||||
|
"""
|
||||||
|
path0 = request.prepath.pop(0)
|
||||||
|
request.postpath.insert(0, path0)
|
||||||
|
return self.wsgi_resource
|
||||||
|
|
@ -22,8 +22,21 @@ import os
|
||||||
# This is the name of your server and/or site.
|
# This is the name of your server and/or site.
|
||||||
# Can be anything.
|
# Can be anything.
|
||||||
SERVERNAME = "Evennia"
|
SERVERNAME = "Evennia"
|
||||||
# A list of ports the Evennia server listens on. Can be one or many.
|
# Activate telnet service
|
||||||
GAMEPORTS = [4000]
|
TELNET_ENABLED = True
|
||||||
|
# A list of ports the Evennia telnet server listens on
|
||||||
|
# Can be one or many.
|
||||||
|
TELNET_PORTS = [4000]
|
||||||
|
# Start the evennia django+twisted webserver so you can
|
||||||
|
# browse the evennia website and the admin interface
|
||||||
|
# (Obs - further web configuration can be found below
|
||||||
|
# in the section 'Config for Django web features')
|
||||||
|
WEBSERVER_ENABLED = True
|
||||||
|
# A list of ports the Evennia webserver listens on
|
||||||
|
WEBSERVER_PORTS = [8020]
|
||||||
|
# Start the evennia ajax client on /webclient
|
||||||
|
# (the webserver must also be running)
|
||||||
|
WEBCLIENT_ENABLED = True
|
||||||
# Activate full persistence if you want everything in-game to be
|
# Activate full persistence if you want everything in-game to be
|
||||||
# stored to the database. With it set, you can do typeclass.attr=value
|
# stored to the database. With it set, you can do typeclass.attr=value
|
||||||
# and value will be saved to the database under the name 'attr'.
|
# and value will be saved to the database under the name 'attr'.
|
||||||
|
|
@ -54,6 +67,9 @@ GAME_DIR = os.path.join(BASE_PATH, 'game')
|
||||||
# Place to put log files
|
# Place to put log files
|
||||||
LOG_DIR = os.path.join(GAME_DIR, 'logs')
|
LOG_DIR = os.path.join(GAME_DIR, 'logs')
|
||||||
DEFAULT_LOG_FILE = os.path.join(LOG_DIR, 'evennia.log')
|
DEFAULT_LOG_FILE = os.path.join(LOG_DIR, 'evennia.log')
|
||||||
|
# Where to log server requests to the web server. This is VERY spammy, so this
|
||||||
|
# file should be removed at regular intervals.
|
||||||
|
HTTP_LOG_FILE = os.path.join(LOG_DIR, 'http_requests.log')
|
||||||
# Local time zone for this installation. All choices can be found here:
|
# Local time zone for this installation. All choices can be found here:
|
||||||
# http://www.postgresql.org/docs/8.0/interactive/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
|
# http://www.postgresql.org/docs/8.0/interactive/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
|
||||||
TIME_ZONE = 'UTC'
|
TIME_ZONE = 'UTC'
|
||||||
|
|
@ -70,12 +86,18 @@ IMPORT_MUX_HELP = False
|
||||||
# thrown off by sending the empty system command 'idle' to the server
|
# thrown off by sending the empty system command 'idle' to the server
|
||||||
# at regular intervals. Set <=0 to deactivate idle timout completely.
|
# at regular intervals. Set <=0 to deactivate idle timout completely.
|
||||||
IDLE_TIMEOUT = 3600
|
IDLE_TIMEOUT = 3600
|
||||||
# If the PlayerAttribute 'encoding' is not set, or wrong encoding is
|
# The idle command can be sent to keep your session active without actually
|
||||||
# given, this list is tried, in order, stopping on the first match.
|
# having to spam normal commands regularly. It gives no feedback, only updates
|
||||||
# Add sets for languages/regions your players are likely to use. (see
|
# the idle timer.
|
||||||
# http://en.wikipedia.org/wiki/Character_encoding).
|
IDLE_COMMAND = "idle"
|
||||||
|
# The set of encodings tried. A Player object may set an attribute "encoding" on
|
||||||
|
# itself to match the client used. If not set, or wrong encoding is
|
||||||
|
# given, this list is tried, in order, aborting on the first match.
|
||||||
|
# Add sets for languages/regions your players are likely to use.
|
||||||
|
# (see http://en.wikipedia.org/wiki/Character_encoding)
|
||||||
ENCODINGS = ["utf-8", "latin-1", "ISO-8859-1"]
|
ENCODINGS = ["utf-8", "latin-1", "ISO-8859-1"]
|
||||||
|
|
||||||
|
|
||||||
###################################################
|
###################################################
|
||||||
# Evennia Database config
|
# Evennia Database config
|
||||||
###################################################
|
###################################################
|
||||||
|
|
@ -314,8 +336,8 @@ IRC_NICKNAME = ""
|
||||||
# While DEBUG is False, show a regular server error page on the web
|
# While DEBUG is False, show a regular server error page on the web
|
||||||
# stuff, email the traceback to the people in the ADMINS tuple
|
# stuff, email the traceback to the people in the ADMINS tuple
|
||||||
# below. If True, show a detailed traceback for the web
|
# below. If True, show a detailed traceback for the web
|
||||||
# browser to display. Note however that this might leak memory when
|
# browser to display. Note however that this will leak memory when
|
||||||
# active, so make sure turn it off for a production server!
|
# active, so make sure to turn it off for a production server!
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
# While true, show "pretty" error messages for template syntax errors.
|
# While true, show "pretty" error messages for template syntax errors.
|
||||||
TEMPLATE_DEBUG = DEBUG
|
TEMPLATE_DEBUG = DEBUG
|
||||||
|
|
@ -350,7 +372,7 @@ USE_I18N = False
|
||||||
# you're running Django's built-in test server. Normally you want a webserver
|
# you're running Django's built-in test server. Normally you want a webserver
|
||||||
# that is optimized for serving static content to handle media files (apache,
|
# that is optimized for serving static content to handle media files (apache,
|
||||||
# lighttpd).
|
# lighttpd).
|
||||||
SERVE_MEDIA = True
|
SERVE_MEDIA = False
|
||||||
|
|
||||||
# The master urlconf file that contains all of the sub-branches to the
|
# The master urlconf file that contains all of the sub-branches to the
|
||||||
# applications.
|
# applications.
|
||||||
|
|
@ -367,7 +389,7 @@ MEDIA_URL = '/media/'
|
||||||
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
|
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
|
||||||
# trailing slash.
|
# trailing slash.
|
||||||
# Examples: "http://foo.com/media/", "/media/".
|
# Examples: "http://foo.com/media/", "/media/".
|
||||||
ADMIN_MEDIA_PREFIX = '/media/amedia/'
|
ADMIN_MEDIA_PREFIX = '/media/admin/'
|
||||||
# The name of the currently selected web template. This corresponds to the
|
# The name of the currently selected web template. This corresponds to the
|
||||||
# directory names shown in the webtemplates directory.
|
# directory names shown in the webtemplates directory.
|
||||||
ACTIVE_TEMPLATE = 'prosimii'
|
ACTIVE_TEMPLATE = 'prosimii'
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ class ANSIParser(object):
|
||||||
|
|
||||||
# MUX-style mappings %cr %cn etc
|
# MUX-style mappings %cr %cn etc
|
||||||
|
|
||||||
mux_ansi_map = [
|
self.mux_ansi_map = [
|
||||||
(r'%r', ANSITable.ansi["return"]),
|
(r'%r', ANSITable.ansi["return"]),
|
||||||
(r'%t', ANSITable.ansi["tab"]),
|
(r'%t', ANSITable.ansi["tab"]),
|
||||||
(r'%b', ANSITable.ansi["space"]),
|
(r'%b', ANSITable.ansi["space"]),
|
||||||
|
|
@ -102,7 +102,7 @@ class ANSIParser(object):
|
||||||
|
|
||||||
hilite = ANSITable.ansi['hilite']
|
hilite = ANSITable.ansi['hilite']
|
||||||
normal = ANSITable.ansi['normal']
|
normal = ANSITable.ansi['normal']
|
||||||
ext_ansi_map = [
|
self.ext_ansi_map = [
|
||||||
(r'{r', hilite + ANSITable.ansi['red']),
|
(r'{r', hilite + ANSITable.ansi['red']),
|
||||||
(r'{R', normal + ANSITable.ansi['red']),
|
(r'{R', normal + ANSITable.ansi['red']),
|
||||||
(r'{g', hilite + ANSITable.ansi['green']),
|
(r'{g', hilite + ANSITable.ansi['green']),
|
||||||
|
|
@ -122,7 +122,7 @@ class ANSIParser(object):
|
||||||
(r'{n', normal) #reset
|
(r'{n', normal) #reset
|
||||||
]
|
]
|
||||||
|
|
||||||
self.ansi_map = mux_ansi_map + ext_ansi_map
|
self.ansi_map = self.mux_ansi_map + self.ext_ansi_map
|
||||||
|
|
||||||
# prepare regex matching
|
# prepare regex matching
|
||||||
self.ansi_sub = [(re.compile(sub[0], re.DOTALL), sub[1])
|
self.ansi_sub = [(re.compile(sub[0], re.DOTALL), sub[1])
|
||||||
|
|
@ -149,6 +149,8 @@ class ANSIParser(object):
|
||||||
string = self.ansi_regex.sub("", string)
|
string = self.ansi_regex.sub("", string)
|
||||||
return string
|
return string
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ANSI_PARSER = ANSIParser()
|
ANSI_PARSER = ANSIParser()
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
@ -161,3 +163,5 @@ def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return parser.parse_ansi(string, strip_ansi=strip_ansi)
|
return parser.parse_ansi(string, strip_ansi=strip_ansi)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
137
src/utils/ansi2html.py
Normal file
137
src/utils/ansi2html.py
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
|
||||||
|
"""
|
||||||
|
ANSI -> html converter
|
||||||
|
|
||||||
|
Credit for original idea and implementation
|
||||||
|
goes to Muhammad Alkarouri and his
|
||||||
|
snippet #577349 on http://code.activestate.com.
|
||||||
|
|
||||||
|
(extensively modified by Griatch 2010)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import cgi
|
||||||
|
from src.utils import ansi
|
||||||
|
|
||||||
|
class ANSItoHTMLparser(object):
|
||||||
|
"""
|
||||||
|
This class describes a parser for converting from ansi to html.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# mapping html color name <-> ansi code.
|
||||||
|
# Obs order matters - longer ansi codes are replaced first.
|
||||||
|
colorcodes = [('white', '\033[1m\033[37m'),
|
||||||
|
('cyan', '\033[1m\033[36m'),
|
||||||
|
('blue', '\033[1m\033[34m'),
|
||||||
|
('red', '\033[1m\033[31m'),
|
||||||
|
('magenta', '\033[1m\033[35m'),
|
||||||
|
('lime', '\033[1m\033[32m'),
|
||||||
|
('yellow', '\033[1m\033[33m'),
|
||||||
|
('gray', '\033[37m'),
|
||||||
|
('teal', '\033[36m'),
|
||||||
|
('navy', '\033[34m'),
|
||||||
|
('maroon', '\033[31m'),
|
||||||
|
('purple', '\033[35m'),
|
||||||
|
('green', '\033[32m'),
|
||||||
|
('olive', '\033[33m')]
|
||||||
|
normalcode = '\033[0m'
|
||||||
|
tabstop = 4
|
||||||
|
|
||||||
|
re_string = re.compile(r'(?P<htmlchars>[<&>])|(?P<space>^[ \t]+)|(?P<lineend>\r\n|\r|\n)|(?P<protocal>(^|\s)((http|ftp)://.*?))(\s|$)',
|
||||||
|
re.S|re.M|re.I)
|
||||||
|
|
||||||
|
def re_color(self, text):
|
||||||
|
"Replace ansi colors with html color tags"
|
||||||
|
for colorname, code in self.colorcodes:
|
||||||
|
regexp = "(?:%s)(.*?)(?:%s)" % (code, self.normalcode)
|
||||||
|
regexp = regexp.replace('[', r'\[')
|
||||||
|
text = re.sub(regexp, r'''<span style="color: %s">\1</span>''' % colorname, text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
def re_bold(self, text):
|
||||||
|
"Replace ansi hilight with bold text"
|
||||||
|
regexp = "(?:%s)(.*?)(?:%s)" % ('\033[1m', self.normalcode)
|
||||||
|
regexp = regexp.replace('[', r'\[')
|
||||||
|
return re.sub(regexp, r'<span style="font-weight:bold">\1</span>', text)
|
||||||
|
|
||||||
|
def re_underline(self, text):
|
||||||
|
"Replace ansi underline with html equivalent"
|
||||||
|
regexp = "(?:%s)(.*?)(?:%s)" % ('\033[4m', self.normalcode)
|
||||||
|
regexp = regexp.replace('[', r'\[')
|
||||||
|
return re.sub(regexp, r'<span style="text-decoration: underline">\1</span>', text)
|
||||||
|
|
||||||
|
def remove_bells(self, text):
|
||||||
|
"Remove ansi specials"
|
||||||
|
return text.replace('\07', '')
|
||||||
|
|
||||||
|
def remove_backspaces(self, text):
|
||||||
|
"Removes special escape sequences"
|
||||||
|
backspace_or_eol = r'(.\010)|(\033\[K)'
|
||||||
|
n = 1
|
||||||
|
while n > 0:
|
||||||
|
text, n = re.subn(backspace_or_eol, '', text, 1)
|
||||||
|
return text
|
||||||
|
|
||||||
|
def convert_linebreaks(self, text):
|
||||||
|
"Extra method for cleaning linebreaks"
|
||||||
|
return text.replace(r'\n', r'<br>')
|
||||||
|
|
||||||
|
def do_sub(self, m):
|
||||||
|
"Helper method to be passed to re.sub."
|
||||||
|
c = m.groupdict()
|
||||||
|
if c['htmlchars']:
|
||||||
|
return cgi.escape(c['htmlchars'])
|
||||||
|
if c['lineend']:
|
||||||
|
return '<br>'
|
||||||
|
elif c['space']:
|
||||||
|
t = m.group().replace('\t', ' '*self.tabstop)
|
||||||
|
t = t.replace(' ', ' ')
|
||||||
|
return t
|
||||||
|
elif c['space'] == '\t':
|
||||||
|
return ' '*self.tabstop
|
||||||
|
else:
|
||||||
|
url = m.group('protocal')
|
||||||
|
if url.startswith(' '):
|
||||||
|
prefix = ' '
|
||||||
|
url = url[1:]
|
||||||
|
else:
|
||||||
|
prefix = ''
|
||||||
|
last = m.groups()[-1]
|
||||||
|
if last in ['\n', '\r', '\r\n']:
|
||||||
|
last = '<br>'
|
||||||
|
return '%s%s' % (prefix, url)
|
||||||
|
|
||||||
|
def parse(self, text):
|
||||||
|
"""
|
||||||
|
Main access function, converts a text containing
|
||||||
|
ansi codes into html statements.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# parse everything to ansi first
|
||||||
|
text = ansi.parse_ansi(text)
|
||||||
|
|
||||||
|
# convert all ansi to html
|
||||||
|
result = re.sub(self.re_string, self.do_sub, text)
|
||||||
|
result = self.re_color(result)
|
||||||
|
result = self.re_bold(result)
|
||||||
|
result = self.re_underline(result)
|
||||||
|
result = self.remove_bells(result)
|
||||||
|
result = self.convert_linebreaks(result)
|
||||||
|
result = self.remove_backspaces(result)
|
||||||
|
|
||||||
|
# clean out eventual ansi that was missed
|
||||||
|
result = ansi.parse_ansi(result, strip_ansi=True)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
HTML_PARSER = ANSItoHTMLparser()
|
||||||
|
|
||||||
|
#
|
||||||
|
# Access function
|
||||||
|
#
|
||||||
|
|
||||||
|
def parse_html(string, parser=HTML_PARSER):
|
||||||
|
"""
|
||||||
|
Parses a string, replace ansi markup with html
|
||||||
|
"""
|
||||||
|
return parser.parse(string)
|
||||||
|
|
@ -22,11 +22,8 @@ Models covered:
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from src.players.models import PlayerDB
|
|
||||||
from src.help.models import HelpEntry
|
|
||||||
from src.comms.models import Msg, Channel
|
|
||||||
from src.comms import channelhandler
|
|
||||||
from src.comms.managers import to_object
|
|
||||||
from src.permissions.permissions import set_perm
|
from src.permissions.permissions import set_perm
|
||||||
from src.permissions.models import PermissionGroup
|
from src.permissions.models import PermissionGroup
|
||||||
from src.utils import logger
|
from src.utils import logger
|
||||||
|
|
@ -212,6 +209,8 @@ def create_help_entry(key, entrytext, category="General", permissions=None):
|
||||||
general help on the game, more extensive info, in-game setting information
|
general help on the game, more extensive info, in-game setting information
|
||||||
and so on.
|
and so on.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from src.help.models import HelpEntry
|
||||||
try:
|
try:
|
||||||
new_help = HelpEntry()
|
new_help = HelpEntry()
|
||||||
new_help.key = key
|
new_help.key = key
|
||||||
|
|
@ -293,6 +292,8 @@ def create_message(senderobj, message, channels=None,
|
||||||
at the same time, it's up to the command definitions to limit this as
|
at the same time, it's up to the command definitions to limit this as
|
||||||
desired.
|
desired.
|
||||||
"""
|
"""
|
||||||
|
from src.comms.models import Msg
|
||||||
|
from src.comms.managers import to_object
|
||||||
|
|
||||||
def to_player(obj):
|
def to_player(obj):
|
||||||
"Make sure the object is a player object"
|
"Make sure the object is a player object"
|
||||||
|
|
@ -345,6 +346,9 @@ def create_channel(key, aliases=None, description=None,
|
||||||
listen/send/admin permissions are strings if permissions separated
|
listen/send/admin permissions are strings if permissions separated
|
||||||
by commas.
|
by commas.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from src.comms.models import Channel
|
||||||
|
from src.comms import channelhandler
|
||||||
try:
|
try:
|
||||||
new_channel = Channel()
|
new_channel = Channel()
|
||||||
new_channel.key = key
|
new_channel.key = key
|
||||||
|
|
@ -408,6 +412,8 @@ def create_player(name, email, password,
|
||||||
# isn't already registered, and that the password is ok before
|
# isn't already registered, and that the password is ok before
|
||||||
# getting here.
|
# getting here.
|
||||||
|
|
||||||
|
from src.players.models import PlayerDB
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
new_user = user
|
new_user = user
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,16 @@ def is_iter(iterable):
|
||||||
they are actually iterable), since string iterations
|
they are actually iterable), since string iterations
|
||||||
are usually not what we want to do with a string.
|
are usually not what we want to do with a string.
|
||||||
"""
|
"""
|
||||||
if isinstance(iterable, basestring):
|
return hasattr(iterable, '__iter__')
|
||||||
# skip all forms of strings (str, unicode etc)
|
|
||||||
return False
|
# if isinstance(iterable, basestring):
|
||||||
try:
|
# # skip all forms of strings (str, unicode etc)
|
||||||
# check if object implements iter protocol
|
# return False
|
||||||
return iter(iterable)
|
# try:
|
||||||
except TypeError:
|
# # check if object implements iter protocol
|
||||||
return False
|
# return iter(iterable)
|
||||||
|
# except TypeError:
|
||||||
|
# return False
|
||||||
|
|
||||||
def fill(text, width=78):
|
def fill(text, width=78):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
0
src/web/media/admin/__init__.py
Normal file
0
src/web/media/admin/__init__.py
Normal file
1
src/web/media/admin/css/base.css
Symbolic link
1
src/web/media/admin/css/base.css
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/css/base.css
|
||||||
1
src/web/media/admin/css/changelists.css
Symbolic link
1
src/web/media/admin/css/changelists.css
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/css/changelists.css
|
||||||
1
src/web/media/admin/css/dashboard.css
Symbolic link
1
src/web/media/admin/css/dashboard.css
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/css/dashboard.css
|
||||||
1
src/web/media/admin/css/forms.css
Symbolic link
1
src/web/media/admin/css/forms.css
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/css/forms.css
|
||||||
1
src/web/media/admin/css/ie.css
Symbolic link
1
src/web/media/admin/css/ie.css
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/css/ie.css
|
||||||
1
src/web/media/admin/css/login.css
Symbolic link
1
src/web/media/admin/css/login.css
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/css/login.css
|
||||||
1
src/web/media/admin/css/rtl.css
Symbolic link
1
src/web/media/admin/css/rtl.css
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/css/rtl.css
|
||||||
1
src/web/media/admin/css/widgets.css
Symbolic link
1
src/web/media/admin/css/widgets.css
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/css/widgets.css
|
||||||
1
src/web/media/admin/img/admin/arrow-down.gif
Symbolic link
1
src/web/media/admin/img/admin/arrow-down.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/arrow-down.gif
|
||||||
1
src/web/media/admin/img/admin/arrow-up.gif
Symbolic link
1
src/web/media/admin/img/admin/arrow-up.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/arrow-up.gif
|
||||||
1
src/web/media/admin/img/admin/changelist-bg.gif
Symbolic link
1
src/web/media/admin/img/admin/changelist-bg.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/changelist-bg.gif
|
||||||
1
src/web/media/admin/img/admin/changelist-bg_rtl.gif
Symbolic link
1
src/web/media/admin/img/admin/changelist-bg_rtl.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/changelist-bg_rtl.gif
|
||||||
1
src/web/media/admin/img/admin/chooser-bg.gif
Symbolic link
1
src/web/media/admin/img/admin/chooser-bg.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/chooser-bg.gif
|
||||||
1
src/web/media/admin/img/admin/chooser_stacked-bg.gif
Symbolic link
1
src/web/media/admin/img/admin/chooser_stacked-bg.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/chooser_stacked-bg.gif
|
||||||
1
src/web/media/admin/img/admin/default-bg-reverse.gif
Symbolic link
1
src/web/media/admin/img/admin/default-bg-reverse.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/default-bg-reverse.gif
|
||||||
1
src/web/media/admin/img/admin/default-bg.gif
Symbolic link
1
src/web/media/admin/img/admin/default-bg.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/default-bg.gif
|
||||||
1
src/web/media/admin/img/admin/deleted-overlay.gif
Symbolic link
1
src/web/media/admin/img/admin/deleted-overlay.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/deleted-overlay.gif
|
||||||
1
src/web/media/admin/img/admin/icon-no.gif
Symbolic link
1
src/web/media/admin/img/admin/icon-no.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon-no.gif
|
||||||
1
src/web/media/admin/img/admin/icon-unknown.gif
Symbolic link
1
src/web/media/admin/img/admin/icon-unknown.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon-unknown.gif
|
||||||
1
src/web/media/admin/img/admin/icon-yes.gif
Symbolic link
1
src/web/media/admin/img/admin/icon-yes.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon-yes.gif
|
||||||
1
src/web/media/admin/img/admin/icon_addlink.gif
Symbolic link
1
src/web/media/admin/img/admin/icon_addlink.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_addlink.gif
|
||||||
1
src/web/media/admin/img/admin/icon_alert.gif
Symbolic link
1
src/web/media/admin/img/admin/icon_alert.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_alert.gif
|
||||||
1
src/web/media/admin/img/admin/icon_calendar.gif
Symbolic link
1
src/web/media/admin/img/admin/icon_calendar.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_calendar.gif
|
||||||
1
src/web/media/admin/img/admin/icon_changelink.gif
Symbolic link
1
src/web/media/admin/img/admin/icon_changelink.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_changelink.gif
|
||||||
1
src/web/media/admin/img/admin/icon_clock.gif
Symbolic link
1
src/web/media/admin/img/admin/icon_clock.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_clock.gif
|
||||||
1
src/web/media/admin/img/admin/icon_deletelink.gif
Symbolic link
1
src/web/media/admin/img/admin/icon_deletelink.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_deletelink.gif
|
||||||
1
src/web/media/admin/img/admin/icon_error.gif
Symbolic link
1
src/web/media/admin/img/admin/icon_error.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_error.gif
|
||||||
1
src/web/media/admin/img/admin/icon_searchbox.png
Symbolic link
1
src/web/media/admin/img/admin/icon_searchbox.png
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_searchbox.png
|
||||||
1
src/web/media/admin/img/admin/icon_success.gif
Symbolic link
1
src/web/media/admin/img/admin/icon_success.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_success.gif
|
||||||
1
src/web/media/admin/img/admin/inline-delete-8bit.png
Symbolic link
1
src/web/media/admin/img/admin/inline-delete-8bit.png
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/inline-delete-8bit.png
|
||||||
1
src/web/media/admin/img/admin/inline-delete.png
Symbolic link
1
src/web/media/admin/img/admin/inline-delete.png
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/inline-delete.png
|
||||||
1
src/web/media/admin/img/admin/inline-restore-8bit.png
Symbolic link
1
src/web/media/admin/img/admin/inline-restore-8bit.png
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/inline-restore-8bit.png
|
||||||
1
src/web/media/admin/img/admin/inline-restore.png
Symbolic link
1
src/web/media/admin/img/admin/inline-restore.png
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/inline-restore.png
|
||||||
1
src/web/media/admin/img/admin/inline-splitter-bg.gif
Symbolic link
1
src/web/media/admin/img/admin/inline-splitter-bg.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/inline-splitter-bg.gif
|
||||||
1
src/web/media/admin/img/admin/nav-bg-grabber.gif
Symbolic link
1
src/web/media/admin/img/admin/nav-bg-grabber.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/nav-bg-grabber.gif
|
||||||
1
src/web/media/admin/img/admin/nav-bg-reverse.gif
Symbolic link
1
src/web/media/admin/img/admin/nav-bg-reverse.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/nav-bg-reverse.gif
|
||||||
1
src/web/media/admin/img/admin/nav-bg.gif
Symbolic link
1
src/web/media/admin/img/admin/nav-bg.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/nav-bg.gif
|
||||||
1
src/web/media/admin/img/admin/selector-add.gif
Symbolic link
1
src/web/media/admin/img/admin/selector-add.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/selector-add.gif
|
||||||
1
src/web/media/admin/img/admin/selector-addall.gif
Symbolic link
1
src/web/media/admin/img/admin/selector-addall.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/selector-addall.gif
|
||||||
1
src/web/media/admin/img/admin/selector-remove.gif
Symbolic link
1
src/web/media/admin/img/admin/selector-remove.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/selector-remove.gif
|
||||||
1
src/web/media/admin/img/admin/selector-removeall.gif
Symbolic link
1
src/web/media/admin/img/admin/selector-removeall.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/selector-removeall.gif
|
||||||
1
src/web/media/admin/img/admin/selector-search.gif
Symbolic link
1
src/web/media/admin/img/admin/selector-search.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/selector-search.gif
|
||||||
1
src/web/media/admin/img/admin/selector_stacked-add.gif
Symbolic link
1
src/web/media/admin/img/admin/selector_stacked-add.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/selector_stacked-add.gif
|
||||||
1
src/web/media/admin/img/admin/selector_stacked-remove.gif
Symbolic link
1
src/web/media/admin/img/admin/selector_stacked-remove.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/selector_stacked-remove.gif
|
||||||
1
src/web/media/admin/img/admin/tool-left.gif
Symbolic link
1
src/web/media/admin/img/admin/tool-left.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/tool-left.gif
|
||||||
1
src/web/media/admin/img/admin/tool-left_over.gif
Symbolic link
1
src/web/media/admin/img/admin/tool-left_over.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/tool-left_over.gif
|
||||||
1
src/web/media/admin/img/admin/tool-right.gif
Symbolic link
1
src/web/media/admin/img/admin/tool-right.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/tool-right.gif
|
||||||
1
src/web/media/admin/img/admin/tool-right_over.gif
Symbolic link
1
src/web/media/admin/img/admin/tool-right_over.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/tool-right_over.gif
|
||||||
1
src/web/media/admin/img/admin/tooltag-add.gif
Symbolic link
1
src/web/media/admin/img/admin/tooltag-add.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/tooltag-add.gif
|
||||||
1
src/web/media/admin/img/admin/tooltag-add_over.gif
Symbolic link
1
src/web/media/admin/img/admin/tooltag-add_over.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/tooltag-add_over.gif
|
||||||
1
src/web/media/admin/img/admin/tooltag-arrowright.gif
Symbolic link
1
src/web/media/admin/img/admin/tooltag-arrowright.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/tooltag-arrowright.gif
|
||||||
1
src/web/media/admin/img/admin/tooltag-arrowright_over.gif
Symbolic link
1
src/web/media/admin/img/admin/tooltag-arrowright_over.gif
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/admin/tooltag-arrowright_over.gif
|
||||||
1
src/web/media/admin/img/gis/move_vertex_off.png
Symbolic link
1
src/web/media/admin/img/gis/move_vertex_off.png
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/gis/move_vertex_off.png
|
||||||
1
src/web/media/admin/img/gis/move_vertex_on.png
Symbolic link
1
src/web/media/admin/img/gis/move_vertex_on.png
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/img/gis/move_vertex_on.png
|
||||||
1
src/web/media/admin/js/LICENSE-JQUERY.txt
Symbolic link
1
src/web/media/admin/js/LICENSE-JQUERY.txt
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/LICENSE-JQUERY.txt
|
||||||
1
src/web/media/admin/js/SelectBox.js
Symbolic link
1
src/web/media/admin/js/SelectBox.js
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/SelectBox.js
|
||||||
1
src/web/media/admin/js/SelectFilter2.js
Symbolic link
1
src/web/media/admin/js/SelectFilter2.js
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/SelectFilter2.js
|
||||||
0
src/web/media/admin/js/__init__.py
Normal file
0
src/web/media/admin/js/__init__.py
Normal file
1
src/web/media/admin/js/actions.js
Symbolic link
1
src/web/media/admin/js/actions.js
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/actions.js
|
||||||
1
src/web/media/admin/js/actions.min.js
vendored
Symbolic link
1
src/web/media/admin/js/actions.min.js
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/actions.min.js
|
||||||
1
src/web/media/admin/js/admin/DateTimeShortcuts.js
Symbolic link
1
src/web/media/admin/js/admin/DateTimeShortcuts.js
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/admin/DateTimeShortcuts.js
|
||||||
1
src/web/media/admin/js/admin/RelatedObjectLookups.js
Symbolic link
1
src/web/media/admin/js/admin/RelatedObjectLookups.js
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/admin/RelatedObjectLookups.js
|
||||||
1
src/web/media/admin/js/admin/ordering.js
Symbolic link
1
src/web/media/admin/js/admin/ordering.js
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/admin/ordering.js
|
||||||
1
src/web/media/admin/js/calendar.js
Symbolic link
1
src/web/media/admin/js/calendar.js
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/calendar.js
|
||||||
1
src/web/media/admin/js/collapse.js
Symbolic link
1
src/web/media/admin/js/collapse.js
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/collapse.js
|
||||||
1
src/web/media/admin/js/collapse.min.js
vendored
Symbolic link
1
src/web/media/admin/js/collapse.min.js
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/collapse.min.js
|
||||||
1
src/web/media/admin/js/compress.py
Symbolic link
1
src/web/media/admin/js/compress.py
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/compress.py
|
||||||
1
src/web/media/admin/js/core.js
Symbolic link
1
src/web/media/admin/js/core.js
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/core.js
|
||||||
1
src/web/media/admin/js/dateparse.js
Symbolic link
1
src/web/media/admin/js/dateparse.js
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/dateparse.js
|
||||||
1
src/web/media/admin/js/getElementsBySelector.js
Symbolic link
1
src/web/media/admin/js/getElementsBySelector.js
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/getElementsBySelector.js
|
||||||
1
src/web/media/admin/js/inlines.js
Symbolic link
1
src/web/media/admin/js/inlines.js
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/inlines.js
|
||||||
1
src/web/media/admin/js/inlines.min.js
vendored
Symbolic link
1
src/web/media/admin/js/inlines.min.js
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/inlines.min.js
|
||||||
1
src/web/media/admin/js/jquery.init.js
Symbolic link
1
src/web/media/admin/js/jquery.init.js
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/jquery.init.js
|
||||||
1
src/web/media/admin/js/prepopulate.js
Symbolic link
1
src/web/media/admin/js/prepopulate.js
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/usr/share/pyshared/django/contrib/admin/media/js/prepopulate.js
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue