Reshuffling the Evennia package into the new template paradigm.

This commit is contained in:
Griatch 2015-01-06 14:53:45 +01:00
parent 2846e64833
commit 2b3a32e447
371 changed files with 17250 additions and 304 deletions

14
lib/comms/__init__.py Normal file
View file

@ -0,0 +1,14 @@
"""
Makes it easier to import by grouping all relevant things already at this
level.
You can henceforth import most things directly from src.comms
Also, the initiated object manager is available as src.comms.msgmanager and
src.comms.channelmanager.
"""
#from src.comms.models import *
#msgmanager = Msg.objects
#channelmanager = ChannelDB.objects

49
lib/comms/admin.py Normal file
View file

@ -0,0 +1,49 @@
#
# This sets up how models are displayed
# in the web admin interface.
#
from django.contrib import admin
from src.comms.models import ChannelDB
from src.typeclasses.admin import AttributeInline, TagInline
class ChannelAttributeInline(AttributeInline):
model = ChannelDB.db_attributes.through
class ChannelTagInline(TagInline):
model = ChannelDB.db_tags.through
class MsgAdmin(admin.ModelAdmin):
list_display = ('id', 'db_date_sent', 'db_sender', 'db_receivers',
'db_channels', 'db_message', 'db_lock_storage')
list_display_links = ("id",)
ordering = ["db_date_sent", 'db_sender', 'db_receivers', 'db_channels']
#readonly_fields = ['db_message', 'db_sender', 'db_receivers', 'db_channels']
search_fields = ['id', '^db_date_sent', '^db_message']
save_as = True
save_on_top = True
list_select_related = True
#admin.site.register(Msg, MsgAdmin)
class ChannelAdmin(admin.ModelAdmin):
inlines = [ChannelTagInline, ChannelAttributeInline]
list_display = ('id', 'db_key', 'db_lock_storage', "subscriptions")
list_display_links = ("id", 'db_key')
ordering = ["db_key"]
search_fields = ['id', 'db_key', 'db_aliases']
save_as = True
save_on_top = True
list_select_related = True
fieldsets = (
(None, {'fields': (('db_key',), 'db_lock_storage', 'db_subscriptions')}),
)
def subscriptions(self, obj):
"Helper method to get subs from a channel"
return ", ".join([str(sub) for sub in obj.db_subscriptions.all()])
admin.site.register(ChannelDB, ChannelAdmin)

161
lib/comms/channelhandler.py Normal file
View file

@ -0,0 +1,161 @@
"""
The channel handler handles the stored set of channels
and how they are represented against the cmdhandler.
If there is a channel named 'newbie', we want to be able
to just write
> newbie Hello!
For this to work, 'newbie', the name of the channel, must
be identified by the cmdhandler as a command name. The
channelhandler stores all channels as custom 'commands'
that the cmdhandler can import and look through.
Warning - channel names take precedence over command names,
so make sure to not pick clashing channel names.
Unless deleting a channel you normally don't need to bother about
the channelhandler at all - the create_channel method handles the update.
To delete a channel cleanly, delete the channel object, then call
update() on the channelhandler. Or use Channel.objects.delete() which
does this for you.
"""
from src.comms.models import ChannelDB
from src.commands import cmdset, command
class ChannelCommand(command.Command):
"""
Channel
Usage:
<channel name or alias> <message>
This is a channel. If you have subscribed to it, you can send to
it by entering its name or alias, followed by the text you want to
send.
"""
# this flag is what identifies this cmd as a channel cmd
# and branches off to the system send-to-channel command
# (which is customizable by admin)
is_channel = True
key = "general"
help_category = "Channel Names"
obj = None
def parse(self):
"""
Simple parser
"""
# cmdhandler sends channame:msg here.
channelname, msg = self.args.split(":", 1)
self.args = (channelname.strip(), msg.strip())
def func(self):
"""
Create a new message and send it to channel, using
the already formatted input.
"""
channelkey, msg = self.args
caller = self.caller
if not msg:
self.msg("Say what?")
return
channel = ChannelDB.objects.get_channel(channelkey)
if not channel:
self.msg("Channel '%s' not found." % channelkey)
return
if not channel.has_connection(caller):
string = "You are not connected to channel '%s'."
self.msg(string % channelkey)
return
if not channel.access(caller, 'send'):
string = "You are not permitted to send to channel '%s'."
self.msg(string % channelkey)
return
channel.msg(msg, senders=self.caller, online=True)
class ChannelHandler(object):
"""
Handles the set of commands related to channels.
"""
def __init__(self):
self.cached_channel_cmds = []
self.cached_cmdsets = {}
def __str__(self):
return ", ".join(str(cmd) for cmd in self.cached_channel_cmds)
def clear(self):
"""
Reset the cache storage.
"""
self.cached_channel_cmds = []
def _format_help(self, channel):
"builds a doc string"
key = channel.key
aliases = channel.aliases.all()
ustring = "%s <message>" % key.lower() + "".join(["\n %s <message>" % alias.lower() for alias in aliases])
desc = channel.db.desc
string = \
"""
Channel '%s'
Usage (not including your personal aliases):
%s
%s
""" % (key, ustring, desc)
return string
def add_channel(self, channel):
"""
Add an individual channel to the handler. This should be
called whenever a new channel is created. To
remove a channel, simply delete the channel object
and run self.update on the handler.
"""
# map the channel to a searchable command
cmd = ChannelCommand(key=channel.key.strip().lower(),
aliases=channel.aliases.all(),
locks="cmd:all();%s" % channel.locks,
help_category="Channel names",
obj=channel,
arg_regex=r"\s.*?",
is_channel=True)
self.cached_channel_cmds.append(cmd)
self.cached_cmdsets = {}
def update(self):
"Updates the handler completely."
self.cached_channel_cmds = []
self.cached_cmdsets = {}
for channel in ChannelDB.objects.get_all_channels():
self.add_channel(channel)
def get_cmdset(self, source_object):
"""
Retrieve cmdset for channels this source_object has
access to send to.
"""
if source_object in self.cached_cmdsets:
return self.cached_cmdsets[source_object]
else:
# create a new cmdset holding all channels
chan_cmdset = cmdset.CmdSet()
chan_cmdset.key = '_channelset'
chan_cmdset.priority = 120
chan_cmdset.duplicates = True
for cmd in [cmd for cmd in self.cached_channel_cmds
if cmd.access(source_object, 'send')]:
chan_cmdset.add(cmd)
self.cached_cmdsets[source_object] = chan_cmdset
return chan_cmdset
CHANNELHANDLER = ChannelHandler()

