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:
Griatch 2010-12-07 02:34:59 +00:00
parent ecefbfac01
commit 251f94aa7a
118 changed files with 9049 additions and 593 deletions

View file

@ -107,6 +107,15 @@ def cycle_logfile():
os.remove(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):
"""
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 '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:
call([TWISTED_BINARY,
'-n',

View file

@ -318,7 +318,7 @@ class CmdQuit(MuxCommand):
sessions = self.caller.sessions
for session in sessions:
session.msg("Quitting. Hope to see you soon again.")
session.handle_close()
session.at_disconnect()
class CmdWho(MuxCommand):
"""

View file

@ -29,27 +29,20 @@ from src.server import session, sessionhandler
# print all feedback from test commands (can become very verbose!)
VERBOSE = False
class FakeSession(session.SessionProtocol):
class FakeSession(session.Session):
"""
A fake session that
implements dummy versions of the real thing; this is needed to
mimic a logged-in player.
"""
protocol_key = "TestProtocol"
def connectionMade(self):
self.prep_session()
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
self.session_connect('0,0,0,0')
def disconnectClient(self):
pass
def lineReceived(self, raw_string):
pass
def msg(self, message, markup=True):
def msg(self, message, data=None):
if VERBOSE:
print message

View file

@ -5,11 +5,13 @@ import traceback
#from django.contrib.auth.models import User
from django.conf import settings
from django.contrib.auth.models import User
from src.server import sessionhandler
from src.players.models import PlayerDB
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.utils import create, logger, utils
from src.utils import create, logger, utils, ansi
from src.commands.default.muxcommand import MuxCommand
class CmdConnect(MuxCommand):
@ -94,7 +96,7 @@ class CmdConnect(MuxCommand):
player.at_pre_login()
character.at_pre_login()
session.login(player)
session.session_login(player)
player.at_post_login()
character.at_post_login()
@ -230,7 +232,7 @@ class CmdQuit(MuxCommand):
"Simply close the connection."
session = self.caller
session.msg("Good bye! Disconnecting ...")
session.handle_close()
session.at_disconnect()
class CmdUnconnectedLook(MuxCommand):
"""
@ -243,8 +245,11 @@ class CmdUnconnectedLook(MuxCommand):
def func(self):
"Show the connect screen."
try:
self.caller.game_connect_screen()
except Exception:
screen = ConnectScreen.objects.get_random_connect_screen()
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.")
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:
1) If you have no previous account, you need to use the 'create'
command followed by your desired character name (in quotes), your
e-mail address and finally a password of your choice. Like
this:
command 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)
to not use a regular word for your password. Make it longer than
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
one in 1) above, or you are returning, use the 'connect' command
followed by the e-mail and password you previously set.
Example:
one in 1) above or you are returning, use the 'connect' command:
> connect anna@myemail.com tuK3221mP
> connect anna@myemail.com c67jHL8p
This should log you in. Run 'help' again once you're logged in
to get more aid. Hope you enjoy your stay!

View file

@ -3,7 +3,6 @@ These managers handles the
"""
from django.db import models
from src.players.models import PlayerDB
from django.contrib.contenttypes.models import ContentType
from src.utils.utils import is_iter
@ -21,6 +20,7 @@ def to_object(inp, objtype='player'):
inp - the input object/string
objtype - 'player' or 'channel'
"""
from src.players.models import PlayerDB
if objtype == 'player':
if type(inp) == PlayerDB:
return inp

View file

@ -17,8 +17,8 @@ be able to delete connections on the fly).
from django.db import models
from src.utils.idmapper.models import SharedMemoryModel
from src.players.models import PlayerDB
from src.comms import managers
from src.server import sessionhandler
from src.comms import managers
from src.permissions.permissions import has_perm
from src.utils.utils import is_iter
from src.utils.utils import dbref as is_dbref
@ -81,7 +81,7 @@ class Msg(SharedMemoryModel):
permissions - perm strings
"""
from src.players.models import PlayerDB
#
# Msg database model setup
#
@ -509,7 +509,7 @@ class Channel(SharedMemoryModel):
# send message to all connected players
for conn in conns:
for session in \
sessionhandler.find_sessions_from_username(conn.player.name):
sessionhandler.SESSIONS.sessions_from_player(conn.player):
session.msg(msg)
return True
@ -539,6 +539,7 @@ class ChannelConnection(SharedMemoryModel):
The advantage of making it like this is that one can easily
break the connection just by deleting this object.
"""
from src.players.models import PlayerDB
# Player connected to a channel
db_player = models.ForeignKey(PlayerDB)
# Channel the player is connected to

View file

@ -26,7 +26,7 @@ class ConfigValueManager(models.Manager):
new_conf.db_value = db_value
new_conf.save()
def get_configvalue(self, config_key):
def get_configvalue(self, config_key, default=None):
"""
Retrieve a configuration value.
@ -35,7 +35,7 @@ class ConfigValueManager(models.Manager):
try:
return self.get(db_key__iexact=config_key).db_value
except self.model.DoesNotExist:
return None
return default
# a simple wrapper for consistent naming in utils.search
def config_search(self, ostring):
@ -46,7 +46,7 @@ class ConfigValueManager(models.Manager):
"""
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.
This will act as a get/setter, lister or deleter
@ -64,7 +64,7 @@ class ConfigValueManager(models.Manager):
elif db_value != None:
self.set_configvalue(db_key, db_value)
else:
return self.get_configvalue(db_key)
return self.get_configvalue(db_key, default=default)
class ConnectScreenManager(models.Manager):

View file

@ -32,7 +32,6 @@ class HelpEntry(SharedMemoryModel):
permissions - perm strings
"""
#
# HelpEntry Database Model setup

