Fixing making of calls
[gc-dialer] / src / backends / gv_backend.py
1 #!/usr/bin/python
2
3 """
4 DialCentral - Front end for Google's GoogleVoice service.
5 Copyright (C) 2008  Eric Warnke ericew AT gmail DOT com
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 Lesser General Public License for more details.
16
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
20
21 Google Voice backend code
22
23 Resources
24         http://thatsmith.com/2009/03/google-voice-addon-for-firefox/
25         http://posttopic.com/topic/google-voice-add-on-development
26 """
27
28 from __future__ import with_statement
29
30 import os
31 import re
32 import urllib
33 import urllib2
34 import time
35 import datetime
36 import itertools
37 import logging
38 from xml.sax import saxutils
39
40 from xml.etree import ElementTree
41
42 try:
43         import simplejson as _simplejson
44         simplejson = _simplejson
45 except ImportError:
46         simplejson = None
47
48 import browser_emu
49
50
51 _moduleLogger = logging.getLogger("gvoice.dialer")
52
53
54 class NetworkError(RuntimeError):
55         pass
56
57
58 class GVDialer(object):
59         """
60         This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
61         the functions include login, setting up a callback number, and initalting a callback
62         """
63
64         PHONE_TYPE_HOME = 1
65         PHONE_TYPE_MOBILE = 2
66         PHONE_TYPE_WORK = 3
67         PHONE_TYPE_GIZMO = 7
68
69         def __init__(self, cookieFile = None):
70                 # Important items in this function are the setup of the browser emulation and cookie file
71                 self._browser = browser_emu.MozillaEmulator(1)
72                 self._loadedFromCookies = self._browser.load_cookies(cookieFile)
73
74                 self._token = ""
75                 self._accountNum = ""
76                 self._lastAuthed = 0.0
77                 self._callbackNumber = ""
78                 self._callbackNumbers = {}
79
80                 # Suprisingly, moving all of these from class to self sped up startup time
81
82                 self._validateRe = re.compile("^[0-9]{10,}$")
83
84                 self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
85
86                 SECURE_URL_BASE = "https://www.google.com/voice/"
87                 SECURE_MOBILE_URL_BASE = SECURE_URL_BASE + "mobile/"
88                 self._forwardURL = SECURE_MOBILE_URL_BASE + "phones"
89                 self._tokenURL = SECURE_URL_BASE + "m"
90                 self._callUrl = SECURE_URL_BASE + "call/connect"
91                 self._callCancelURL = SECURE_URL_BASE + "call/cancel"
92                 self._sendSmsURL = SECURE_URL_BASE + "sms/send"
93
94                 self._isDndURL = "https://www.google.com/voice/m/donotdisturb"
95                 self._isDndRe = re.compile(r"""<input.*?id="doNotDisturb".*?checked="(.*?)"\s*/>""")
96                 self._setDndURL = "https://www.google.com/voice/m/savednd"
97
98                 self._downloadVoicemailURL = SECURE_URL_BASE + "media/send_voicemail/"
99
100                 self._XML_SEARCH_URL = SECURE_URL_BASE + "inbox/search/"
101                 self._XML_ACCOUNT_URL = SECURE_URL_BASE + "inbox/contacts/"
102                 self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
103
104                 self.XML_FEEDS = (
105                         'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms',
106                         'recorded', 'placed', 'received', 'missed'
107                 )
108                 self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox"
109                 self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred"
110                 self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all"
111                 self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam"
112                 self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash"
113                 self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/"
114                 self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/"
115                 self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/"
116                 self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/"
117                 self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
118                 self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
119
120                 self._contactsURL = SECURE_MOBILE_URL_BASE + "contacts"
121                 self._contactDetailURL = SECURE_MOBILE_URL_BASE + "contact"
122
123                 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
124                 self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
125                 self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
126                 self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
127
128                 self._contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
129                 self._contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
130                 self._contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
131
132                 self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
133                 self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
134                 self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
135                 self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
136                 self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
137                 self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
138                 self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
139                 self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
140                 self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
141                 self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
142                 self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
143                 self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
144
145         def is_quick_login_possible(self):
146                 """
147                 @returns True then is_authed might be enough to login, else full login is required
148                 """
149                 return self._loadedFromCookies or 0.0 < self._lastAuthed
150
151         def is_authed(self, force = False):
152                 """
153                 Attempts to detect a current session
154                 @note Once logged in try not to reauth more than once a minute.
155                 @returns If authenticated
156                 """
157                 isRecentledAuthed = (time.time() - self._lastAuthed) < 120
158                 isPreviouslyAuthed = self._token is not None
159                 if isRecentledAuthed and isPreviouslyAuthed and not force:
160                         return True
161
162                 try:
163                         page = self._get_page(self._forwardURL)
164                         self._grab_account_info(page)
165                 except Exception, e:
166                         _moduleLogger.exception(str(e))
167                         return False
168
169                 self._browser.save_cookies()
170                 self._lastAuthed = time.time()
171                 return True
172
173         def _get_token(self):
174                 tokenPage = self._get_page(self._tokenURL)
175
176                 galxTokens = self._galxRe.search(tokenPage)
177                 if galxTokens is not None:
178                         galxToken = galxTokens.group(1)
179                 else:
180                         galxToken = ""
181                         _moduleLogger.debug("Could not grab GALX token")
182                 return galxToken
183
184         def _login(self, username, password, token):
185                 loginData = {
186                         'Email' : username,
187                         'Passwd' : password,
188                         'service': "grandcentral",
189                         "ltmpl": "mobile",
190                         "btmpl": "mobile",
191                         "PersistentCookie": "yes",
192                         "GALX": token,
193                         "continue": self._forwardURL,
194                 }
195
196                 loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
197                 return loginSuccessOrFailurePage
198
199         def login(self, username, password):
200                 """
201                 Attempt to login to GoogleVoice
202                 @returns Whether login was successful or not
203                 """
204                 self.logout()
205                 galxToken = self._get_token()
206                 loginSuccessOrFailurePage = self._login(username, password, galxToken)
207
208                 try:
209                         self._grab_account_info(loginSuccessOrFailurePage)
210                 except Exception, e:
211                         # Retry in case the redirect failed
212                         # luckily is_authed does everything we need for a retry
213                         loggedIn = self.is_authed(True)
214                         if not loggedIn:
215                                 _moduleLogger.exception(str(e))
216                                 return False
217                         _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
218
219                 self._browser.save_cookies()
220                 self._lastAuthed = time.time()
221                 return True
222
223         def logout(self):
224                 self._browser.clear_cookies()
225                 self._browser.save_cookies()
226                 self._token = None
227                 self._lastAuthed = 0.0
228
229         def is_dnd(self):
230                 isDndPage = self._get_page(self._isDndURL)
231
232                 dndGroup = self._isDndRe.search(isDndPage)
233                 if dndGroup is None:
234                         return False
235                 dndStatus = dndGroup.group(1)
236                 isDnd = True if dndStatus.strip().lower() == "true" else False
237                 return isDnd
238
239         def set_dnd(self, doNotDisturb):
240                 dndPostData = {
241                         "doNotDisturb": 1 if doNotDisturb else 0,
242                         "_rnr_se": self._token,
243                 }
244
245                 dndPage = self._get_page(self._setDndURL, dndPostData)
246
247         def call(self, outgoingNumber):
248                 """
249                 This is the main function responsible for initating the callback
250                 """
251                 outgoingNumber = self._send_validation(outgoingNumber)
252                 subscriberNumber = None
253                 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
254
255                 callData = {
256                                 'outgoingNumber': outgoingNumber,
257                                 'forwardingNumber': self._callbackNumber,
258                                 'subscriberNumber': subscriberNumber or 'undefined',
259                                 'phoneType': str(phoneType),
260                                 'remember': '1',
261                 }
262                 _moduleLogger.info("%r" % callData)
263
264                 page = self._get_page_with_token(
265                         self._callUrl,
266                         callData,
267                 )
268                 self._parse_with_validation(page)
269                 return True
270
271         def cancel(self, outgoingNumber=None):
272                 """
273                 Cancels a call matching outgoing and forwarding numbers (if given). 
274                 Will raise an error if no matching call is being placed
275                 """
276                 page = self._get_page_with_token(
277                         self._callCancelURL,
278                         {
279                         'outgoingNumber': outgoingNumber or 'undefined',
280                         'forwardingNumber': self._callbackNumber or 'undefined',
281                         'cancelType': 'C2C',
282                         },
283                 )
284                 self._parse_with_validation(page)
285
286         def send_sms(self, phoneNumber, message):
287                 phoneNumber = self._send_validation(phoneNumber)
288                 page = self._get_page_with_token(
289                         self._sendSmsURL,
290                         {
291                                 'phoneNumber': phoneNumber,
292                                 'text': message
293                         },
294                 )
295                 self._parse_with_validation(page)
296
297         def search(self, query):
298                 """
299                 Search your Google Voice Account history for calls, voicemails, and sms
300                 Returns ``Folder`` instance containting matching messages
301                 """
302                 page = self._get_page(
303                         self._XML_SEARCH_URL,
304                         {"q": query},
305                 )
306                 json, html = extract_payload(page)
307                 return json
308
309         def get_feed(self, feed):
310                 actualFeed = "_XML_%s_URL" % feed.upper()
311                 feedUrl = getattr(self, actualFeed)
312
313                 page = self._get_page(feedUrl)
314                 json, html = extract_payload(page)
315
316                 return json
317
318         def download(self, messageId, adir):
319                 """
320                 Download a voicemail or recorded call MP3 matching the given ``msg``
321                 which can either be a ``Message`` instance, or a SHA1 identifier. 
322                 Saves files to ``adir`` (defaults to current directory). 
323                 Message hashes can be found in ``self.voicemail().messages`` for example. 
324                 Returns location of saved file.
325                 """
326                 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
327                 fn = os.path.join(adir, '%s.mp3' % messageId)
328                 with open(fn, 'wb') as fo:
329                         fo.write(page)
330                 return fn
331
332         def is_valid_syntax(self, number):
333                 """
334                 @returns If This number be called ( syntax validation only )
335                 """
336                 return self._validateRe.match(number) is not None
337
338         def get_account_number(self):
339                 """
340                 @returns The GoogleVoice phone number
341                 """
342                 return self._accountNum
343
344         def get_callback_numbers(self):
345                 """
346                 @returns a dictionary mapping call back numbers to descriptions
347                 @note These results are cached for 30 minutes.
348                 """
349                 if not self.is_authed():
350                         return {}
351                 return self._callbackNumbers
352
353         def set_callback_number(self, callbacknumber):
354                 """
355                 Set the number that GoogleVoice calls
356                 @param callbacknumber should be a proper 10 digit number
357                 """
358                 self._callbackNumber = callbacknumber
359                 return True
360
361         def get_callback_number(self):
362                 """
363                 @returns Current callback number or None
364                 """
365                 return self._callbackNumber
366
367         def get_recent(self):
368                 """
369                 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
370                 """
371                 for action, url in (
372                         ("Received", self._XML_RECEIVED_URL),
373                         ("Missed", self._XML_MISSED_URL),
374                         ("Placed", self._XML_PLACED_URL),
375                 ):
376                         flatXml = self._get_page(url)
377
378                         allRecentHtml = self._grab_html(flatXml)
379                         allRecentData = self._parse_voicemail(allRecentHtml)
380                         for recentCallData in allRecentData:
381                                 recentCallData["action"] = action
382                                 yield recentCallData
383
384         def get_contacts(self):
385                 """
386                 @returns Iterable of (contact id, contact name)
387                 """
388                 contactsPagesUrls = [self._contactsURL]
389                 for contactsPageUrl in contactsPagesUrls:
390                         contactsPage = self._get_page(contactsPageUrl)
391                         for contact_match in self._contactsRe.finditer(contactsPage):
392                                 contactId = contact_match.group(1)
393                                 contactName = saxutils.unescape(contact_match.group(2))
394                                 contact = contactId, contactName
395                                 yield contact
396
397                         next_match = self._contactsNextRe.match(contactsPage)
398                         if next_match is not None:
399                                 newContactsPageUrl = self._contactsURL + next_match.group(1)
400                                 contactsPagesUrls.append(newContactsPageUrl)
401
402         def get_contact_details(self, contactId):
403                 """
404                 @returns Iterable of (Phone Type, Phone Number)
405                 """
406                 detailPage = self._get_page(self._contactDetailURL + '/' + contactId)
407
408                 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
409                         phoneNumber = detail_match.group(1)
410                         phoneType = saxutils.unescape(detail_match.group(2))
411                         yield (phoneType, phoneNumber)
412
413         def get_messages(self):
414                 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
415                 voicemailHtml = self._grab_html(voicemailPage)
416                 voicemailJson = self._grab_json(voicemailPage)
417                 parsedVoicemail = self._parse_voicemail(voicemailHtml)
418                 voicemails = self._merge_messages(parsedVoicemail, voicemailJson)
419                 decoratedVoicemails = self._decorate_voicemail(voicemails)
420
421                 smsPage = self._get_page(self._XML_SMS_URL)
422                 smsHtml = self._grab_html(smsPage)
423                 smsJson = self._grab_json(smsPage)
424                 parsedSms = self._parse_sms(smsHtml)
425                 smss = self._merge_messages(parsedSms, smsJson)
426                 decoratedSms = self._decorate_sms(smss)
427
428                 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
429                 return allMessages
430
431         def clear_caches(self):
432                 pass
433
434         def get_addressbooks(self):
435                 """
436                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
437                 """
438                 yield self, "", ""
439
440         def open_addressbook(self, bookId):
441                 return self
442
443         @staticmethod
444         def contact_source_short_name(contactId):
445                 return "GV"
446
447         @staticmethod
448         def factory_name():
449                 return "Google Voice"
450
451         def _grab_json(self, flatXml):
452                 xmlTree = ElementTree.fromstring(flatXml)
453                 jsonElement = xmlTree.getchildren()[0]
454                 flatJson = jsonElement.text
455                 jsonTree = parse_json(flatJson)
456                 return jsonTree
457
458         def _grab_html(self, flatXml):
459                 xmlTree = ElementTree.fromstring(flatXml)
460                 htmlElement = xmlTree.getchildren()[1]
461                 flatHtml = htmlElement.text
462                 return flatHtml
463
464         def _grab_account_info(self, page):
465                 tokenGroup = self._tokenRe.search(page)
466                 if tokenGroup is None:
467                         raise RuntimeError("Could not extract authentication token from GoogleVoice")
468                 self._token = tokenGroup.group(1)
469
470                 anGroup = self._accountNumRe.search(page)
471                 if anGroup is not None:
472                         self._accountNum = anGroup.group(1)
473                 else:
474                         _moduleLogger.debug("Could not extract account number from GoogleVoice")
475
476                 self._callbackNumbers = {}
477                 for match in self._callbackRe.finditer(page):
478                         callbackNumber = match.group(2)
479                         callbackName = match.group(1)
480                         self._callbackNumbers[callbackNumber] = callbackName
481                 if len(self._callbackNumbers) == 0:
482                         _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
483
484         def _send_validation(self, number):
485                 if not self.is_valid_syntax(number):
486                         raise ValueError('Number is not valid: "%s"' % number)
487                 elif not self.is_authed():
488                         raise RuntimeError("Not Authenticated")
489
490                 if len(number) == 11 and number[0] == 1:
491                         # Strip leading 1 from 11 digit dialing
492                         number = number[1:]
493                 return number
494
495         @staticmethod
496         def _interpret_voicemail_regex(group):
497                 quality, content, number = group.group(2), group.group(3), group.group(4)
498                 if quality is not None and content is not None:
499                         return quality, content
500                 elif number is not None:
501                         return "high", number
502
503         def _parse_voicemail(self, voicemailHtml):
504                 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
505                 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
506                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
507                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
508                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
509                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
510                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
511                         locationGroup = self._voicemailLocationRegex.search(messageHtml)
512                         location = locationGroup.group(1).strip() if locationGroup else ""
513
514                         nameGroup = self._voicemailNameRegex.search(messageHtml)
515                         name = nameGroup.group(1).strip() if nameGroup else ""
516                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
517                         number = numberGroup.group(1).strip() if numberGroup else ""
518                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
519                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
520                         contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
521                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
522
523                         messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
524                         messageParts = (
525                                 self._interpret_voicemail_regex(group)
526                                 for group in messageGroups
527                         ) if messageGroups else ()
528
529                         yield {
530                                 "id": messageId.strip(),
531                                 "contactId": contactId,
532                                 "name": name,
533                                 "time": exactTime,
534                                 "relTime": relativeTime,
535                                 "prettyNumber": prettyNumber,
536                                 "number": number,
537                                 "location": location,
538                                 "messageParts": messageParts,
539                                 "type": "Voicemail",
540                         }
541
542         def _decorate_voicemail(self, parsedVoicemails):
543                 messagePartFormat = {
544                         "med1": "<i>%s</i>",
545                         "med2": "%s",
546                         "high": "<b>%s</b>",
547                 }
548                 for voicemailData in parsedVoicemails:
549                         message = " ".join((
550                                 messagePartFormat[quality] % part
551                                 for (quality, part) in voicemailData["messageParts"]
552                         )).strip()
553                         if not message:
554                                 message = "No Transcription"
555                         whoFrom = voicemailData["name"]
556                         when = voicemailData["time"]
557                         voicemailData["messageParts"] = ((whoFrom, message, when), )
558                         yield voicemailData
559
560         def _parse_sms(self, smsHtml):
561                 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
562                 for messageId, messageHtml in itergroup(splitSms[1:], 2):
563                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
564                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
565                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
566                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
567                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
568
569                         nameGroup = self._voicemailNameRegex.search(messageHtml)
570                         name = nameGroup.group(1).strip() if nameGroup else ""
571                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
572                         number = numberGroup.group(1).strip() if numberGroup else ""
573                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
574                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
575                         contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
576                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
577
578                         fromGroups = self._smsFromRegex.finditer(messageHtml)
579                         fromParts = (group.group(1).strip() for group in fromGroups)
580                         textGroups = self._smsTextRegex.finditer(messageHtml)
581                         textParts = (group.group(1).strip() for group in textGroups)
582                         timeGroups = self._smsTimeRegex.finditer(messageHtml)
583                         timeParts = (group.group(1).strip() for group in timeGroups)
584
585                         messageParts = itertools.izip(fromParts, textParts, timeParts)
586
587                         yield {
588                                 "id": messageId.strip(),
589                                 "contactId": contactId,
590                                 "name": name,
591                                 "time": exactTime,
592                                 "relTime": relativeTime,
593                                 "prettyNumber": prettyNumber,
594                                 "number": number,
595                                 "location": "",
596                                 "messageParts": messageParts,
597                                 "type": "Texts",
598                         }
599
600         def _decorate_sms(self, parsedTexts):
601                 return parsedTexts
602
603         @staticmethod
604         def _merge_messages(parsedMessages, json):
605                 for message in parsedMessages:
606                         id = message["id"]
607                         jsonItem = json["messages"][id]
608                         message["isRead"] = jsonItem["isRead"]
609                         message["isSpam"] = jsonItem["isSpam"]
610                         message["isTrash"] = jsonItem["isTrash"]
611                         message["isArchived"] = "inbox" not in jsonItem["labels"]
612                         yield message
613
614         def _get_page(self, url, data = None, refererUrl = None):
615                 headers = {}
616                 if refererUrl is not None:
617                         headers["Referer"] = refererUrl
618
619                 encodedData = urllib.urlencode(data) if data is not None else None
620
621                 try:
622                         page = self._browser.download(url, encodedData, None, headers)
623                 except urllib2.URLError, e:
624                         _moduleLogger.error("Translating error: %s" % str(e))
625                         raise NetworkError("%s is not accesible" % url)
626
627                 return page
628
629         def _get_page_with_token(self, url, data = None, refererUrl = None):
630                 if data is None:
631                         data = {}
632                 data['_rnr_se'] = self._token
633
634                 page = self._get_page(url, data, refererUrl)
635
636                 return page
637
638         def _parse_with_validation(self, page):
639                 json = parse_json(page)
640                 validate_response(json)
641                 return json
642
643
644 def itergroup(iterator, count, padValue = None):
645         """
646         Iterate in groups of 'count' values. If there
647         aren't enough values, the last result is padded with
648         None.
649
650         >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
651         ...     print tuple(val)
652         (1, 2, 3)
653         (4, 5, 6)
654         >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
655         ...     print list(val)
656         [1, 2, 3]
657         [4, 5, 6]
658         >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
659         ...     print tuple(val)
660         (1, 2, 3)
661         (4, 5, 6)
662         (7, None, None)
663         >>> for val in itergroup("123456", 3):
664         ...     print tuple(val)
665         ('1', '2', '3')
666         ('4', '5', '6')
667         >>> for val in itergroup("123456", 3):
668         ...     print repr("".join(val))
669         '123'
670         '456'
671         """
672         paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
673         nIterators = (paddedIterator, ) * count
674         return itertools.izip(*nIterators)
675
676
677 def safe_eval(s):
678         _TRUE_REGEX = re.compile("true")
679         _FALSE_REGEX = re.compile("false")
680         s = _TRUE_REGEX.sub("True", s)
681         s = _FALSE_REGEX.sub("False", s)
682         return eval(s, {}, {})
683
684
685 def _fake_parse_json(flattened):
686         return safe_eval(flattened)
687
688
689 def _actual_parse_json(flattened):
690         return simplejson.loads(flattened)
691
692
693 if simplejson is None:
694         parse_json = _fake_parse_json
695 else:
696         parse_json = _actual_parse_json
697
698
699 def extract_payload(flatXml):
700         xmlTree = ElementTree.fromstring(flatXml)
701
702         jsonElement = xmlTree.getchildren()[0]
703         flatJson = jsonElement.text
704         jsonTree = parse_json(flatJson)
705
706         htmlElement = xmlTree.getchildren()[1]
707         flatHtml = htmlElement.text
708
709         return jsonTree, flatHtml
710
711
712 def validate_response(response):
713         """
714         Validates that the JSON response is A-OK
715         """
716         try:
717                 assert 'ok' in response and response['ok']
718         except AssertionError:
719                 raise RuntimeError('There was a problem with GV: %s' % response)
720
721
722 def guess_phone_type(number):
723         if number.startswith("747") or number.startswith("1747"):
724                 return GVDialer.PHONE_TYPE_GIZMO
725         else:
726                 return GVDialer.PHONE_TYPE_MOBILE
727
728
729 def set_sane_callback(backend):
730         """
731         Try to set a sane default callback number on these preferences
732         1) 1747 numbers ( Gizmo )
733         2) anything with gizmo in the name
734         3) anything with computer in the name
735         4) the first value
736         """
737         numbers = backend.get_callback_numbers()
738
739         priorityOrderedCriteria = [
740                 ("1747", None),
741                 (None, "gizmo"),
742                 (None, "computer"),
743                 (None, "sip"),
744                 (None, None),
745         ]
746
747         for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
748                 for number, description in numbers.iteritems():
749                         if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
750                                 continue
751                         if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
752                                 continue
753                         backend.set_callback_number(number)
754                         return
755
756
757 def sort_messages(allMessages):
758         sortableAllMessages = [
759                 (message["time"], message)
760                 for message in allMessages
761         ]
762         sortableAllMessages.sort(reverse=True)
763         return (
764                 message
765                 for (exactTime, message) in sortableAllMessages
766         )
767
768
769 def decorate_recent(recentCallData):
770         """
771         @returns (personsName, phoneNumber, date, action)
772         """
773         contactId = recentCallData["contactId"]
774         if recentCallData["name"]:
775                 header = recentCallData["name"]
776         elif recentCallData["prettyNumber"]:
777                 header = recentCallData["prettyNumber"]
778         elif recentCallData["location"]:
779                 header = recentCallData["location"]
780         else:
781                 header = "Unknown"
782
783         number = recentCallData["number"]
784         relTime = recentCallData["relTime"]
785         action = recentCallData["action"]
786         return contactId, header, number, relTime, action
787
788
789 def decorate_message(messageData):
790         contactId = messageData["contactId"]
791         exactTime = messageData["time"]
792         if messageData["name"]:
793                 header = messageData["name"]
794         elif messageData["prettyNumber"]:
795                 header = messageData["prettyNumber"]
796         else:
797                 header = "Unknown"
798         number = messageData["number"]
799         relativeTime = messageData["relTime"]
800
801         messageParts = list(messageData["messageParts"])
802         if len(messageParts) == 0:
803                 messages = ("No Transcription", )
804         elif len(messageParts) == 1:
805                 messages = (messageParts[0][1], )
806         else:
807                 messages = [
808                         "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
809                         for messagePart in messageParts
810                 ]
811
812         decoratedResults = contactId, header, number, relativeTime, messages
813         return decoratedResults
814
815
816 def test_backend(username, password):
817         backend = GVDialer()
818         print "Authenticated: ", backend.is_authed()
819         if not backend.is_authed():
820                 print "Login?: ", backend.login(username, password)
821         print "Authenticated: ", backend.is_authed()
822         print "Is Dnd: ", backend.is_dnd()
823         #print "Setting Dnd", backend.set_dnd(True)
824         #print "Is Dnd: ", backend.is_dnd()
825         #print "Setting Dnd", backend.set_dnd(False)
826         #print "Is Dnd: ", backend.is_dnd()
827
828         #print "Token: ", backend._token
829         #print "Account: ", backend.get_account_number()
830         #print "Callback: ", backend.get_callback_number()
831         #print "All Callback: ",
832         import pprint
833         #pprint.pprint(backend.get_callback_numbers())
834
835         #print "Recent: "
836         #for data in backend.get_recent():
837         #       pprint.pprint(data)
838         #for data in sort_messages(backend.get_recent()):
839         #       pprint.pprint(decorate_recent(data))
840         #pprint.pprint(list(backend.get_recent()))
841
842         #print "Contacts: ",
843         #for contact in backend.get_contacts():
844         #       print contact
845         #       pprint.pprint(list(backend.get_contact_details(contact[0])))
846
847         #print "Messages: ",
848         #for message in backend.get_messages():
849         #       message["messageParts"] = list(message["messageParts"])
850         #       pprint.pprint(message)
851         #for message in sort_messages(backend.get_messages()):
852         #       pprint.pprint(decorate_message(message))
853
854         return backend
855
856
857 def grab_debug_info(username, password):
858         cookieFile = os.path.join(".", "raw_cookies.txt")
859         try:
860                 os.remove(cookieFile)
861         except OSError:
862                 pass
863
864         backend = GVDialer(cookieFile)
865         browser = backend._browser
866
867         _TEST_WEBPAGES = [
868                 ("forward", backend._forwardURL),
869                 ("token", backend._tokenURL),
870                 ("login", backend._loginURL),
871                 ("isdnd", backend._isDndURL),
872                 ("contacts", backend._contactsURL),
873
874                 ("account", backend._XML_ACCOUNT_URL),
875                 ("voicemail", backend._XML_VOICEMAIL_URL),
876                 ("sms", backend._XML_SMS_URL),
877
878                 ("recent", backend._XML_RECENT_URL),
879                 ("placed", backend._XML_PLACED_URL),
880                 ("recieved", backend._XML_RECEIVED_URL),
881                 ("missed", backend._XML_MISSED_URL),
882         ]
883
884         # Get Pages
885         print "Grabbing pre-login pages"
886         for name, url in _TEST_WEBPAGES:
887                 try:
888                         page = browser.download(url)
889                 except StandardError, e:
890                         print e.message
891                         continue
892                 print "\tWriting to file"
893                 with open("not_loggedin_%s.txt" % name, "w") as f:
894                         f.write(page)
895
896         # Login
897         print "Attempting login"
898         galxToken = backend._get_token()
899         loginSuccessOrFailurePage = backend._login(username, password, galxToken)
900         with open("loggingin.txt", "w") as f:
901                 print "\tWriting to file"
902                 f.write(loginSuccessOrFailurePage)
903         try:
904                 backend._grab_account_info(loginSuccessOrFailurePage)
905         except Exception:
906                 # Retry in case the redirect failed
907                 # luckily is_authed does everything we need for a retry
908                 loggedIn = backend.is_authed(True)
909                 if not loggedIn:
910                         raise
911
912         # Get Pages
913         print "Grabbing post-login pages"
914         for name, url in _TEST_WEBPAGES:
915                 try:
916                         page = browser.download(url)
917                 except StandardError, e:
918                         print e.message
919                         continue
920                 print "\tWriting to file"
921                 with open("loggedin_%s.txt" % name, "w") as f:
922                         f.write(page)
923
924         # Cookies
925         browser.cookies.save()
926         print "\tWriting cookies to file"
927         with open("cookies.txt", "w") as f:
928                 f.writelines(
929                         "%s: %s\n" % (c.name, c.value)
930                         for c in browser.cookies
931                 )
932
933
934 if __name__ == "__main__":
935         import sys
936         logging.basicConfig(level=logging.DEBUG)
937         if True:
938                 grab_debug_info(sys.argv[1], sys.argv[2])
939         else:
940                 test_backend(sys.argv[1], sys.argv[2])