330
lib/comms/comms.py Normal file
View file

@ -0,0 +1,330 @@
"""
Default Typeclass for Comms.
See objects.objects for more information on Typeclassing.
"""
from src.typeclasses.models import TypeclassBase
from src.comms.models import Msg, TempMsg, ChannelDB
from src.comms.managers import ChannelManager
from src.utils import logger
from src.utils.utils import make_iter
class Channel(ChannelDB):
"""
This is the base class for all Comms. Inherit from this to create different
types of communication channels.
"""
__metaclass__ = TypeclassBase
objects = ChannelManager()
def at_first_save(self):
"""
Called by the typeclass system the very first time the channel
is saved to the database. Generally, don't overload this but
the hooks called by this method.
"""
self.at_channel_creation()
if hasattr(self, "_createdict"):
# this is only set if the channel was created
# with the utils.create.create_channel function.
cdict = self._createdict
if not cdict.get("key"):
if not self.db_key:
self.db_key = "#i" % self.dbid
elif cdict["key"] and self.key != cdict["key"]:
self.key = cdict["key"]
if cdict.get("keep_log"):
self.db_keep_log = cdict["keep_log"]
if cdict.get("aliases"):
self.aliases.add(cdict["aliases"])
if cdict.get("locks"):
self.locks.add(cdict["locks"])
if cdict.get("keep_log"):
self.attributes.add("keep_log", cdict["keep_log"])
if cdict.get("desc"):
self.attributes.add("desc", cdict["desc"])
def at_channel_creation(self):
"""
Called once, when the channel is first created.
"""
pass
# helper methods, for easy overloading
def has_connection(self, player):
"""
Checks so this player is actually listening
to this channel.
"""
if hasattr(player, "player"):
player = player.player
return player in self.db_subscriptions.all()
def connect(self, player):
"Connect the user to this channel. This checks access."
if hasattr(player, "player"):
player = player.player
# check access
if not self.access(player, 'listen'):
return False
# pre-join hook
connect = self.pre_join_channel(player)
if not connect:
return False
# subscribe
self.db_subscriptions.add(player)
# post-join hook
self.post_join_channel(player)
return True
def disconnect(self, player):
"Disconnect user from this channel."
if hasattr(player, "player"):
player = player.player
# pre-disconnect hook
disconnect = self.pre_leave_channel(player)
if not disconnect:
return False
# disconnect
self.db_subscriptions.remove(player)
# post-disconnect hook
self.post_leave_channel(player)
return True
def access(self, accessing_obj, access_type='listen', default=False):
"""
Determines if another object has permission to access.
accessing_obj - object trying to access this one
access_type - type of access sought
default - what to return if no lock of access_type was found
"""
return self.locks.check(accessing_obj, access_type=access_type, default=default)
def delete(self):
"""
Deletes channel while also cleaning up channelhandler
"""
self.attributes.clear()
self.aliases.clear()
super(Channel, self).delete()
from src.comms.channelhandler import CHANNELHANDLER
CHANNELHANDLER.update()
def channel_prefix(self, msg=None, emit=False):
"""
How the channel should prefix itself for users. Return a string.
"""
return '[%s] ' % self.key
def format_senders(self, senders=None):
"""
Function used to format a list of sender names.
This function exists separately so that external sources can use
it to format source names in the same manner as normal object/player
names.
"""
if not senders:
return ''
return ', '.join(senders)
def pose_transform(self, msg, sender_string):
"""
Detects if the sender is posing, and modifies the message accordingly.
"""
pose = False
message = msg.message
message_start = message.lstrip()
if message_start.startswith((':', ';')):
pose = True
message = message[1:]
if not message.startswith((':', "'", ',')):
if not message.startswith(' '):
message = ' ' + message
if pose:
return '%s%s' % (sender_string, message)
else:
return '%s: %s' % (sender_string, message)
def format_external(self, msg, senders, emit=False):
"""
Used for formatting external messages. This is needed as a separate
operation because the senders of external messages may not be in-game
objects/players, and so cannot have things like custom user
preferences.
senders should be a list of strings, each containing a sender.
msg should contain the body of the message to be sent.
"""
if not senders:
emit = True
if emit:
return msg.message
senders = ', '.join(senders)
return self.pose_transform(msg, senders)
def format_message(self, msg, emit=False):
"""
Formats a message body for display.
If emit is True, it means the message is intended to be posted detached
from an identity.
"""
# We don't want to count things like external sources as senders for
# the purpose of constructing the message string.
senders = [sender for sender in msg.senders if hasattr(sender, 'key')]
if not senders:
emit = True
if emit:
return msg.message
else:
senders = [sender.key for sender in msg.senders]
senders = ', '.join(senders)
return self.pose_transform(msg, senders)
def message_transform(self, msg, emit=False, prefix=True,
sender_strings=None, external=False):
"""
Generates the formatted string sent to listeners on a channel.
"""
if sender_strings or external:
body = self.format_external(msg, sender_strings, emit=emit)
else:
body = self.format_message(msg, emit=emit)
if prefix:
body = "%s%s" % (self.channel_prefix(msg, emit=emit), body)
msg.message = body
return msg
def pre_join_channel(self, joiner):
"""
Run right before a channel is joined. If this returns a false value,
channel joining is aborted.
"""
return True
def post_join_channel(self, joiner):
"""
Run right after an object or player joins a channel.
"""
return True
def pre_leave_channel(self, leaver):
"""
Run right before a user leaves a channel. If this returns a false
value, leaving the channel will be aborted.
"""
return True
def post_leave_channel(self, leaver):
"""
Run right after an object or player leaves a channel.
"""
pass
def pre_send_message(self, msg):
"""
Run before a message is sent to the channel.
This should return the message object, after any transformations.
If the message is to be discarded, return a false value.
"""
return msg
def post_send_message(self, msg):
"""
Run after a message is sent to the channel.
"""
pass
def at_init(self):
"""
This is always called whenever this channel is initiated --
that is, whenever it its typeclass is cached from memory. This
happens on-demand first time the channel is used or activated
in some way after being created but also after each server
restart or reload.
"""
pass
def distribute_message(self, msg, online=False):
"""
Method for grabbing all listeners that a message should be sent to on
this channel, and sending them a message.
"""
# get all players connected to this channel and send to them
for player in self.db_subscriptions.all():
try:
# note our addition of the from_channel keyword here. This could be checked
# by a custom player.msg() to treat channel-receives differently.
player.msg(msg.message, from_obj=msg.senders, from_channel=self.id)
except AttributeError, e:
logger.log_trace("%s\nCannot send msg to player '%s'." % (e, player))
def msg(self, msgobj, header=None, senders=None, sender_strings=None,
persistent=False, online=False, emit=False, external=False):
"""
Send the given message to all players connected to channel. Note that
no permission-checking is done here; it is assumed to have been
done before calling this method. The optional keywords are not used if
persistent is False.
msgobj - a Msg/TempMsg instance or a message string. If one of the
former, the remaining keywords will be ignored. If a string,
this will either be sent as-is (if persistent=False) or it
will be used together with header and senders keywords to
create a Msg instance on the fly.
senders - an object, player or a list of objects or players.
Optional if persistent=False.
sender_strings - Name strings of senders. Used for external
connections where the sender is not a player or object. When
this is defined, external will be assumed.
external - Treat this message agnostic of its sender.
persistent (default False) - ignored if msgobj is a Msg or TempMsg.
If True, a Msg will be created, using header and senders
keywords. If False, other keywords will be ignored.
online (bool) - If this is set true, only messages people who are
online. Otherwise, messages all players connected. This can
make things faster, but may not trigger listeners on players
that are offline.
emit (bool) - Signals to the message formatter that this message is
not to be directly associated with a name.
"""
if senders:
senders = make_iter(senders)
else:
senders = []
if isinstance(msgobj, basestring):
# given msgobj is a string
msg = msgobj
if persistent and self.db.keep_log:
msgobj = Msg()
msgobj.save()
else:
# Use TempMsg, so this message is not stored.
msgobj = TempMsg()
msgobj.header = header
msgobj.message = msg
msgobj.channels = [self] # add this channel
if not msgobj.senders:
msgobj.senders = senders
msgobj = self.pre_send_message(msgobj)
if not msgobj:
return False
msgobj = self.message_transform(msgobj, emit=emit,
sender_strings=sender_strings,
external=external)
self.distribute_message(msgobj, online=online)
self.post_send_message(msgobj)
return True
def tempmsg(self, message, header=None, senders=None):
"""
A wrapper for sending non-persistent messages.
"""
self.msg(message, senders=senders, header=header, persistent=False)

