# -*- 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")