View file

@ -111,10 +111,8 @@ class ObjectDB(TypedObject):
has_player - bool if an active player is currently connected
contents - other objects having this object as location
"""
#
# ObjectDB Database model setup
#
@ -354,8 +352,6 @@ class ObjectDB(TypedObject):
return ObjectDB.objects.get_contents(self)
contents = property(contents_get)
#
# Nicks - custom nicknames
#
@ -379,7 +375,6 @@ class ObjectDB(TypedObject):
# game).
#
def set_nick(self, nick, realname=None):
"""
Map a nick to a realname. Be careful if mapping an
@ -514,33 +509,30 @@ class ObjectDB(TypedObject):
break
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.
message (str): The message to send
from_obj (obj): object that is sending.
markup (bool): Markup. Determines if the message is parsed
for special markup, such as ansi colors. If
false, all markup will be cleaned from the
message in the session.msg() and message
passed on as raw text.
data (object): an optional data object that may or may not
be used by the protocol.
"""
# This is an important function that must always work.
# we use a different __getattribute__ to avoid recursive loops.
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"
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.
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
if exclude:
@ -549,11 +541,11 @@ class ObjectDB(TypedObject):
contents = [obj for obj in contents
if (obj not in exclude and obj not in exclude)]
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"
self.msg_contents(message, exclude)
self.msg_contents(message, exclude=exclude, from_obj=from_obj, data=data)
def move_to(self, destination, quiet=False,
emit_to_obj=None):
@ -736,12 +728,3 @@ class ObjectDB(TypedObject):
# Deferred import to avoid circular import errors.
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)

View file

@ -44,7 +44,7 @@ from django.conf import settings
from django.db import models
from django.contrib.auth.models import User
from django.utils.encoding import smart_str
from src.server import sessionhandler
from src.server import sessionhandler
from src.players import manager
from src.typeclasses.models import Attribute, TypedObject
from src.permissions import permissions
@ -216,11 +216,11 @@ class PlayerDB(TypedObject):
name = property(name_get, name_set, name_del)
key = property(name_get, name_set, name_del)
# sessions property (wraps sessionhandler)
# sessions property
#@property
def sessions_get(self):
"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
def sessions_set(self, value):
"Setter. Protects the sessions property from adding things"
@ -251,26 +251,20 @@ class PlayerDB(TypedObject):
# 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:
try:
from_obj.at_msg_send(message, self)
from_obj.at_msg_send(outgoing_string, self)
except Exception:
pass
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'):
session.msg(message, markup)
def emit_to(self, message, from_obj=None):
"""
Deprecated. Use msg instead.
"""
self.msg(message, from_obj)
session.msg(outgoing_string, data)
def swap_character(self, new_character, delete_old_character=False):
"""

View file

@ -57,7 +57,7 @@ class Player(TypeClass):
them loose.
"""
pass
def at_disconnect(self):
def at_disconnect(self, reason=None):
"""
Called just before user
is disconnected.

View file