332
lib/comms/managers.py Normal file
View file

@ -0,0 +1,332 @@
"""
These managers handles the
"""
from django.db import models
from django.db.models import Q
from src.typeclasses.managers import (TypedObjectManager, TypeclassManager,
returns_typeclass_list, returns_typeclass)
_GA = object.__getattribute__
_PlayerDB = None
_ObjectDB = None
_ChannelDB = None
_SESSIONS = None
# error class
class CommError(Exception):
"Raise by comm system, to allow feedback to player when caught."
pass
#
# helper functions
#
def dbref(dbref, reqhash=True):
"""
Valid forms of dbref (database reference number)
are either a string '#N' or an integer N.
Output is the integer part.
"""
if reqhash and not (isinstance(dbref, basestring) and dbref.startswith("#")):
return None
if isinstance(dbref, basestring):
dbref = dbref.lstrip('#')
try:
if int(dbref) < 0:
return None
except Exception:
return None
return dbref
def identify_object(inp):
"identify if an object is a player or an object; return its database model"
# load global stores
global _PlayerDB, _ObjectDB, _ChannelDB
if not _PlayerDB:
from src.players.models import PlayerDB as _PlayerDB
if not _ObjectDB:
from src.objects.models import ObjectDB as _ObjectDB
if not _ChannelDB:
from src.comms.models import ChannelDB as _ChannelDB
if not inp:
return inp, None
if isinstance(inp, basestring):
return inp, "string"
elif inp.is_typeclass(_PlayerDB, exact=False):
return inp, "player"
elif inp.is_typeclass(_ObjectDB, exact=False):
return inp, "object"
elif inp.is_typeclass(_ChannelDB, exact=False):
return inp, "channel"
elif dbref(inp):
return dbref(inp), "dbref"
return inp, None # something else
def to_object(inp, objtype='player'):
"""
Locates the object related to the given
playername or channel key. If input was already
the correct object, return it.
inp - the input object/string
objtype - 'player' or 'channel'
"""
obj, typ = identify_object(inp)
if typ == objtype:
return obj
if objtype == 'player':
if typ == 'object':
return obj.player
if typ == 'string':
return _PlayerDB.objects.get(user_username__iexact=obj)
if typ == 'dbref':
return _PlayerDB.objects.get(id=obj)
print objtype, inp, obj, typ, type(inp)
raise CommError()
elif objtype == 'object':
if typ == 'player':
return obj.obj
if typ == 'string':
return _ObjectDB.objects.get(db_key__iexact=obj)
if typ == 'dbref':
return _ObjectDB.objects.get(id=obj)
print objtype, inp, obj, typ, type(inp)
raise CommError()
elif objtype == 'channel':
if typ == 'string':
return _ChannelDB.objects.get(db_key__iexact=obj)
if typ == 'dbref':
return _ChannelDB.objects.get(id=obj)
print objtype, inp, obj, typ, type(inp)
raise CommError()
#
# Msg manager
#
class MsgManager(models.Manager):
"""
This MsgManager implements methods for searching
and manipulating Messages directly from the database.
These methods will all return database objects
(or QuerySets) directly.
A Message represents one unit of communication, be it over a
Channel or via some form of in-game mail system. Like an e-mail,
it always has a sender and can have any number of receivers (some
of which may be Channels).
Evennia-specific:
get_message_by_id
get_messages_by_sender
get_messages_by_receiver
get_messages_by_channel
text_search
message_search (equivalent to ev.search_messages)
"""
def identify_object(self, obj):
"method version for easy access"
return identify_object(obj)
def get_message_by_id(self, idnum):
"Retrieve message by its id."
try:
return self.get(id=self.dbref(idnum, reqhash=False))
except Exception:
return None
def get_messages_by_sender(self, obj, exclude_channel_messages=False):
"""
Get all messages sent by one entity - this could be either a
player or an object
only_non_channel: only return messages -not- aimed at a channel
(e.g. private tells)
"""
obj, typ = identify_object(obj)
if exclude_channel_messages:
# explicitly exclude channel recipients
if typ == 'player':
return list(self.filter(db_sender_players=obj,
db_receivers_channels__isnull=True).exclude(db_hide_from_players=obj))
elif typ == 'object':
return list(self.filter(db_sender_objects=obj,
db_receivers_channels__isnull=True).exclude(db_hide_from_objects=obj))
else:
raise CommError
else:
# get everything, channel or not
if typ == 'player':
return list(self.filter(db_sender_players=obj).exclude(db_hide_from_players=obj))
elif typ == 'object':
return list(self.filter(db_sender_objects=obj).exclude(db_hide_from_objects=obj))
else:
raise CommError
def get_messages_by_receiver(self, obj):
"""
Get all messages sent to one give recipient
"""
obj, typ = identify_object(obj)
if typ == 'player':
return list(self.filter(db_receivers_players=obj).exclude(db_hide_from_players=obj))
elif typ == 'object':
return list(self.filter(db_receivers_objects=obj).exclude(db_hide_from_objects=obj))
elif typ == 'channel':
return list(self.filter(db_receivers_channels=obj).exclude(db_hide_from_channels=obj))
else:
raise CommError
def get_messages_by_channel(self, channel):
"""
Get all messages sent to one channel
"""
return self.filter(db_receivers_channels=channel).exclude(db_hide_from_channels=channel)
def message_search(self, sender=None, receiver=None, freetext=None, dbref=None):
"""
Search the message database for particular messages. At least one
of the arguments must be given to do a search.
sender - get messages sent by a particular player or object
receiver - get messages received by a certain player,object or channel
freetext - Search for a text string in a message.
NOTE: This can potentially be slow, so make sure to supply
one of the other arguments to limit the search.
dbref - (int) the exact database id of the message. This will override
all other search criteria since it's unique and
always gives a list with only one match.
"""
# unique msg id
if dbref:
msg = self.objects.filter(id=dbref)
if msg:
return msg[0]
# We use Q objects to gradually build up the query - this way we only
# need to do one database lookup at the end rather than gradually
# refining with multiple filter:s. Django Note: Q objects can be
# combined with & and | (=AND,OR). ~ negates the queryset
# filter by sender
sender, styp = identify_object(sender)
if styp == 'player':
sender_restrict = Q(db_sender_players=sender) & ~Q(db_hide_from_players=sender)
elif styp == 'object':
sender_restrict = Q(db_sender_objects=sender) & ~Q(db_hide_from_objects=sender)
else:
sender_restrict = Q()
# filter by receiver
receiver, rtyp = identify_object(receiver)
if rtyp == 'player':
receiver_restrict = Q(db_receivers_players=receiver) & ~Q(db_hide_from_players=receiver)
elif rtyp == 'object':
receiver_restrict = Q(db_receivers_objects=receiver) & ~Q(db_hide_from_objects=receiver)
elif rtyp == 'channel':
receiver_restrict = Q(db_receivers_channels=receiver) & ~Q(db_hide_from_channels=receiver)
else:
receiver_restrict = Q()
# filter by full text
if freetext:
fulltext_restrict = Q(db_header__icontains=freetext) | Q(db_message__icontains=freetext)
else:
fulltext_restrict = Q()
# execute the query
return list(self.filter(sender_restrict & receiver_restrict & fulltext_restrict))
#
# Channel manager
#
class ChannelDBManager(TypedObjectManager):
"""
This ChannelManager implements methods for searching
and manipulating Channels directly from the database.
These methods will all return database objects
(or QuerySets) directly.
A Channel is an in-game venue for communication. It's
essentially representation of a re-sender: Users sends
Messages to the Channel, and the Channel re-sends those
messages to all users subscribed to the Channel.
Evennia-specific:
get_all_channels
get_channel(channel)
get_subscriptions(player)
channel_search (equivalent to ev.search_channel)
"""
@returns_typeclass_list
def get_all_channels(self):
"""
Returns all channels in game.
"""
return self.all()
@returns_typeclass
def get_channel(self, channelkey):
"""
Return the channel object if given its key.
Also searches its aliases.
"""
# first check the channel key
channels = self.filter(db_key__iexact=channelkey)
if not channels:
# also check aliases
channels = [channel for channel in self.all()
if channelkey in channel.aliases.all()]
if channels:
return channels[0]
return None
@returns_typeclass_list
def get_subscriptions(self, player):
"""
Return all channels a given player is subscribed to
"""
return player.subscription_set.all()
@returns_typeclass_list
def channel_search(self, ostring, exact=True):
"""
Search the channel database for a particular channel.
ostring - the key or database id of the channel.
exact - require an exact key match (still not case sensitive)
"""
channels = []
if not ostring: return channels
try:
# try an id match first
dbref = int(ostring.strip('#'))
channels = self.filter(id=dbref)
except Exception:
pass
if not channels:
# no id match. Search on the key.
if exact:
channels = self.filter(db_key__iexact=ostring)
else:
channels = self.filter(db_key__icontains=ostring)
if not channels:
# still no match. Search by alias.
channels = [channel for channel in self.all()
if ostring.lower() in [a.lower
for a in channel.aliases.all()]]
return channels
class ChannelManager(ChannelDBManager, TypeclassManager):
pass

