4 DialCentral - Front end for Google's GoogleVoice service.
5 Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com
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.
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.
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
21 Google Voice backend code
24 http://thatsmith.com/2009/03/google-voice-addon-for-firefox/
25 http://posttopic.com/topic/google-voice-add-on-development
28 from __future__ import with_statement
40 from xml.sax import saxutils
41 from xml.etree import ElementTree
44 import simplejson as _simplejson
45 simplejson = _simplejson
52 _moduleLogger = logging.getLogger(__name__)
55 class NetworkError(RuntimeError):
59 class MessageText(object):
62 ACCURACY_MEDIUM = "med2"
63 ACCURACY_HIGH = "high"
75 def __eq__(self, other):
76 return self.accuracy == other.accuracy and self.text == other.text
79 class Message(object):
87 return "%s (%s): %s" % (
90 "".join(str(part) for part in self.body)
94 selfDict = to_dict(self)
95 selfDict["body"] = [text.to_dict() for text in self.body] if self.body is not None else None
98 def __eq__(self, other):
99 return self.whoFrom == other.whoFrom and self.when == other.when and self.body == other.body
102 class Conversation(object):
104 TYPE_VOICEMAIL = "Voicemail"
110 self.contactId = None
113 self.prettyNumber = None
122 self.isArchived = None
124 def __cmp__(self, other):
125 cmpValue = cmp(self.contactId, other.contactId)
129 cmpValue = cmp(self.time, other.time)
133 cmpValue = cmp(self.id, other.id)
138 selfDict = to_dict(self)
139 selfDict["messages"] = [message.to_dict() for message in self.messages] if self.messages is not None else None
143 class GVoiceBackend(object):
145 This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
146 the functions include login, setting up a callback number, and initalting a callback
150 PHONE_TYPE_MOBILE = 2
154 def __init__(self, cookieFile = None):
155 # Important items in this function are the setup of the browser emulation and cookie file
156 self._browser = browser_emu.MozillaEmulator(1)
157 self._loadedFromCookies = self._browser.load_cookies(cookieFile)
160 self._accountNum = ""
161 self._lastAuthed = 0.0
162 self._callbackNumber = ""
163 self._callbackNumbers = {}
165 # Suprisingly, moving all of these from class to self sped up startup time
167 self._validateRe = re.compile("^\+?[0-9]{10,}$")
169 self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
171 SECURE_URL_BASE = "https://www.google.com/voice/"
172 SECURE_MOBILE_URL_BASE = SECURE_URL_BASE + "mobile/"
173 self._forwardURL = SECURE_MOBILE_URL_BASE + "phones"
174 self._tokenURL = SECURE_URL_BASE + "m"
175 self._callUrl = SECURE_URL_BASE + "call/connect"
176 self._callCancelURL = SECURE_URL_BASE + "call/cancel"
177 self._sendSmsURL = SECURE_URL_BASE + "sms/send"
179 self._isDndURL = "https://www.google.com/voice/m/donotdisturb"
180 self._isDndRe = re.compile(r"""<input.*?id="doNotDisturb".*?checked="(.*?)"\s*/>""")
181 self._setDndURL = "https://www.google.com/voice/m/savednd"
183 self._downloadVoicemailURL = SECURE_URL_BASE + "media/send_voicemail/"
184 self._markAsReadURL = SECURE_URL_BASE + "m/mark"
185 self._archiveMessageURL = SECURE_URL_BASE + "m/archive"
187 self._XML_SEARCH_URL = SECURE_URL_BASE + "inbox/search/"
188 self._XML_ACCOUNT_URL = SECURE_URL_BASE + "contacts/"
189 # HACK really this redirects to the main pge and we are grabbing some javascript
190 self._XML_CONTACTS_URL = "http://www.google.com/voice/inbox/search/contact"
191 self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
194 'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms',
195 'recorded', 'placed', 'received', 'missed'
197 self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox"
198 self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred"
199 self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all"
200 self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam"
201 self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash"
202 self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/"
203 self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/"
204 self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/"
205 self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/"
206 self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
207 self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
209 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
210 self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
211 self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
212 self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
214 self._contactsBodyRe = re.compile(r"""gcData\s*=\s*({.*?});""", re.MULTILINE | re.DOTALL)
215 self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
216 self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
217 self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
218 self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
219 self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
220 self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
221 self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
222 self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
223 self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
224 self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
225 self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
226 self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
228 def is_quick_login_possible(self):
230 @returns True then is_authed might be enough to login, else full login is required
232 return self._loadedFromCookies or 0.0 < self._lastAuthed
234 def is_authed(self, force = False):
236 Attempts to detect a current session
237 @note Once logged in try not to reauth more than once a minute.
238 @returns If authenticated
241 isRecentledAuthed = (time.time() - self._lastAuthed) < 120
242 isPreviouslyAuthed = self._token is not None
243 if isRecentledAuthed and isPreviouslyAuthed and not force:
247 page = self._get_page(self._forwardURL)
248 self._grab_account_info(page)
250 _moduleLogger.exception(str(e))
253 self._browser.save_cookies()
254 self._lastAuthed = time.time()
257 def _get_token(self):
258 tokenPage = self._get_page(self._tokenURL)
260 galxTokens = self._galxRe.search(tokenPage)
261 if galxTokens is not None:
262 galxToken = galxTokens.group(1)
265 _moduleLogger.debug("Could not grab GALX token")
268 def _login(self, username, password, token):
272 'service': "grandcentral",
275 "PersistentCookie": "yes",
277 "continue": self._forwardURL,
280 loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
281 return loginSuccessOrFailurePage
283 def login(self, username, password):
285 Attempt to login to GoogleVoice
286 @returns Whether login was successful or not
290 galxToken = self._get_token()
291 loginSuccessOrFailurePage = self._login(username, password, galxToken)
294 self._grab_account_info(loginSuccessOrFailurePage)
296 # Retry in case the redirect failed
297 # luckily is_authed does everything we need for a retry
298 loggedIn = self.is_authed(True)
300 _moduleLogger.exception(str(e))
302 _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
304 self._browser.save_cookies()
305 self._lastAuthed = time.time()
309 self._browser.save_cookies()
312 self._browser.save_cookies()
314 self._lastAuthed = 0.0
317 self._browser.clear_cookies()
318 self._browser.save_cookies()
320 self._lastAuthed = 0.0
326 isDndPage = self._get_page(self._isDndURL)
328 dndGroup = self._isDndRe.search(isDndPage)
331 dndStatus = dndGroup.group(1)
332 isDnd = True if dndStatus.strip().lower() == "true" else False
335 def set_dnd(self, doNotDisturb):
340 "doNotDisturb": 1 if doNotDisturb else 0,
343 dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
345 def call(self, outgoingNumber):
347 This is the main function responsible for initating the callback
350 outgoingNumber = self._send_validation(outgoingNumber)
351 subscriberNumber = None
352 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
355 'outgoingNumber': outgoingNumber,
356 'forwardingNumber': self._callbackNumber,
357 'subscriberNumber': subscriberNumber or 'undefined',
358 'phoneType': str(phoneType),
361 _moduleLogger.info("%r" % callData)
363 page = self._get_page_with_token(
367 self._parse_with_validation(page)
370 def cancel(self, outgoingNumber=None):
372 Cancels a call matching outgoing and forwarding numbers (if given).
373 Will raise an error if no matching call is being placed
376 page = self._get_page_with_token(
379 'outgoingNumber': outgoingNumber or 'undefined',
380 'forwardingNumber': self._callbackNumber or 'undefined',
384 self._parse_with_validation(page)
386 def send_sms(self, phoneNumbers, message):
390 validatedPhoneNumbers = [
391 self._send_validation(phoneNumber)
392 for phoneNumber in phoneNumbers
394 flattenedPhoneNumbers = ",".join(validatedPhoneNumbers)
395 page = self._get_page_with_token(
398 'phoneNumber': flattenedPhoneNumbers,
402 self._parse_with_validation(page)
404 def search(self, query):
406 Search your Google Voice Account history for calls, voicemails, and sms
407 Returns ``Folder`` instance containting matching messages
410 page = self._get_page(
411 self._XML_SEARCH_URL,
414 json, html = extract_payload(page)
417 def get_feed(self, feed):
421 actualFeed = "_XML_%s_URL" % feed.upper()
422 feedUrl = getattr(self, actualFeed)
424 page = self._get_page(feedUrl)
425 json, html = extract_payload(page)
429 def download(self, messageId, adir):
431 Download a voicemail or recorded call MP3 matching the given ``msg``
432 which can either be a ``Message`` instance, or a SHA1 identifier.
433 Saves files to ``adir`` (defaults to current directory).
434 Message hashes can be found in ``self.voicemail().messages`` for example.
435 @returns location of saved file.
438 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
439 fn = os.path.join(adir, '%s.mp3' % messageId)
440 with open(fn, 'wb') as fo:
444 def is_valid_syntax(self, number):
446 @returns If This number be called ( syntax validation only )
448 return self._validateRe.match(number) is not None
450 def get_account_number(self):
452 @returns The GoogleVoice phone number
454 return self._accountNum
456 def get_callback_numbers(self):
458 @returns a dictionary mapping call back numbers to descriptions
459 @note These results are cached for 30 minutes.
461 if not self.is_authed():
463 return self._callbackNumbers
465 def set_callback_number(self, callbacknumber):
467 Set the number that GoogleVoice calls
468 @param callbacknumber should be a proper 10 digit number
470 self._callbackNumber = callbacknumber
471 _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
474 def get_callback_number(self):
476 @returns Current callback number or None
478 return self._callbackNumber
480 def get_recent(self):
482 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
486 ("Received", self._XML_RECEIVED_URL),
487 ("Missed", self._XML_MISSED_URL),
488 ("Placed", self._XML_PLACED_URL),
490 flatXml = self._get_page(url)
492 allRecentHtml = self._grab_html(flatXml)
493 allRecentData = self._parse_history(allRecentHtml)
494 for recentCallData in allRecentData:
495 recentCallData["action"] = action
498 def get_contacts(self):
500 @returns Iterable of (contact id, contact name)
503 page = self._get_page(self._XML_CONTACTS_URL)
504 contactsBody = self._contactsBodyRe.search(page)
505 if contactsBody is None:
506 raise RuntimeError("Could not extract contact information")
507 accountData = _fake_parse_json(contactsBody.group(1))
508 if accountData is None:
510 for contactId, contactDetails in accountData["contacts"].iteritems():
511 # A zero contact id is the catch all for unknown contacts
513 if "name" in contactDetails:
514 contactDetails["name"] = unescape(contactDetails["name"])
515 yield contactId, contactDetails
517 def get_voicemails(self):
521 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
522 voicemailHtml = self._grab_html(voicemailPage)
523 voicemailJson = self._grab_json(voicemailPage)
524 if voicemailJson is None:
526 parsedVoicemail = self._parse_voicemail(voicemailHtml)
527 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
534 smsPage = self._get_page(self._XML_SMS_URL)
535 smsHtml = self._grab_html(smsPage)
536 smsJson = self._grab_json(smsPage)
539 parsedSms = self._parse_sms(smsHtml)
540 smss = self._merge_conversation_sources(parsedSms, smsJson)
543 def mark_message(self, messageId, asRead):
548 "read": 1 if asRead else 0,
552 markPage = self._get_page(self._markAsReadURL, postData)
554 def archive_message(self, messageId):
562 markPage = self._get_page(self._archiveMessageURL, postData)
564 def _grab_json(self, flatXml):
565 xmlTree = ElementTree.fromstring(flatXml)
566 jsonElement = xmlTree.getchildren()[0]
567 flatJson = jsonElement.text
568 jsonTree = parse_json(flatJson)
571 def _grab_html(self, flatXml):
572 xmlTree = ElementTree.fromstring(flatXml)
573 htmlElement = xmlTree.getchildren()[1]
574 flatHtml = htmlElement.text
577 def _grab_account_info(self, page):
578 tokenGroup = self._tokenRe.search(page)
579 if tokenGroup is None:
580 raise RuntimeError("Could not extract authentication token from GoogleVoice")
581 self._token = tokenGroup.group(1)
583 anGroup = self._accountNumRe.search(page)
584 if anGroup is not None:
585 self._accountNum = anGroup.group(1)
587 _moduleLogger.debug("Could not extract account number from GoogleVoice")
589 self._callbackNumbers = {}
590 for match in self._callbackRe.finditer(page):
591 callbackNumber = match.group(2)
592 callbackName = match.group(1)
593 self._callbackNumbers[callbackNumber] = callbackName
594 if len(self._callbackNumbers) == 0:
595 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
597 def _send_validation(self, number):
598 if not self.is_valid_syntax(number):
599 raise ValueError('Number is not valid: "%s"' % number)
600 elif not self.is_authed():
601 raise RuntimeError("Not Authenticated")
604 def _parse_history(self, historyHtml):
605 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
606 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
607 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
608 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
609 exactTime = google_strptime(exactTime)
610 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
611 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
612 locationGroup = self._voicemailLocationRegex.search(messageHtml)
613 location = locationGroup.group(1).strip() if locationGroup else ""
615 nameGroup = self._voicemailNameRegex.search(messageHtml)
616 name = nameGroup.group(1).strip() if nameGroup else ""
617 numberGroup = self._voicemailNumberRegex.search(messageHtml)
618 number = numberGroup.group(1).strip() if numberGroup else ""
619 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
620 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
621 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
622 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
625 "id": messageId.strip(),
626 "contactId": contactId,
627 "name": unescape(name),
629 "relTime": relativeTime,
630 "prettyNumber": prettyNumber,
632 "location": unescape(location),
636 def _interpret_voicemail_regex(group):
637 quality, content, number = group.group(2), group.group(3), group.group(4)
639 if quality is not None and content is not None:
640 text.accuracy = quality
641 text.text = unescape(content)
643 elif number is not None:
644 text.accuracy = MessageText.ACCURACY_HIGH
648 def _parse_voicemail(self, voicemailHtml):
649 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
650 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
651 conv = Conversation()
652 conv.type = Conversation.TYPE_VOICEMAIL
653 conv.id = messageId.strip()
655 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
656 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
657 conv.time = google_strptime(exactTimeText)
658 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
659 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
660 locationGroup = self._voicemailLocationRegex.search(messageHtml)
661 conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "")
663 nameGroup = self._voicemailNameRegex.search(messageHtml)
664 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
665 numberGroup = self._voicemailNumberRegex.search(messageHtml)
666 conv.number = numberGroup.group(1).strip() if numberGroup else ""
667 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
668 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
669 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
670 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
672 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
674 self._interpret_voicemail_regex(group)
675 for group in messageGroups
676 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
678 message.body = messageParts
679 message.whoFrom = conv.name
680 message.when = conv.time.strftime("%I:%M %p")
681 conv.messages = (message, )
686 def _interpret_sms_message_parts(fromPart, textPart, timePart):
688 text.accuracy = MessageText.ACCURACY_MEDIUM
689 text.text = unescape(textPart)
692 message.body = (text, )
693 message.whoFrom = fromPart
694 message.when = timePart
698 def _parse_sms(self, smsHtml):
699 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
700 for messageId, messageHtml in itergroup(splitSms[1:], 2):
701 conv = Conversation()
702 conv.type = Conversation.TYPE_SMS
703 conv.id = messageId.strip()
705 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
706 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
707 conv.time = google_strptime(exactTimeText)
708 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
709 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
712 nameGroup = self._voicemailNameRegex.search(messageHtml)
713 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
714 numberGroup = self._voicemailNumberRegex.search(messageHtml)
715 conv.number = numberGroup.group(1).strip() if numberGroup else ""
716 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
717 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
718 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
719 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
721 fromGroups = self._smsFromRegex.finditer(messageHtml)
722 fromParts = (group.group(1).strip() for group in fromGroups)
723 textGroups = self._smsTextRegex.finditer(messageHtml)
724 textParts = (group.group(1).strip() for group in textGroups)
725 timeGroups = self._smsTimeRegex.finditer(messageHtml)
726 timeParts = (group.group(1).strip() for group in timeGroups)
728 messageParts = itertools.izip(fromParts, textParts, timeParts)
729 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
730 conv.messages = messages
735 def _merge_conversation_sources(parsedMessages, json):
736 for message in parsedMessages:
737 jsonItem = json["messages"][message.id]
738 message.isRead = jsonItem["isRead"]
739 message.isSpam = jsonItem["isSpam"]
740 message.isTrash = jsonItem["isTrash"]
741 message.isArchived = "inbox" not in jsonItem["labels"]
744 def _get_page(self, url, data = None, refererUrl = None):
746 if refererUrl is not None:
747 headers["Referer"] = refererUrl
749 encodedData = urllib.urlencode(data) if data is not None else None
752 page = self._browser.download(url, encodedData, None, headers)
753 except urllib2.URLError, e:
754 _moduleLogger.error("Translating error: %s" % str(e))
755 raise NetworkError("%s is not accesible" % url)
759 def _get_page_with_token(self, url, data = None, refererUrl = None):
762 data['_rnr_se'] = self._token
764 page = self._get_page(url, data, refererUrl)
768 def _parse_with_validation(self, page):
769 json = parse_json(page)
770 validate_response(json)
774 _UNESCAPE_ENTITIES = {
782 plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
786 def google_strptime(time):
788 Hack: Google always returns the time in the same locale. Sadly if the
789 local system's locale is different, there isn't a way to perfectly handle
790 the time. So instead we handle implement some time formatting
792 abbrevTime = time[:-3]
793 parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
794 if time.endswith("PM"):
795 parsedTime += datetime.timedelta(hours=12)
799 def itergroup(iterator, count, padValue = None):
801 Iterate in groups of 'count' values. If there
802 aren't enough values, the last result is padded with
805 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
809 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
813 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
818 >>> for val in itergroup("123456", 3):
822 >>> for val in itergroup("123456", 3):
823 ... print repr("".join(val))
827 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
828 nIterators = (paddedIterator, ) * count
829 return itertools.izip(*nIterators)
833 _TRUE_REGEX = re.compile("true")
834 _FALSE_REGEX = re.compile("false")
835 _COMMENT_REGEX = re.compile("^\s+//.*$", re.M)
836 s = _TRUE_REGEX.sub("True", s)
837 s = _FALSE_REGEX.sub("False", s)
838 s = _COMMENT_REGEX.sub("#", s)
840 results = eval(s, {}, {})
842 _moduleLogger.exception("Oops")
847 def _fake_parse_json(flattened):
848 return safe_eval(flattened)
851 def _actual_parse_json(flattened):
852 return simplejson.loads(flattened)
855 if simplejson is None:
856 parse_json = _fake_parse_json
858 parse_json = _actual_parse_json
861 def extract_payload(flatXml):
862 xmlTree = ElementTree.fromstring(flatXml)
864 jsonElement = xmlTree.getchildren()[0]
865 flatJson = jsonElement.text
866 jsonTree = parse_json(flatJson)
868 htmlElement = xmlTree.getchildren()[1]
869 flatHtml = htmlElement.text
871 return jsonTree, flatHtml
874 def validate_response(response):
876 Validates that the JSON response is A-OK
879 assert response is not None
880 assert 'ok' in response
881 assert response['ok']
882 except AssertionError:
883 raise RuntimeError('There was a problem with GV: %s' % response)
886 def guess_phone_type(number):
887 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
888 return GVoiceBackend.PHONE_TYPE_GIZMO
890 return GVoiceBackend.PHONE_TYPE_MOBILE
893 def get_sane_callback(backend):
895 Try to set a sane default callback number on these preferences
896 1) 1747 numbers ( Gizmo )
897 2) anything with gizmo in the name
898 3) anything with computer in the name
901 numbers = backend.get_callback_numbers()
903 priorityOrderedCriteria = [
913 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
915 descriptionMatcher = None
916 if numberCriteria is not None:
917 numberMatcher = re.compile(numberCriteria)
918 elif descriptionCriteria is not None:
919 descriptionMatcher = re.compile(descriptionCriteria, re.I)
921 for number, description in numbers.iteritems():
922 if numberMatcher is not None and numberMatcher.match(number) is None:
924 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
929 def set_sane_callback(backend):
931 Try to set a sane default callback number on these preferences
932 1) 1747 numbers ( Gizmo )
933 2) anything with gizmo in the name
934 3) anything with computer in the name
937 number = get_sane_callback(backend)
938 backend.set_callback_number(number)
941 def _is_not_special(name):
942 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
946 members = inspect.getmembers(obj)
947 return dict((name, value) for (name, value) in members if _is_not_special(name))
950 def grab_debug_info(username, password):
951 cookieFile = os.path.join(".", "raw_cookies.txt")
953 os.remove(cookieFile)
957 backend = GVoiceBackend(cookieFile)
958 browser = backend._browser
961 ("forward", backend._forwardURL),
962 ("token", backend._tokenURL),
963 ("login", backend._loginURL),
964 ("isdnd", backend._isDndURL),
965 ("account", backend._XML_ACCOUNT_URL),
966 ("contacts", backend._XML_CONTACTS_URL),
968 ("voicemail", backend._XML_VOICEMAIL_URL),
969 ("sms", backend._XML_SMS_URL),
971 ("recent", backend._XML_RECENT_URL),
972 ("placed", backend._XML_PLACED_URL),
973 ("recieved", backend._XML_RECEIVED_URL),
974 ("missed", backend._XML_MISSED_URL),
978 print "Grabbing pre-login pages"
979 for name, url in _TEST_WEBPAGES:
981 page = browser.download(url)
982 except StandardError, e:
985 print "\tWriting to file"
986 with open("not_loggedin_%s.txt" % name, "w") as f:
990 print "Attempting login"
991 galxToken = backend._get_token()
992 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
993 with open("loggingin.txt", "w") as f:
994 print "\tWriting to file"
995 f.write(loginSuccessOrFailurePage)
997 backend._grab_account_info(loginSuccessOrFailurePage)
999 # Retry in case the redirect failed
1000 # luckily is_authed does everything we need for a retry
1001 loggedIn = backend.is_authed(True)
1006 print "Grabbing post-login pages"
1007 for name, url in _TEST_WEBPAGES:
1009 page = browser.download(url)
1010 except StandardError, e:
1013 print "\tWriting to file"
1014 with open("loggedin_%s.txt" % name, "w") as f:
1018 browser.save_cookies()
1019 print "\tWriting cookies to file"
1020 with open("cookies.txt", "w") as f:
1022 "%s: %s\n" % (c.name, c.value)
1023 for c in browser._cookies
1029 logging.basicConfig(level=logging.DEBUG)
1035 grab_debug_info(username, password)
1038 if __name__ == "__main__":