@ -26,7 +26,6 @@ Common examples of uses of Scripts:
"""
from django.conf import settings
from django.db import models
from src.objects.models import ObjectDB
from src.typeclasses.models import Attribute, TypedObject
from src.scripts.manager import ScriptManager
@ -91,7 +90,7 @@ class ScriptDB(TypedObject):
# optional description.
db_desc = models.CharField(max_length=255, blank=True)
# 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
db_interval = models.IntegerField(default=-1)
# start script right away or wait interval seconds first

View file

@ -234,7 +234,7 @@ class CheckSessions(Script):
"called every 60 seconds"
#print "session check!"
#print "ValidateSessions run"
sessionhandler.check_all_sessions()
sessionhandler.validate_sessions()
class ValidateScripts(Script):
"Check script validation regularly"

View file

@ -9,15 +9,9 @@ Everything starts at handle_setup()
from django.contrib.auth.models import User
from django.core import management
from django.conf import settings
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.scripts import scripts
from src.utils import create
from src.utils import gametime
def create_config_values():
"""
@ -34,14 +28,14 @@ def create_connect_screens():
print " Creating startup screen(s) ..."
text = "%ch%cb==================================================================%cn"
text += "\r\n Welcome to %chEvennia%cn! Please type one of the following to begin:\r\n"
text = "{b=================================================================={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 += "%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 += "%chcreate \"<username>\" <email> <password>%cn\r\n"
text += "\r\n Enter %chhelp%cn for more info. %chlook%cn will re-show this screen.\r\n"
text += "%ch%cb==================================================================%cn\r\n"
text += "{wcreate \"<username>\" <email> <password>{n\r\n"
text += "\r\n Enter {whelp{n for more info. {wlook{n will re-show this screen.\r\n"
text += "{b=================================================================={n\r\n"
ConnectScreen(db_key="Default", db_text=text, db_is_active=True).save()
def get_god_user():
@ -116,6 +110,7 @@ def create_channels():
# connect the god user to all these channels by default.
goduser = get_god_user()
from src.comms.models import ChannelConnection
ChannelConnection.objects.create_connection(goduser, pchan)
ChannelConnection.objects.create_connection(goduser, ichan)
ChannelConnection.objects.create_connection(goduser, cchan)
@ -164,9 +159,10 @@ def create_system_scripts():
Setup the system repeat scripts. They are automatically started
by the create_script function.
"""
from src.scripts import scripts
print " Creating and starting global scripts ..."
# check so that all sessions are alive.
script1 = create.create_script(scripts.CheckSessions)
# validate all scripts in script table.
@ -184,7 +180,7 @@ def start_game_time():
(the uptime can also be found directly from the server though).
"""
print " Starting in-game time ..."
from src.utils import gametime
gametime.init_gametime()
def handle_setup(last_step):
@ -230,11 +226,16 @@ def handle_setup(last_step):
setup_func()
except Exception:
if last_step + num == 2:
from src.players.models import PlayerDB
from src.objects.models import ObjectDB
for obj in ObjectDB.objects.all():
obj.delete()
for profile in PlayerDB.objects.all():
profile.delete()
elif last_step + num == 3:
from src.comms.models import Channel, ChannelConnection
for chan in Channel.objects.all():
chan.delete()
for conn in ChannelConnection.objects.all():

View file