View file

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='ChannelDB',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('db_key', models.CharField(max_length=255, verbose_name=b'key', db_index=True)),
('db_typeclass_path', models.CharField(help_text=b"this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.", max_length=255, null=True, verbose_name=b'typeclass')),
('db_date_created', models.DateTimeField(auto_now_add=True, verbose_name=b'creation date')),
('db_lock_storage', models.TextField(help_text=b"locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.", verbose_name=b'locks', blank=True)),
],
options={
'verbose_name': 'Channel',
'verbose_name_plural': 'Channels',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Msg',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('db_sender_external', models.CharField(help_text=b"identifier for external sender, for example a sender over an IRC connection (i.e. someone who doesn't have an exixtence in-game).", max_length=255, null=True, verbose_name=b'external sender', db_index=True)),
('db_header', models.TextField(null=True, verbose_name=b'header', blank=True)),
('db_message', models.TextField(verbose_name=b'messsage')),
('db_date_sent', models.DateTimeField(auto_now_add=True, verbose_name=b'date sent', db_index=True)),
('db_lock_storage', models.TextField(help_text=b'access locks on this message.', verbose_name=b'locks', blank=True)),
('db_hide_from_channels', models.ManyToManyField(related_name=b'hide_from_channels_set', null=True, to='comms.ChannelDB')),
],
options={
'verbose_name': 'Message',
},
bases=(models.Model,),
),
]

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('objects', '0001_initial'),
('comms', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='msg',
name='db_hide_from_objects',
field=models.ManyToManyField(related_name=b'hide_from_objects_set', null=True, to='objects.ObjectDB'),
preserve_default=True,
),
]

