Source code for ploneintranet.messaging.messaging

# -*- coding: utf-8 -*-
from BTrees.LOBTree import LOBTree
from BTrees.OOBTree import OOBTree
from Persistence import Persistent
from datetime import datetime
from plone import api
from ploneintranet.messaging.events import MessageSendEvent
from ploneintranet.messaging.interfaces import IConversation
from ploneintranet.messaging.interfaces import IInbox
from ploneintranet.messaging.interfaces import IInboxes
from ploneintranet.messaging.interfaces import IMessage
from ploneintranet.messaging.interfaces import IMessagingLocator
from zope.event import notify
from zope.interface import implementer
from zope.interface.verify import verifyObject
import logging
import pytz
import time

logger = logging.getLogger(__name__)


[docs]class BTreeDictBase(Persistent): """Pass through the dict api to a BTree saved in self.data This is required cause attributes on **BTree subclasses seem to not be persistent. It also takes care to set a __parent__ pointer """ __parent__ = None def __setitem__(self, key, value): value.__parent__ = self return self.data.__setitem__(key, value) def __getitem__(self, key): return self.data.__getitem__(key) def __contains__(self, key): return self.data.__contains__(key) def __delitem__(self, key): return self.data.__delitem__(key)
[docs] def keys(self): return self.data.keys()
[docs]@implementer(IMessage) class Message(Persistent): __parent__ = None sender = None recipient = None text = None created = None uid = None new = True def __init__(self, sender, recipient, text, created): if sender == recipient: msg = "Sender and recipient are identical ({0}, {1})" raise ValueError(msg.format(sender, recipient)) # FIXME: test if not text.strip(): raise ValueError("Message has no text") # FIXME: test if not isinstance(created, datetime): raise ValueError("created has to be a datetime object") self.sender = sender self.recipient = recipient self.text = text self.created = created
[docs] def to_dict(self): return dict( sender=self.sender, recipient=self.recipient, text=self.text, created=self.created, new=self.new, uid=self.uid, )
[docs]@implementer(IConversation) class Conversation(BTreeDictBase): username = None # other user new_messages_count = 0 created = None last = None # last msg from other to inbox owner def __init__(self, username, created=None): self.data = LOBTree() self.username = username # not inbox owner but other user if created is None: created = datetime.now(pytz.utc) self.created = created
[docs] def to_long(self, dt): """Turns a `datetime` object into a long. Since this is used as BTree key it must be sequential, hence we force UTC. """ if dt.tzinfo != pytz.utc: raise ValueError("datetime storage values MUST be UTC") return long(time.mktime(dt.timetuple()) * 1000000 + dt.microsecond)
[docs] def generate_key(self, message): """Generate a long int key for a message.""" key = self.to_long(message.created) while key in self.data: key = key + 1 return key
[docs] def add_message(self, message): key = self.generate_key(message) message.uid = key self[key] = message self.last = message return key
def __setitem__(self, key, message): if key != message.uid: msg = "key and message.uid differ ({0}/{1})" raise KeyError(msg.format(key, message.uid)) # delete old message if there is one to make sure the # new_messages_count is correct and update the new_messages_count # with the new message if key in self: del self[key] if message.new is True: self.update_new_messages_count(+1) super(Conversation, self).__setitem__(key, message) def __delitem__(self, uid): message = self[uid] if message.new is True: self.update_new_messages_count(-1) super(Conversation, self).__delitem__(uid)
[docs] def get_messages(self): return self.data.values()
[docs] def mark_read(self, message=None): if message: message.new = False self.update_new_messages_count(-1) else: # use update function to update inbox too self.update_new_messages_count(self.new_messages_count * -1) # mark all messages as read for message in self.data.values(): message.new = False
[docs] def update_new_messages_count(self, difference): count = self.new_messages_count count = count + difference if count < 0: # FIXME: Error. Log? count = 0 self.new_messages_count = count # update the inbox accordingly self.__parent__.update_new_messages_count(difference)
[docs] def to_dict(self): member = api.user.get(self.username) return { "username": self.username, "fullname": member.getProperty("fullname"), "new_messages_count": self.new_messages_count, }
[docs]@implementer(IInbox) class Inbox(BTreeDictBase): username = None # owner of inbox new_messages_count = 0 def __init__(self, username): self.data = OOBTree() self.username = username
[docs] def add_conversation(self, conversation): self[conversation.username] = conversation return conversation
def __setitem__(self, key, conversation): if key != conversation.username: msg = "conversation.username and key differ ({0}, {1})" raise KeyError(msg.format(conversation.username, key)) if conversation.username == self.username: raise ValueError("You can't speak to yourself") verifyObject(IConversation, conversation) if key in self: raise KeyError("Conversation exists already") super(Inbox, self).__setitem__(conversation.username, conversation) self.update_new_messages_count(conversation.new_messages_count) return conversation def __delitem__(self, key): conversation = self[key] self.update_new_messages_count(conversation.new_messages_count * -1) super(Inbox, self).__delitem__(key)
[docs] def get_conversations(self): return self.data.values()
[docs] def is_blocked(self, username): # FIXME: not implemented return False
[docs] def update_new_messages_count(self, difference): count = self.new_messages_count count = count + difference if count < 0: # FIXME: Error. Log? count = 0 self.new_messages_count = count
[docs]@implementer(IInboxes) class Inboxes(BTreeDictBase): def __init__(self): self.data = OOBTree()
[docs] def add_inbox(self, username): if username in self: raise ValueError("Inbox for user {0} exists".format(username)) inbox = Inbox(username) self[username] = inbox return inbox
def __setitem__(self, key, inbox): verifyObject(IInbox, inbox) if key != inbox.username: msg = "Inbox username and key differ ({0}/{1})" raise KeyError(msg.format(inbox.username, key)) # outside tests we need to remove the acquisition wrapper # unwrapped_self = self.aq_base if hasattr(self, 'aq_base') else self return BTreeDictBase.__setitem__(self, key, inbox) # return super(Inboxes, unwrapped_self).__setitem__(key, inbox)
[docs] def send_message(self, sender, recipient, text, created=None): if sender not in self: self.add_inbox(sender) sender_inbox = self[sender] if recipient not in self: self.add_inbox(recipient) recipient_inbox = self[recipient] if recipient_inbox.is_blocked("sender"): # FIXME: Own Exception or security exception? raise ValueError( "User is not allowed to send a Message to " "the Recipient" ) # force UTC datetimes always # - needed for sequential BTree storage keying # - is assumed in browser rendering timezone conversion if created is None: created = datetime.now(pytz.utc) elif not created.tzinfo or created.tzinfo.utcoffset(created) is None: # naive datetime, just BOFH it to UTC. Should happen only in tests logger.warn("Naive datetime not allowed. Forcing to UTC.") created = created.replace(tzinfo=pytz.utc) elif created.tzinfo != pytz.utc: created = created.astimezone(pytz.utc) for pair in ( (sender_inbox, recipient, True, False), (recipient_inbox, sender, False, True), ): inbox = pair[0] conversation_user = pair[1] if conversation_user not in inbox: inbox.add_conversation(Conversation(conversation_user, created)) conversation = inbox[conversation_user] message = Message(sender, recipient, text, created) conversation.add_message(message) mark_read = pair[2] if mark_read: conversation.mark_read(message) send_event = pair[3] if send_event: event = MessageSendEvent(message) notify(event)
[docs]@implementer(IMessagingLocator) class MessagingLocator(object): """A utility used to locate conversations and messages."""
[docs] def get_inboxes(self): return api.portal.get_tool("ploneintranet_messaging")