@ -1,6 +1,11 @@
"""
This module implements the main Evennia
server process, the core of the game engine.
This module implements the main Evennia server process, the core of
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 sys
@ -12,75 +17,82 @@ if os.name == 'nt':
from twisted.application import internet, service
from twisted.internet import protocol, reactor
from twisted.web import server, static
from django.db import connection
from django.conf import settings
from src.utils import reloads
from src.config.models import ConfigValue
from src.server.session import SessionProtocol
from src.server import sessionhandler
from src.server import initial_setup
from src.utils import reloads
from src.utils.utils import get_evennia_version
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):
# Holds the TCP services.
self.service_collection = None
self.game_running = True
def __init__(self, application):
"""
Setup the server.
application - an instantiated Twisted application
"""
sys.path.append('.')
# create a store of services
self.services = service.IServiceCollection(application)
print '\n' + '-'*50
# Database-specific startup optimizations.
if (settings.DATABASE_ENGINE == "sqlite3"
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
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:
# 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
self.sqlite3_prep()
# Run the initial setup if needed
self.run_initial_setup()
# we have to null this here.
sessionhandler.change_session_count(0)
sessionhandler.SESSIONS.session_count(0)
self.start_time = time.time()
# initialize channelhandler
channelhandler.CHANNELHANDLER.update()
# init all global scripts
reloads.reload_scripts(init_mode=True)
# Make output to the terminal.
print ' %s (%s) started on port(s):' % \
(settings.SERVERNAME, get_evennia_version())
for port in settings.GAMEPORTS:
print ' * %s' % (port)
print '-'*50
# Make info output to the terminal.
self.terminal_output()
print '-'*50
self.game_running = True
# Server startup methods
@ -89,14 +101,50 @@ class EvenniaService(service.Service):
Optimize some SQLite stuff at startup since we
can't save it to the database.
"""
cursor = connection.cursor()
cursor.execute("PRAGMA cache_size=10000")
cursor.execute("PRAGMA synchronous=OFF")
cursor.execute("PRAGMA count_changes=OFF")
cursor.execute("PRAGMA temp_store=2")
if (settings.DATABASE_ENGINE == "sqlite3"
or hasattr(settings, 'DATABASE')
and settings.DATABASE.get('ENGINE', None)
== 'django.db.backends.sqlite3'):
cursor = connection.cursor()
cursor.execute("PRAGMA cache_size=10000")
cursor.execute("PRAGMA synchronous=OFF")
cursor.execute("PRAGMA count_changes=OFF")
cursor.execute("PRAGMA temp_store=2")
# General methods
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
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):
"""
@ -104,52 +152,88 @@ class EvenniaService(service.Service):
"""
if not message:
message = 'The server has been shutdown. Please check back soon.'
sessionhandler.announce_all(message)
sessionhandler.disconnect_all_sessions()
sessionhandler.SESSIONS.disconnect_all_sessions(reason=message)
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 = SessionProtocol
factory.server = self
return factory
factory.protocol = telnet.TelnetProtocol
telnet_service = internet.TCPServer(port, factory)
telnet_service.setName('Evennia%s' % port)
EVENNIA.services.addService(telnet_service)
def start_services(self, application):
"""
Starts all of the TCP services.
"""
self.service_collection = service.IServiceCollection(application)
for port in settings.GAMEPORTS:
evennia_server = \
internet.TCPServer(port, self.getEvenniaServiceFactory())
evennia_server.setName('Evennia%s' %port)
evennia_server.setServiceParent(self.service_collection)
if settings.IMC2_ENABLED:
from src.imc2.connection import IMC2ClientFactory
from src.imc2 import events as imc2_events
imc2_factory = IMC2ClientFactory()
svc = internet.TCPClient(settings.IMC2_SERVER_ADDRESS,
settings.IMC2_SERVER_PORT,
imc2_factory)
svc.setName('IMC2')
svc.setServiceParent(self.service_collection)
imc2_events.add_events()
if WEBSERVER_ENABLED:
if settings.IRC_ENABLED:
from src.irc.connection import IRC_BotFactory
irc = internet.TCPClient(settings.IRC_NETWORK,
settings.IRC_PORT,
IRC_BotFactory(settings.IRC_CHANNEL,
settings.IRC_NETWORK,
settings.IRC_NICKNAME))
irc.setName("%s:%s" % ("IRC", settings.IRC_CHANNEL))
irc.setServiceParent(self.service_collection)
# a django-compatible webserver.
from src.server.webserver import DjangoWebRoot
# define the root url (/) as a wsgi resource recognized by Django
web_root = DjangoWebRoot()
# point our media resources to url /media
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)
# 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)
if IMC2_ENABLED:
# IMC2 channel connections
from src.imc2.connection import IMC2ClientFactory
from src.imc2 import events as imc2_events
imc2_factory = IMC2ClientFactory()
svc = internet.TCPClient(settings.IMC2_SERVER_ADDRESS,
settings.IMC2_SERVER_PORT,
imc2_factory)
svc.setName('IMC2')
EVENNIA.services.addService(svc)
imc2_events.add_events()
if IRC_ENABLED:
# IRC channel connections
from src.irc.connection import IRC_BotFactory
irc = internet.TCPClient(settings.IRC_NETWORK,
settings.IRC_PORT,
IRC_BotFactory(settings.IRC_CHANNEL,
settings.IRC_NETWORK,
settings.IRC_NICKNAME))
irc.setName("%s:%s" % ("IRC", settings.IRC_CHANNEL))
EVENNIA.services.addService(irc)

View file