View file

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('objects', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('typeclasses', '0001_initial'),
('comms', '0002_msg_db_hide_from_objects'),
]
operations = [
migrations.AddField(
model_name='msg',
name='db_hide_from_players',
field=models.ManyToManyField(related_name=b'hide_from_players_set', null=True, to=settings.AUTH_USER_MODEL),
preserve_default=True,
),
migrations.AddField(
model_name='msg',
name='db_receivers_channels',
field=models.ManyToManyField(help_text=b'channel recievers', related_name=b'channel_set', null=True, to='comms.ChannelDB'),
preserve_default=True,
),
migrations.AddField(
model_name='msg',
name='db_receivers_objects',
field=models.ManyToManyField(help_text=b'object receivers', related_name=b'receiver_object_set', null=True, to='objects.ObjectDB'),
preserve_default=True,
),
migrations.AddField(
model_name='msg',
name='db_receivers_players',
field=models.ManyToManyField(help_text=b'player receivers', related_name=b'receiver_player_set', null=True, to=settings.AUTH_USER_MODEL),
preserve_default=True,
),
migrations.AddField(
model_name='msg',
name='db_sender_objects',
field=models.ManyToManyField(related_name=b'sender_object_set', null=True, verbose_name=b'sender(object)', to='objects.ObjectDB', db_index=True),
preserve_default=True,
),
migrations.AddField(
model_name='msg',
name='db_sender_players',
field=models.ManyToManyField(related_name=b'sender_player_set', null=True, verbose_name=b'sender(player)', to=settings.AUTH_USER_MODEL, db_index=True),
preserve_default=True,
),
migrations.AddField(
model_name='channeldb',
name='db_attributes',
field=models.ManyToManyField(help_text=b'attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute', null=True),
preserve_default=True,
),
migrations.AddField(
model_name='channeldb',
name='db_subscriptions',
field=models.ManyToManyField(related_name=b'subscription_set', null=True, verbose_name=b'subscriptions', to=settings.AUTH_USER_MODEL, db_index=True),
preserve_default=True,
),
migrations.AddField(
model_name='channeldb',
name='db_tags',
field=models.ManyToManyField(help_text=b'tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag', null=True),
preserve_default=True,
),
]

