Caching conversation data to avoid false positives on program start
[theonering] / src / gvoice / conversations.py
1 #!/usr/bin/python
2
3 from __future__ import with_statement
4
5 import logging
6
7 try:
8         import cPickle
9         pickle = cPickle
10 except ImportError:
11         import pickle
12
13 import util.coroutines as coroutines
14 import util.misc as util_misc
15
16
17 _moduleLogger = logging.getLogger("gvoice.conversations")
18
19
20 class Conversations(object):
21
22         def __init__(self, getter):
23                 self._get_raw_conversations = getter
24                 self._conversations = {}
25
26                 self.updateSignalHandler = coroutines.CoTee()
27
28         @property
29         def _name(self):
30                 return repr(self._get_raw_conversations.__name__)
31
32         def load(self, path):
33                 assert not self._conversations
34                 try:
35                         with open(path, "rb") as f:
36                                 self._conversations = pickle.load(f)
37                 except (pickle.PickleError, IOError):
38                         _moduleLogger.exception("While loading for %s" % self._name)
39
40         def save(self, path):
41                 try:
42                         with open(path, "wb") as f:
43                                 pickle.dump(self._conversations, f, pickle.HIGHEST_PROTOCOL)
44                 except (pickle.PickleError, IOError):
45                         _moduleLogger.exception("While saving for %s" % self._name)
46
47         def update(self, force=False):
48                 if not force and self._conversations:
49                         return
50
51                 oldConversationIds = set(self._conversations.iterkeys())
52
53                 updateConversationIds = set()
54                 conversations = list(self._get_raw_conversations())
55                 conversations.sort()
56                 for conversation in conversations:
57                         key = conversation.contactId, util_misc.normalize_number(conversation.number)
58                         try:
59                                 mergedConversations = self._conversations[key]
60                         except KeyError:
61                                 mergedConversations = MergedConversations()
62                                 self._conversations[key] = mergedConversations
63
64                         try:
65                                 mergedConversations.append_conversation(conversation)
66                                 isConversationUpdated = True
67                         except RuntimeError, e:
68                                 if False:
69                                         _moduleLogger.info("%s Skipping conversation for %r because '%s'" % (self._name, key, e))
70                                 isConversationUpdated = False
71
72                         if isConversationUpdated:
73                                 updateConversationIds.add(key)
74
75                 if updateConversationIds:
76                         message = (self, updateConversationIds, )
77                         self.updateSignalHandler.stage.send(message)
78
79         def get_conversations(self):
80                 return self._conversations.iterkeys()
81
82         def get_conversation(self, key):
83                 return self._conversations[key]
84
85         def clear_conversation(self, key):
86                 try:
87                         del self._conversations[key]
88                 except KeyError:
89                         _moduleLogger.info("%s Conversation never existed for %r" % (self._name, key, ))
90
91         def clear_all(self):
92                 self._conversations.clear()
93
94
95 class MergedConversations(object):
96
97         def __init__(self):
98                 self._conversations = []
99
100         def append_conversation(self, newConversation):
101                 self._validate(newConversation)
102                 for similarConversation in self._find_related_conversation(newConversation.id):
103                         self._update_previous_related_conversation(similarConversation, newConversation)
104                         self._remove_repeats(similarConversation, newConversation)
105                 self._conversations.append(newConversation)
106
107         @property
108         def conversations(self):
109                 return self._conversations
110
111         def _validate(self, newConversation):
112                 if not self._conversations:
113                         return
114
115                 for constantField in ("contactId", "number"):
116                         assert getattr(self._conversations[0], constantField) == getattr(newConversation, constantField), "Constant field changed, soemthing is seriously messed up: %r v %r" % (
117                                 getattr(self._conversations[0], constantField),
118                                 getattr(newConversation, constantField),
119                         )
120
121                 if newConversation.time <= self._conversations[-1].time:
122                         raise RuntimeError("Conversations got out of order")
123
124         def _find_related_conversation(self, convId):
125                 similarConversations = (
126                         conversation
127                         for conversation in self._conversations
128                         if conversation.id == convId
129                 )
130                 return similarConversations
131
132         def _update_previous_related_conversation(self, relatedConversation, newConversation):
133                 for commonField in ("isRead", "isSpam", "isTrash", "isArchived"):
134                         newValue = getattr(newConversation, commonField)
135                         setattr(relatedConversation, commonField, newValue)
136
137         def _remove_repeats(self, relatedConversation, newConversation):
138                 newConversationMessages = newConversation.messages
139                 newConversation.messages = [
140                         newMessage
141                         for newMessage in newConversationMessages
142                         if newMessage not in relatedConversation.messages
143                 ]
144                 _moduleLogger.debug("%s Found %d new messages in conversation %s (%d/%d)" % (
145                         self._name,
146                         len(newConversationMessages) - len(newConversation.messages),
147                         newConversation.id,
148                         len(newConversation.messages),
149                         len(newConversationMessages),
150                 ))
151                 assert 0 < len(newConversation.messages), "Everything shouldn't have been removed"