@ -1,214 +1,183 @@
"""
This module contains classes related to Sessions. sessionhandler has the things
needed to manage them.
This defines a generic session class.
All protocols should implement this class and its hook methods.
"""
import time
import time
from datetime import datetime
from twisted.conch.telnet import StatefulTelnetProtocol
#from django.contrib.auth.models import User
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.config.models import ConnectScreen
from src.utils import logger, reloads
from src.commands import cmdhandler
from src.utils import ansi
from src.utils import reloads
from src.utils import logger
from src.utils import utils
from src.server import sessionhandler
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
gets a session assigned to them whenever
they connect to the game server. All communication
between game and player goes through here.
A simple storage object that allows for storing
new attributes on it at creation.
"""
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):
"""
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,)
# use this to uniquely identify the protocol name, e.g. "telnet" or "comet"
protocol_key = "BaseProtocol"
def connectionMade(self):
def session_connect(self, address, suid=None):
"""
What to do when we get a connection.
"""
# 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()
The setup of the session. An address (usually an IP address) on any form is required.
def getClientAddress(self):
"""
Returns the client's address and port in a tuple. For example
('127.0.0.1', 41917)
"""
return self.transport.client
This should be called by the protocol at connection time.
def prep_session(self):
suid = this is a session id. Needed by some transport protocols.
"""
This sets up the main parameters of
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()
self.address = address
# player setup
# user setup
self.name = None
self.uid = None
self.suid = suid
self.logged_in = False
self.encoding = "utf-8"
current_time = time.time()
# 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.
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.
self.cmd_total = 0
# The time when the user connected.
self.conn_time = time.time()
#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.
"""
self.transport.loseConnection()
Private startup mechanisms that need to run at login
def connectionLost(self, reason):
player - the connected player
"""
Execute this when a client abruplty loses their connection.
"""
logger.log_infomsg('Disconnected: %s' % self)
self.cemit_info('Disconnected: %s.' % self)
self.handle_close()
self.player = player
self.user = player.user
self.uid = self.user.id
self.name = self.user.username
self.logged_in = True
self.conn_time = time.time()
def lineReceived(self, raw_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 to the Player and their currently
connected character.
"""
# Update account's last login time.
self.user.last_login = datetime.now()
self.user.save()
self.log('Logged in: %s' % self)
if self.encoding:
try:
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
# start (persistent) scripts on this object
reloads.reload_scripts(obj=self.player.character, init_mode=True)
#add session to connected list
sessionhandler.SESSIONS.add_loggedin_session(self)
# malformed/wrong encoding defined on player-try some defaults
for encoding in ENCODINGS:
try:
raw_string = utils.to_unicode(raw_string, encoding=encoding)
err = None
break
except Exception, e:
err = str(e)
continue
if err:
self.sendLine(err)
#call hook
self.at_login()
def session_disconnect(self, reason=None):
"""
Clean up the session, removing it from the game and doing some
accounting. This method is used also for non-loggedin
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:
self.execute_cmd(raw_string)
def msg(self, message, markup=True):
"""
Communication Evennia -> Player
Sends a message to the session.
markup - determines if formatting markup should be
parsed or not. Currently this means ANSI
colors, but could also be html tags for
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))
return None
# if self.logged_in:
# character = ObjectDB.objects.get_object_with_user(self.uid)
# if not character:
# string = "No player match for session uid: %s" % self.uid
# logger.log_errmsg(string)
# return None
# return character.player
# return None
def get_character(self):
"""
Returns the in-game character associated with a session.
This returns the typeclass of the object.
"""
if self.logged_in:
character = ObjectDB.objects.get_object_with_user(self.uid)
if not character:
string = "No character match for session uid: %s" % self.uid
logger.log_errmsg(string)
else:
return character
player = self.get_player()
if player:
return player.character
return None
def execute_cmd(self, raw_string):
def log(self, message, channel=True):
"""
Sends a command to this session's
character for processing.
Emits session info to the appropriate outputs and info channels.
"""
if channel:
try:
cchan = settings.CHANNEL_CONNECTINFO
cchan = Channel.objects.get_channel(cchan[0])
cchan.msg("[%s]: %s" % (cchan.key, message))
except Exception:
pass
logger.log_infomsg(message)
'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 str(raw_string).strip() == 'idle':
self.update_counters(idle=True)
return
# all other inputs, including empty inputs
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):
def update_session_counters(self, idle=False):
"""
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
is not updated.
and command counters.
"""
# Store the timestamp of the user's last command.
self.cmd_last = time.time()
@ -217,80 +186,125 @@ class SessionProtocol(StatefulTelnetProtocol):
self.cmd_total += 1
# Player-visible idle time, not used in idle timeout calcs.
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()
if character:
#call hook functions
character.at_disconnect()
character.player.at_disconnect()
uaccount = character.player.user
uaccount.last_login = datetime.now()
uaccount.save()
self.disconnectClient()
self.logged_in = False
sessionhandler.remove_session(self)
def game_connect_screen(self):
#print "loggedin _execute_cmd: '%s' __ %s" % (command_string, character)
# normal operation.
character.execute_cmd(command_string)
else:
#print "unloggedin _execute_cmd: '%s' __ %s" % (command_string, character)
# we are not logged in yet; call cmdhandler directly
cmdhandler.cmdhandler(self, command_string, unloggedin=True)
def get_data_obj(self, **kwargs):
"""
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.
Create a data object, storing keyword arguments on itself as arguments.
"""
screen = ConnectScreen.objects.get_random_connect_screen()
string = ansi.parse_ansi(screen.text)
self.msg(string)
return IOdata(**kwargs)
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):
"""
After the user has authenticated, this actually
logs them in. At this point the session has
a User account tied to it. User is an django
object that handles stuff like permissions and
access, it has no visible precense in the game.
This User object is in turn tied to a game
Object, which represents whatever existence
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)
"alias for at_login"
self.at_login(player)
def logout(self):
"alias for at_logout"
self.at_disconnect()
def msg(self, string='', data=None):
"alias for at_data_out"
self.at_data_out(string, data)

