fed382b7d2ef8fe9ad4086ddbe0391a7a78b3044
[theonering] / src / gvoice / conversations.py
1 #!/usr/bin/python
2
3 from __future__ import with_statement
4
5 import datetime
6 import logging
7
8 try:
9         import cPickle
10         pickle = cPickle
11 except ImportError:
12         import pickle
13
14 import constants
15 import util.coroutines as coroutines
16 import util.misc as misc_utils
17
18
19 _moduleLogger = logging.getLogger(__name__)
20
21
22 class Conversations(object):
23
24         def __init__(self, getter):
25                 self._get_raw_conversations = getter
26                 self._conversations = {}
27
28                 self.updateSignalHandler = coroutines.CoTee()
29
30         @property
31         def _name(self):
32                 return repr(self._get_raw_conversations.__name__)
33
34         def load(self, path):
35                 assert not self._conversations
36                 try:
37                         with open(path, "rb") as f:
38                                 fileVersion, fileBuild, convs = pickle.load(f)
39                 except (pickle.PickleError, IOError, EOFError, ValueError):
40                         _moduleLogger.exception("While loading for %s" % self._name)
41                         return
42
43                 if misc_utils.compare_versions(
44                         misc_utils.parse_version("0.8.0"),
45                         misc_utils.parse_version(fileVersion),
46                 ) <= 0:
47                         self._conversations = convs
48                 else:
49                         _moduleLogger.debug(
50                                 "%s Skipping cache due to version mismatch (%s-%s)" % (
51                                         self._name, fileVersion, fileBuild
52                                 )
53                         )
54
55         def save(self, path):
56                 try:
57                         dataToDump = (constants.__version__, constants.__build__, self._conversations)
58                         with open(path, "wb") as f:
59                                 pickle.dump(dataToDump, f, pickle.HIGHEST_PROTOCOL)
60                 except (pickle.PickleError, IOError):
61                         _moduleLogger.exception("While saving for %s" % self._name)
62
63         def update(self, force=False):
64                 if not force and self._conversations:
65                         return
66
67                 oldConversationIds = set(self._conversations.iterkeys())
68
69                 updateConversationIds = set()
70                 conversations = list(self._get_raw_conversations())
71                 conversations.sort()
72                 for conversation in conversations:
73                         key = misc_utils.normalize_number(conversation.number)
74                         try:
75                                 mergedConversations = self._conversations[key]
76                         except KeyError:
77                                 mergedConversations = MergedConversations()
78                                 self._conversations[key] = mergedConversations
79
80                         try:
81                                 mergedConversations.append_conversation(conversation)
82                                 isConversationUpdated = True
83                         except RuntimeError, e:
84                                 if False:
85                                         _moduleLogger.debug("%s Skipping conversation for %r because '%s'" % (self._name, key, e))
86                                 isConversationUpdated = False
87
88                         if isConversationUpdated:
89                                 updateConversationIds.add(key)
90
91                 if updateConversationIds:
92                         message = (self, updateConversationIds, )
93                         self.updateSignalHandler.stage.send(message)
94
95         def get_conversations(self):
96                 return self._conversations.iterkeys()
97
98         def get_conversation(self, key):
99                 return self._conversations[key]
100
101         def clear_conversation(self, key):
102                 try:
103                         del self._conversations[key]
104                 except KeyError:
105                         _moduleLogger.info("%s Conversation never existed for %r" % (self._name, key, ))
106
107         def clear_all(self):
108                 self._conversations.clear()
109
110
111 class MergedConversations(object):
112
113         def __init__(self):
114                 self._conversations = []
115
116         def append_conversation(self, newConversation):
117                 self._validate(newConversation)
118                 similarExist = False
119                 for similarConversation in self._find_related_conversation(newConversation.id):
120                         self._update_previous_related_conversation(similarConversation, newConversation)
121                         self._remove_repeats(similarConversation, newConversation)
122                         similarExist = True
123                 if similarExist:
124                         # Hack to reduce a race window with GV marking messages as read
125                         # because it thinks we replied when really we replied to the
126                         # previous message.  Clients of this code are expected to handle
127                         # this gracefully.  Other race conditions may exist but clients are
128                         # responsible for them
129                         if newConversation.messages:
130                                 newConversation.isRead = False
131                         else:
132                                 newConversation.isRead = True
133                 self._conversations.append(newConversation)
134
135         def to_dict(self):
136                 selfDict = {}
137                 selfDict["conversations"] = [conv.to_dict() for conv in self._conversations]
138                 return selfDict
139
140         @property
141         def conversations(self):
142                 return self._conversations
143
144         def _validate(self, newConversation):
145                 if not self._conversations:
146                         return
147
148                 for constantField in ("number", ):
149                         assert getattr(self._conversations[0], constantField) == getattr(newConversation, constantField), "Constant field changed, soemthing is seriously messed up: %r v %r" % (
150                                 getattr(self._conversations[0], constantField),
151                                 getattr(newConversation, constantField),
152                         )
153
154                 if newConversation.time <= self._conversations[-1].time:
155                         raise RuntimeError("Conversations got out of order")
156
157         def _find_related_conversation(self, convId):
158                 similarConversations = (
159                         conversation
160                         for conversation in self._conversations
161                         if conversation.id == convId
162                 )
163                 return similarConversations
164
165         def _update_previous_related_conversation(self, relatedConversation, newConversation):
166                 for commonField in ("isSpam", "isTrash", "isArchived"):
167                         newValue = getattr(newConversation, commonField)
168                         setattr(relatedConversation, commonField, newValue)
169
170         def _remove_repeats(self, relatedConversation, newConversation):
171                 newConversationMessages = newConversation.messages
172                 newConversation.messages = [
173                         newMessage
174                         for newMessage in newConversationMessages
175                         if newMessage not in relatedConversation.messages
176                 ]
177                 _moduleLogger.debug("Found %d new messages in conversation %s (%d/%d)" % (
178                         len(newConversationMessages) - len(newConversation.messages),
179                         newConversation.id,
180                         len(newConversation.messages),
181                         len(newConversationMessages),
182                 ))
183                 assert 0 < len(newConversation.messages), "Everything shouldn't have been removed"
184
185
186 def filter_out_read(conversations):
187         return (
188                 conversation
189                 for conversation in conversations
190                 if not conversation.isRead and not conversation.isArchived
191         )
192
193
194 def is_message_from_self(message):
195         return message.whoFrom == "Me:"
196
197
198 def filter_out_self(conversations):
199         return (
200                 newConversation
201                 for newConversation in conversations
202                 if len(newConversation.messages) and any(
203                         not is_message_from_self(message)
204                         for message in newConversation.messages
205                 )
206         )
207
208
209 class FilterOutReported(object):
210
211         NULL_TIMESTAMP = datetime.datetime(1, 1, 1)
212
213         def __init__(self):
214                 self._lastMessageTimestamp = self.NULL_TIMESTAMP
215
216         def get_last_timestamp(self):
217                 return self._lastMessageTimestamp
218
219         def __call__(self, conversations):
220                 filteredConversations = [
221                         conversation
222                         for conversation in conversations
223                         if self._lastMessageTimestamp < conversation.time
224                 ]
225                 if filteredConversations and self._lastMessageTimestamp < filteredConversations[0].time:
226                         self._lastMessageTimestamp = filteredConversations[0].time
227                 return filteredConversations