View file

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

368
lib/comms/models.py Normal file
View file

@ -0,0 +1,368 @@
"""
Models for the comsystem. The Commsystem is intended to be
used by Players (thematic IC communication is probably
best handled by custom commands instead).
The comm system could take the form of channels, but can also
be adopted for storing tells or in-game mail.
The comsystem's main component is the Message (Msg), which
carries the actual information between two parties.
Msgs are stored in the database and usually not
deleted.
A Msg always have one sender (a user), but can have
any number targets, both users and channels.
Channels are central objects that act as targets for
Msgs. Players can connect to channels by use of a
ChannelConnect object (this object is necessary to easily
be able to delete connections on the fly).
"""
from datetime import datetime
from django.conf import settings
from django.db import models
from src.typeclasses.models import TypedObject
from src.utils.idmapper.models import SharedMemoryModel
from src.comms import managers
from src.comms.managers import identify_object
from src.locks.lockhandler import LockHandler
from src.utils.utils import crop, make_iter, lazy_property
__all__ = ("Msg", "TempMsg", "ChannelDB")
_GA = object.__getattribute__
_SA = object.__setattr__
_DA = object.__delattr__
#------------------------------------------------------------
#
# Msg
#
#------------------------------------------------------------
class Msg(SharedMemoryModel):
"""
A single message. This model describes all ooc messages
sent in-game, both to channels and between players.
The Msg class defines the following properties:
sender - sender of message
receivers - list of target objects for message
channels - list of channels message was sent to
message - the text being sent
date_sent - time message was sent
hide_from_sender - bool if message should be hidden from sender
hide_from_receivers - list of receiver objects to hide message from
hide_from_channels - list of channels objects to hide message from
permissions - perm strings
"""
#
# Msg database model setup
#
#
# These databse fields are all set using their corresponding properties,
# named same as the field, but withtout the db_* prefix.
# Sender is either a player, an object or an external sender, like
# an IRC channel; normally there is only one, but if co-modification of
# a message is allowed, there may be more than one "author"
db_sender_players = models.ManyToManyField("players.PlayerDB", related_name='sender_player_set', null=True, verbose_name='sender(player)', db_index=True)
db_sender_objects = models.ManyToManyField("objects.ObjectDB", related_name='sender_object_set', null=True, verbose_name='sender(object)', db_index=True)
db_sender_external = models.CharField('external sender', max_length=255, null=True, db_index=True,
help_text="identifier for external sender, for example a sender over an IRC connection (i.e. someone who doesn't have an exixtence in-game).")
# The destination objects of this message. Stored as a
# comma-separated string of object dbrefs. Can be defined along
# with channels below.
db_receivers_players = models.ManyToManyField('players.PlayerDB', related_name='receiver_player_set', null=True, help_text="player receivers")
db_receivers_objects = models.ManyToManyField('objects.ObjectDB', related_name='receiver_object_set', null=True, help_text="object receivers")
db_receivers_channels = models.ManyToManyField("ChannelDB", related_name='channel_set', null=True, help_text="channel recievers")
# header could be used for meta-info about the message if your system needs
# it, or as a separate store for the mail subject line maybe.
db_header = models.TextField('header', null=True, blank=True)
# the message body itself
db_message = models.TextField('messsage')
# send date
db_date_sent = models.DateTimeField('date sent', editable=False, auto_now_add=True, db_index=True)
# lock storage
db_lock_storage = models.TextField('locks', blank=True,
help_text='access locks on this message.')
# these can be used to filter/hide a given message from supplied objects/players/channels
db_hide_from_players = models.ManyToManyField("players.PlayerDB", related_name='hide_from_players_set', null=True)
db_hide_from_objects = models.ManyToManyField("objects.ObjectDB", related_name='hide_from_objects_set', null=True)
db_hide_from_channels = models.ManyToManyField("ChannelDB", related_name='hide_from_channels_set', null=True)
# Database manager
objects = managers.MsgManager()
_is_deleted = False
def __init__(self, *args, **kwargs):
SharedMemoryModel.__init__(self, *args, **kwargs)
self.extra_senders = []
class Meta:
"Define Django meta options"
verbose_name = "Message"
# Wrapper properties to easily set database fields. These are
# @property decorators that allows to access these fields using
# normal python operations (without having to remember to save()
# etc). So e.g. a property 'attr' has a get/set/del decorator
# defined that allows the user to do self.attr = value,
# value = self.attr and del self.attr respectively (where self
# is the object in question).
# sender property (wraps db_sender_*)
#@property
def __senders_get(self):
"Getter. Allows for value = self.sender"
return list(self.db_sender_players.all()) + \
list(self.db_sender_objects.all()) + \
self.extra_senders
#@sender.setter
def __senders_set(self, value):
"Setter. Allows for self.sender = value"
for val in (v for v in make_iter(value) if v):
obj, typ = identify_object(val)
if typ == 'player':
self.db_sender_players.add(obj)
elif typ == 'object':
self.db_sender_objects.add(obj)
elif isinstance(typ, basestring):
self.db_sender_external = obj
elif not obj:
return
else:
raise ValueError(obj)
self.save()
#@sender.deleter
def __senders_del(self):
"Deleter. Clears all senders"
self.db_sender_players.clear()
self.db_sender_objects.clear()
self.db_sender_external = ""
self.extra_senders = []
self.save()
senders = property(__senders_get, __senders_set, __senders_del)
def remove_sender(self, value):
"Remove a single sender or a list of senders"
for val in make_iter(value):
obj, typ = identify_object(val)
if typ == 'player':
self.db_sender_players.remove(obj)
elif typ == 'object':
self.db_sender_objects.remove(obj)
elif isinstance(obj, basestring):
self.db_sender_external = obj
else:
raise ValueError(obj)
self.save()
# receivers property
#@property
def __receivers_get(self):
"""
Getter. Allows for value = self.receivers.
Returns three lists of receivers: players, objects and channels.
"""
return list(self.db_receivers_players.all()) + list(self.db_receivers_objects.all())
#@receivers.setter
def __receivers_set(self, value):
"""
Setter. Allows for self.receivers = value.
This appends a new receiver to the message.
"""
for val in (v for v in make_iter(value) if v):
obj, typ = identify_object(val)
if typ == 'player':
self.db_receivers_players.add(obj)
elif typ == 'object':
self.db_receivers_objects.add(obj)
elif not obj:
return
else:
raise ValueError
self.save()
#@receivers.deleter
def __receivers_del(self):
"Deleter. Clears all receivers"
self.db_receivers_players.clear()
self.db_receivers_objects.clear()
self.extra_senders = []
self.save()
receivers = property(__receivers_get, __receivers_set, __receivers_del)
def remove_receiver(self, obj):
"Remove a single recevier"
obj, typ = identify_object(obj)
if typ == 'player':
self.db_receivers_players.remove(obj)
elif typ == 'object':
self.db_receivers_objects.remove(obj)
else:
raise ValueError
self.save()
# channels property
#@property
def __channels_get(self):
"Getter. Allows for value = self.channels. Returns a list of channels."
return self.db_receivers_channels.all()
#@channels.setter
def __channels_set(self, value):
"""
Setter. Allows for self.channels = value.
Requires a channel to be added.
"""
for val in (v for v in make_iter(value) if v):
self.db_receivers_channels.add(val)
#@channels.deleter
def __channels_del(self):
"Deleter. Allows for del self.channels"
self.db_receivers_channels.clear()
self.save()
channels = property(__channels_get, __channels_set, __channels_del)
def __hide_from_get(self):
"""
Getter. Allows for value = self.hide_from.
Returns 3 lists of players, objects and channels
"""
return self.db_hide_from_players.all(), self.db_hide_from_objects.all(), self.db_hide_from_channels.all()
#@hide_from_sender.setter
def __hide_from_set(self, value):
"Setter. Allows for self.hide_from = value. Will append to hiders"
obj, typ = identify_object(value)
if typ == "player":
self.db_hide_from_players.add(obj)
elif typ == "object":
self.db_hide_from_objects.add(obj)
elif typ == "channel":
self.db_hide_from_channels.add(obj)
else:
raise ValueError
self.save()
#@hide_from_sender.deleter
def __hide_from_del(self):
"Deleter. Allows for del self.hide_from_senders"
self.db_hide_from_players.clear()
self.db_hide_from_objects.clear()
self.db_hide_from_channels.clear()
self.save()
hide_from = property(__hide_from_get, __hide_from_set, __hide_from_del)
#
# Msg class methods
#
def __str__(self):
"This handles what is shown when e.g. printing the message"
senders = ",".join(obj.key for obj in self.senders)
receivers = ",".join(["[%s]" % obj.key for obj in self.channels] + [obj.key for obj in self.receivers])
return "%s->%s: %s" % (senders, receivers, crop(self.message, width=40))
#------------------------------------------------------------
#
# TempMsg
#
#------------------------------------------------------------
class TempMsg(object):
"""
This is a non-persistent object for sending
temporary messages that will not be stored.
It mimics the "real" Msg object, but don't require
sender to be given.
"""
def __init__(self, senders=None, receivers=None, channels=None, message="", header="", type="", lockstring="", hide_from=None):
self.senders = senders and make_iter(senders) or []
self.receivers = receivers and make_iter(receivers) or []
self.channels = channels and make_iter(channels) or []
self.type = type
self.header = header
self.message = message
self.lock_storage = lockstring
self.hide_from = hide_from and make_iter(hide_from) or []
self.date_sent = datetime.now()
@lazy_property
def locks(self):
return LockHandler(self)
def __str__(self):
"This handles what is shown when e.g. printing the message"
senders = ",".join(obj.key for obj in self.senders)
receivers = ",".join(["[%s]" % obj.key for obj in self.channels] + [obj.key for obj in self.receivers])
return "%s->%s: %s" % (senders, receivers, crop(self.message, width=40))
def remove_sender(self, obj):
"Remove a sender or a list of senders"
for o in make_iter(obj):
try:
self.senders.remove(o)
except ValueError:
pass # nothing to remove
def remove_receiver(self, obj):
"Remove a sender or a list of senders"
for o in make_iter(obj):
try:
self.senders.remove(o)
except ValueError:
pass # nothing to remove
def access(self, accessing_obj, access_type='read', default=False):
"checks lock access"
return self.locks.check(accessing_obj,
access_type=access_type, default=default)
#------------------------------------------------------------
#
# Channel
#
#------------------------------------------------------------
class ChannelDB(TypedObject):
"""
This is the basis of a comm channel, only implementing
the very basics of distributing messages.
The Channel class defines the following properties:
key - main name for channel
desc - optional description of channel
aliases - alternative names for the channel
permissions - perm strings
"""
db_subscriptions = models.ManyToManyField("players.PlayerDB",
related_name="subscription_set", null=True, verbose_name='subscriptions', db_index=True)
# Database manager
objects = managers.ChannelDBManager()
_typeclass_paths = settings.CHANNEL_TYPECLASS_PATHS
_default_typeclass_path = settings.BASE_CHANNEL_TYPECLASS or "src.comms.comms.Channel"
class Meta:
"Define Django meta options"
verbose_name = "Channel"
verbose_name_plural = "Channels"
def __str__(self):
return "Channel '%s' (%s)" % (self.key, self.db.desc)