View file

@ -1,137 +1,184 @@
"""
Sessionhandler, stores and handles
a list of all player connections (sessions).
This module handles sessions of users connecting
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 src.config.models import ConfigValue
from src.utils import logger
# Our list of connected sessions.
SESSIONS = []
ALLOW_MULTISESSION = settings.ALLOW_MULTISESSION
def add_session(session):
"""
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):
"""
Lists the connected session objects.
"""
if return_unlogged:
return SESSIONS
else:
return [sess for sess in SESSIONS if sess.logged_in]
def get_session_id_list(return_unlogged=False):
"""
Lists the connected session object ids.
"""
if return_unlogged:
return SESSIONS
else:
return [sess.uid for sess in SESSIONS if sess.logged_in]
#------------------------------------------------------------
# SessionHandler class
#------------------------------------------------------------
def disconnect_all_sessions():
class SessionHandler(object):
"""
Cleanly disconnect all of the connected sessions.
"""
for sess in get_sessions():
sess.handle_close()
This object holds the stack of sessions active in the game at
any time.
def disconnect_duplicate_session(session):
"""
Disconnects any existing session under the same object. This is used in
connection recovery to help with record-keeping.
"""
SESSIONS = get_sessions()
session_pobj = session.get_character()
for other_session in SESSIONS:
other_pobject = other_session.get_character()
if session_pobj == other_pobject and other_session != session:
other_session.msg("Your account has been logged in from elsewhere, disconnecting.")
other_session.disconnectClient()
return True
return False
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.
def check_all_sessions():
"""
Check all currently connected sessions and see if any are dead.
"""
idle_timeout = int(ConfigValue.objects.conf('idle_timeout'))
if len(SESSIONS) <= 0:
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
num can be a positive or negative value. If 0, the counter
will be reset to 0.
"""
if num == 0:
# reset
ConfigValue.objects.conf('nr_sessions', 0)
nr = ConfigValue.objects.conf('nr_sessions')
if nr == None:
nr = 0
else:
nr = int(nr)
nr += num
ConfigValue.objects.conf('nr_sessions', str(nr))
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 remove_session(session):
"""
Removes a session from the session list.
"""
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.
"""
try:
uobj = User.objects.get(username=username)
uid = uobj.id
return [session for session in SESSIONS if session.uid == uid]
except User.DoesNotExist:
return None
def sessions_from_object(targ_object):
"""
Returns a list of matching session objects, or None if there are no matches.
targobject: (Object) The object to match.
"""
return [session for session in SESSIONS
if session.get_character() == targ_object]
def add_unloggedin_session(self, session):
"""
Call at first connect. This adds a not-yet authenticated session.
"""
self.unloggedin.insert(0, session)
def announce_all(message):
"""
Announces something to all connected players.
"""
for session in get_sessions():
session.msg('%s' % message)
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:
return self.loggedin
def disconnect_all_sessions(self, reason=None):
"""
Cleanly disconnect all of the connected sessions.
"""
sessions = self.get_sessions(include_unloggedin=True)
for session in sessions:
session.session_disconnect(reason)
self.session_count(0)
def disconnect_duplicate_sessions(self, session):
"""
Disconnects any existing sessions with the same game object. This is used in
connection recovery to help with record-keeping.
"""
reason = "Your account has been logged in from elsewhere. Disconnecting."
sessions = self.get_sessions()
session_character = self.get_character(session)
logged_out = 0
for other_session in sessions:
other_character = self.get_character(other_session)
if session_character == other_character and other_session != session:
self.remove_session(other_session, reason=reason)
logged_out += 1
self.session_count(-logged_out)
return logged_out
def validate_sessions(self):
"""
Check all currently connected sessions (logged in and not)
and see if any are dead.
"""
for session in self.get_sessions(include_unloggedin=True):
session.session_validate()
def session_count(self, num=None):
"""
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 to be added to the current count.
If 0, the counter will be reset to 0.
"""
if num == None:
# show the current value. This also syncs it.
return int(ConfigValue.objects.conf('nr_sessions', default=0))
elif num == 0:
# reset value to 0
ConfigValue.objects.conf('nr_sessions', 0)
else:
# add/remove session count from value
add = int(ConfigValue.objects.conf('nr_sessions', default=0))
num = max(0, num + add)
ConfigValue.objects.conf('nr_sessions', str(num))
def sessions_from_player(self, player):
"""
Given a player, return any matching sessions.
"""
username = player.user.username
try:
uobj = User.objects.get(username=username)
except User.DoesNotExist:
return None
uid = uobj.id
return [session for session in self.loggedin if session.uid == uid]
def sessions_from_character(self, character):
"""
Given a game character, return any matching sessions.
"""
player = character.player
if player:
return self.sessions_from_player(player)
return None
def session_from_suid(self, suid):
"""
Given a session id, retrieve the session (this is primarily
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()

153
src/server/telnet.py Normal file
View 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
View 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
View 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

View file

@ -22,8 +22,21 @@ import os
# This is the name of your server and/or site.
# Can be anything.
SERVERNAME = "Evennia"
# A list of ports the Evennia server listens on. Can be one or many.
GAMEPORTS = [4000]
# Activate telnet service
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
# 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'.
@ -54,6 +67,9 @@ GAME_DIR = os.path.join(BASE_PATH, 'game')
# Place to put log files
LOG_DIR = os.path.join(GAME_DIR, 'logs')
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:
# http://www.postgresql.org/docs/8.0/interactive/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
TIME_ZONE = 'UTC'
@ -70,12 +86,18 @@ IMPORT_MUX_HELP = False
# thrown off by sending the empty system command 'idle' to the server
# at regular intervals. Set <=0 to deactivate idle timout completely.
IDLE_TIMEOUT = 3600
# If the PlayerAttribute 'encoding' is not set, or wrong encoding is
# given, this list is tried, in order, stopping on the first match.
# Add sets for languages/regions your players are likely to use. (see
# http://en.wikipedia.org/wiki/Character_encoding).
# The idle command can be sent to keep your session active without actually
# having to spam normal commands regularly. It gives no feedback, only updates
# the idle timer.
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"]
###################################################
# Evennia Database config
###################################################
@ -314,8 +336,8 @@ IRC_NICKNAME = ""
# While DEBUG is False, show a regular server error page on the web
# stuff, email the traceback to the people in the ADMINS tuple
# below. If True, show a detailed traceback for the web
# browser to display. Note however that this might leak memory when
# active, so make sure turn it off for a production server!
# browser to display. Note however that this will leak memory when
# active, so make sure to turn it off for a production server!
DEBUG = False
# While true, show "pretty" error messages for template syntax errors.
TEMPLATE_DEBUG = DEBUG
@ -350,7 +372,7 @@ USE_I18N = False
# 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,
# lighttpd).
SERVE_MEDIA = True
SERVE_MEDIA = False
# The master urlconf file that contains all of the sub-branches to the
# applications.
@ -367,7 +389,7 @@ MEDIA_URL = '/media/'
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
# trailing slash.
# 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
# directory names shown in the webtemplates directory.
ACTIVE_TEMPLATE = 'prosimii'

View file

@ -72,7 +72,7 @@ class ANSIParser(object):
# MUX-style mappings %cr %cn etc
mux_ansi_map = [
self.mux_ansi_map = [
(r'%r', ANSITable.ansi["return"]),
(r'%t', ANSITable.ansi["tab"]),
(r'%b', ANSITable.ansi["space"]),
@ -102,7 +102,7 @@ class ANSIParser(object):
hilite = ANSITable.ansi['hilite']
normal = ANSITable.ansi['normal']
ext_ansi_map = [
self.ext_ansi_map = [
(r'{r', hilite + ANSITable.ansi['red']),
(r'{R', normal + ANSITable.ansi['red']),
(r'{g', hilite + ANSITable.ansi['green']),
@ -121,8 +121,8 @@ class ANSIParser(object):
(r'{X', normal + ANSITable.ansi['black']), #pure black
(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
self.ansi_sub = [(re.compile(sub[0], re.DOTALL), sub[1])
@ -141,13 +141,15 @@ class ANSIParser(object):
if not string:
return ''
string = str(string)
for sub in self.ansi_sub:
for sub in self.ansi_sub:
# go through all available mappings and translate them
string = sub[0].sub(sub[1], string)
if strip_ansi:
# remove all ANSI escape codes
string = self.ansi_regex.sub("", string)
return string
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)

137
src/utils/ansi2html.py Normal file
View 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', '&nbsp;'*self.tabstop)
t = t.replace(' ', '&nbsp;')
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)

View file

@ -22,11 +22,8 @@ Models covered:
from django.conf import settings
from django.contrib.auth.models import User
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.models import PermissionGroup
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
and so on.
"""
from src.help.models import HelpEntry
try:
new_help = HelpEntry()
new_help.key = key
@ -293,7 +292,9 @@ def create_message(senderobj, message, channels=None,
at the same time, it's up to the command definitions to limit this as
desired.
"""
from src.comms.models import Msg
from src.comms.managers import to_object
def to_player(obj):
"Make sure the object is a player object"
if hasattr(obj, 'user'):
@ -345,6 +346,9 @@ def create_channel(key, aliases=None, description=None,
listen/send/admin permissions are strings if permissions separated
by commas.
"""
from src.comms.models import Channel
from src.comms import channelhandler
try:
new_channel = Channel()
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
# getting here.
from src.players.models import PlayerDB
if user:
new_user = user
else:

View file

@ -18,14 +18,16 @@ def is_iter(iterable):
they are actually iterable), since string iterations
are usually not what we want to do with a string.
"""
if isinstance(iterable, basestring):
# skip all forms of strings (str, unicode etc)
return False
try:
# check if object implements iter protocol
return iter(iterable)
except TypeError:
return False
return hasattr(iterable, '__iter__')
# if isinstance(iterable, basestring):
# # skip all forms of strings (str, unicode etc)
# return False
# try:
# # check if object implements iter protocol
# return iter(iterable)
# except TypeError:
# return False
def fill(text, width=78):
"""

