3 from __future__ import with_statement
14 import util.coroutines as coroutines
15 import util.misc as misc_utils
18 _moduleLogger = logging.getLogger("gvoice.conversations")
21 class Conversations(object):
23 def __init__(self, getter):
24 self._get_raw_conversations = getter
25 self._conversations = {}
27 self.updateSignalHandler = coroutines.CoTee()
31 return repr(self._get_raw_conversations.__name__)
34 assert not self._conversations
36 with open(path, "rb") as f:
37 fileVersion, fileBuild, convs = pickle.load(f)
38 except (pickle.PickleError, IOError):
39 _moduleLogger.exception("While loading for %s" % self._name)
42 if fileVersion == constants.__version__ and fileBuild == constants.__build__:
43 self._conversations = convs
46 "%s Skipping cache due to version mismatch (%s-%s)" % (self._name, fileVersion, fileBuild)
51 dataToDump = (constants.__version__, constants.__build__, self._conversations)
52 with open(path, "wb") as f:
53 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
54 except (pickle.PickleError, IOError):
55 _moduleLogger.exception("While saving for %s" % self._name)
57 def update(self, force=False):
58 if not force and self._conversations:
61 oldConversationIds = set(self._conversations.iterkeys())
63 updateConversationIds = set()
64 conversations = list(self._get_raw_conversations())
66 for conversation in conversations:
67 key = misc_utils.normalize_number(conversation.number)
69 mergedConversations = self._conversations[key]
71 mergedConversations = MergedConversations()
72 self._conversations[key] = mergedConversations
75 mergedConversations.append_conversation(conversation)
76 isConversationUpdated = True
77 except RuntimeError, e:
79 _moduleLogger.debug("%s Skipping conversation for %r because '%s'" % (self._name, key, e))
80 isConversationUpdated = False
82 if isConversationUpdated:
83 updateConversationIds.add(key)
85 if updateConversationIds:
86 message = (self, updateConversationIds, )
87 self.updateSignalHandler.stage.send(message)
89 def get_conversations(self):
90 return self._conversations.iterkeys()
92 def get_conversation(self, key):
93 return self._conversations[key]
95 def clear_conversation(self, key):
97 del self._conversations[key]
99 _moduleLogger.info("%s Conversation never existed for %r" % (self._name, key, ))
102 self._conversations.clear()
105 class MergedConversations(object):
108 self._conversations = []
110 def append_conversation(self, newConversation):
111 self._validate(newConversation)
113 for similarConversation in self._find_related_conversation(newConversation.id):
114 self._update_previous_related_conversation(similarConversation, newConversation)
115 self._remove_repeats(similarConversation, newConversation)
118 # Hack to reduce a race window with GV marking messages as read
119 # because it thinks we replied when really we replied to the
120 # previous message. Clients of this code are expected to handle
121 # this gracefully. Other race conditions may exist but clients are
122 # responsible for them
123 if newConversation.messages:
124 newConversation.isRead = False
126 newConversation.isRead = True
127 self._conversations.append(newConversation)
131 selfDict["conversations"] = [conv.to_dict() for conv in self._conversations]
135 def conversations(self):
136 return self._conversations
138 def _validate(self, newConversation):
139 if not self._conversations:
142 for constantField in ("number", ):
143 assert getattr(self._conversations[0], constantField) == getattr(newConversation, constantField), "Constant field changed, soemthing is seriously messed up: %r v %r" % (
144 getattr(self._conversations[0], constantField),
145 getattr(newConversation, constantField),
148 if newConversation.time <= self._conversations[-1].time:
149 raise RuntimeError("Conversations got out of order")
151 def _find_related_conversation(self, convId):
152 similarConversations = (
154 for conversation in self._conversations
155 if conversation.id == convId
157 return similarConversations
159 def _update_previous_related_conversation(self, relatedConversation, newConversation):
160 for commonField in ("isSpam", "isTrash", "isArchived"):
161 newValue = getattr(newConversation, commonField)
162 setattr(relatedConversation, commonField, newValue)
164 def _remove_repeats(self, relatedConversation, newConversation):
165 newConversationMessages = newConversation.messages
166 newConversation.messages = [
168 for newMessage in newConversationMessages
169 if newMessage not in relatedConversation.messages
171 _moduleLogger.debug("Found %d new messages in conversation %s (%d/%d)" % (
172 len(newConversationMessages) - len(newConversation.messages),
174 len(newConversation.messages),
175 len(newConversationMessages),
177 assert 0 < len(newConversation.messages), "Everything shouldn't have been removed"