View file

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/css/base.css

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/css/changelists.css

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/css/dashboard.css

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/css/forms.css

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/css/ie.css

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/css/login.css

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/css/rtl.css

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/css/widgets.css

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/arrow-down.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/arrow-up.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/changelist-bg.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/changelist-bg_rtl.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/chooser-bg.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/chooser_stacked-bg.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/default-bg-reverse.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/default-bg.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/deleted-overlay.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon-no.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon-unknown.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon-yes.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_addlink.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_alert.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_calendar.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_changelink.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_clock.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_deletelink.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_error.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_searchbox.png

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/icon_success.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/inline-delete-8bit.png

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/inline-delete.png

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/inline-restore-8bit.png

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/inline-restore.png

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/inline-splitter-bg.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/nav-bg-grabber.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/nav-bg-reverse.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/nav-bg.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/selector-add.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/selector-addall.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/selector-remove.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/selector-removeall.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/selector-search.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/selector_stacked-add.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/selector_stacked-remove.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/tool-left.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/tool-left_over.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/tool-right.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/tool-right_over.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/tooltag-add.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/tooltag-add_over.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/tooltag-arrowright.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/admin/tooltag-arrowright_over.gif

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/gis/move_vertex_off.png

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/img/gis/move_vertex_on.png

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/js/LICENSE-JQUERY.txt

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/js/SelectBox.js

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/js/SelectFilter2.js

View file

View file

@ -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
View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/js/actions.min.js

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/js/admin/DateTimeShortcuts.js

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/js/admin/RelatedObjectLookups.js

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/js/admin/ordering.js

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/js/calendar.js

View file

@ -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
View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/js/collapse.min.js

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/js/compress.py

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/js/core.js

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/js/dateparse.js

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/js/getElementsBySelector.js

View file

@ -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
View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/js/inlines.min.js

View file

@ -0,0 +1 @@
/usr/share/pyshared/django/contrib/admin/media/js/jquery.init.js

View file

@ -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