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()
311 self._lastAuthed = 0.0
314 self._browser.clear_cookies()
315 self._browser.save_cookies()
317 self._lastAuthed = 0.0
323 isDndPage = self._get_page(self._isDndURL)
325 dndGroup = self._isDndRe.search(isDndPage)
328 dndStatus = dndGroup.group(1)
329 isDnd = True if dndStatus.strip().lower() == "true" else False
332 def set_dnd(self, doNotDisturb):
337 "doNotDisturb": 1 if doNotDisturb else 0,
340 dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
342 def call(self, outgoingNumber):
344 This is the main function responsible for initating the callback
347 outgoingNumber = self._send_validation(outgoingNumber)
348 subscriberNumber = None
349 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
352 'outgoingNumber': outgoingNumber,
353 'forwardingNumber': self._callbackNumber,
354 'subscriberNumber': subscriberNumber or 'undefined',
355 'phoneType': str(phoneType),
358 _moduleLogger.info("%r" % callData)
360 page = self._get_page_with_token(
364 self._parse_with_validation(page)
367 def cancel(self, outgoingNumber=None):
369 Cancels a call matching outgoing and forwarding numbers (if given).
370 Will raise an error if no matching call is being placed
373 page = self._get_page_with_token(
376 'outgoingNumber': outgoingNumber or 'undefined',
377 'forwardingNumber': self._callbackNumber or 'undefined',
381 self._parse_with_validation(page)
383 def send_sms(self, phoneNumbers, message):
387 validatedPhoneNumbers = [
388 self._send_validation(phoneNumber)
389 for phoneNumber in phoneNumbers
391 flattenedPhoneNumbers = ",".join(validatedPhoneNumbers)
392 page = self._get_page_with_token(
395 'phoneNumber': flattenedPhoneNumbers,
399 self._parse_with_validation(page)
401 def search(self, query):
403 Search your Google Voice Account history for calls, voicemails, and sms
404 Returns ``Folder`` instance containting matching messages
407 page = self._get_page(
408 self._XML_SEARCH_URL,
411 json, html = extract_payload(page)
414 def get_feed(self, feed):
418 actualFeed = "_XML_%s_URL" % feed.upper()
419 feedUrl = getattr(self, actualFeed)
421 page = self._get_page(feedUrl)
422 json, html = extract_payload(page)
426 def download(self, messageId, adir):
428 Download a voicemail or recorded call MP3 matching the given ``msg``
429 which can either be a ``Message`` instance, or a SHA1 identifier.
430 Saves files to ``adir`` (defaults to current directory).
431 Message hashes can be found in ``self.voicemail().messages`` for example.
432 @returns location of saved file.
435 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
436 fn = os.path.join(adir, '%s.mp3' % messageId)
437 with open(fn, 'wb') as fo:
441 def is_valid_syntax(self, number):
443 @returns If This number be called ( syntax validation only )
445 return self._validateRe.match(number) is not None
447 def get_account_number(self):
449 @returns The GoogleVoice phone number
451 return self._accountNum
453 def get_callback_numbers(self):
455 @returns a dictionary mapping call back numbers to descriptions
456 @note These results are cached for 30 minutes.
458 if not self.is_authed():
460 return self._callbackNumbers
462 def set_callback_number(self, callbacknumber):
464 Set the number that GoogleVoice calls
465 @param callbacknumber should be a proper 10 digit number
467 self._callbackNumber = callbacknumber
468 _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
471 def get_callback_number(self):
473 @returns Current callback number or None
475 return self._callbackNumber
477 def get_recent(self):
479 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
483 (action, self._get_page(url))
485 ("Received", self._XML_RECEIVED_URL),
486 ("Missed", self._XML_MISSED_URL),
487 ("Placed", self._XML_PLACED_URL),
490 return self._parse_recent(recentPages)
492 def get_contacts(self):
494 @returns Iterable of (contact id, contact name)
497 page = self._get_page(self._XML_CONTACTS_URL)
498 return self._process_contacts(page)
500 def get_voicemails(self):
504 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
505 voicemailHtml = self._grab_html(voicemailPage)
506 voicemailJson = self._grab_json(voicemailPage)
507 if voicemailJson is None:
509 parsedVoicemail = self._parse_voicemail(voicemailHtml)
510 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
517 smsPage = self._get_page(self._XML_SMS_URL)
518 smsHtml = self._grab_html(smsPage)
519 smsJson = self._grab_json(smsPage)
522 parsedSms = self._parse_sms(smsHtml)
523 smss = self._merge_conversation_sources(parsedSms, smsJson)
526 def mark_message(self, messageId, asRead):
531 "read": 1 if asRead else 0,
535 markPage = self._get_page(self._markAsReadURL, postData)
537 def archive_message(self, messageId):
545 markPage = self._get_page(self._archiveMessageURL, postData)
547 def _grab_json(self, flatXml):
548 xmlTree = ElementTree.fromstring(flatXml)
549 jsonElement = xmlTree.getchildren()[0]
550 flatJson = jsonElement.text
551 jsonTree = parse_json(flatJson)
554 def _grab_html(self, flatXml):
555 xmlTree = ElementTree.fromstring(flatXml)
556 htmlElement = xmlTree.getchildren()[1]
557 flatHtml = htmlElement.text
560 def _grab_account_info(self, page):
561 tokenGroup = self._tokenRe.search(page)
562 if tokenGroup is None:
563 raise RuntimeError("Could not extract authentication token from GoogleVoice")
564 self._token = tokenGroup.group(1)
566 anGroup = self._accountNumRe.search(page)
567 if anGroup is not None:
568 self._accountNum = anGroup.group(1)
570 _moduleLogger.debug("Could not extract account number from GoogleVoice")
572 self._callbackNumbers = {}
573 for match in self._callbackRe.finditer(page):
574 callbackNumber = match.group(2)
575 callbackName = match.group(1)
576 self._callbackNumbers[callbackNumber] = callbackName
577 if len(self._callbackNumbers) == 0:
578 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
580 def _send_validation(self, number):
581 if not self.is_valid_syntax(number):
582 raise ValueError('Number is not valid: "%s"' % number)
583 elif not self.is_authed():
584 raise RuntimeError("Not Authenticated")
587 def _parse_recent(self, recentPages):
588 for action, flatXml in recentPages:
589 allRecentHtml = self._grab_html(flatXml)
590 allRecentData = self._parse_history(allRecentHtml)
591 for recentCallData in allRecentData:
592 recentCallData["action"] = action
595 def _process_contacts(self, page):
596 contactsBody = self._contactsBodyRe.search(page)
597 if contactsBody is None:
598 raise RuntimeError("Could not extract contact information")
599 accountData = _fake_parse_json(contactsBody.group(1))
600 for contactId, contactDetails in accountData["contacts"].iteritems():
601 # A zero contact id is the catch all for unknown contacts
603 yield contactId, contactDetails
605 def _parse_history(self, historyHtml):
606 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
607 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
608 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
609 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
610 exactTime = google_strptime(exactTime)
611 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
612 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
613 locationGroup = self._voicemailLocationRegex.search(messageHtml)
614 location = locationGroup.group(1).strip() if locationGroup else ""
616 nameGroup = self._voicemailNameRegex.search(messageHtml)
617 name = nameGroup.group(1).strip() if nameGroup else ""
618 numberGroup = self._voicemailNumberRegex.search(messageHtml)
619 number = numberGroup.group(1).strip() if numberGroup else ""
620 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
621 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
622 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
623 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
626 "id": messageId.strip(),
627 "contactId": contactId,
628 "name": unescape(name),
630 "relTime": relativeTime,
631 "prettyNumber": prettyNumber,
633 "location": unescape(location),
637 def _interpret_voicemail_regex(group):
638 quality, content, number = group.group(2), group.group(3), group.group(4)
640 if quality is not None and content is not None:
641 text.accuracy = quality
644 elif number is not None:
645 text.accuracy = MessageText.ACCURACY_HIGH
649 def _parse_voicemail(self, voicemailHtml):
650 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
651 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
652 conv = Conversation()
653 conv.type = Conversation.TYPE_VOICEMAIL
654 conv.id = messageId.strip()
656 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
657 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
658 conv.time = google_strptime(exactTimeText)
659 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
660 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
661 locationGroup = self._voicemailLocationRegex.search(messageHtml)
662 conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "")
664 nameGroup = self._voicemailNameRegex.search(messageHtml)
665 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
666 numberGroup = self._voicemailNumberRegex.search(messageHtml)
667 conv.number = numberGroup.group(1).strip() if numberGroup else ""
668 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
669 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
670 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
671 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
673 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
675 self._interpret_voicemail_regex(group)
676 for group in messageGroups
677 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
679 message.body = messageParts
680 message.whoFrom = conv.name
681 message.when = conv.time.strftime("%I:%M %p")
682 conv.messages = (message, )
687 def _interpret_sms_message_parts(fromPart, textPart, timePart):
689 text.accuracy = MessageText.ACCURACY_MEDIUM
693 message.body = (text, )
694 message.whoFrom = fromPart
695 message.when = timePart
699 def _parse_sms(self, smsHtml):
700 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
701 for messageId, messageHtml in itergroup(splitSms[1:], 2):
702 conv = Conversation()
703 conv.type = Conversation.TYPE_SMS
704 conv.id = messageId.strip()
706 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
707 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
708 conv.time = google_strptime(exactTimeText)
709 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
710 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
713 nameGroup = self._voicemailNameRegex.search(messageHtml)
714 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
715 numberGroup = self._voicemailNumberRegex.search(messageHtml)
716 conv.number = numberGroup.group(1).strip() if numberGroup else ""
717 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
718 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
719 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
720 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
722 fromGroups = self._smsFromRegex.finditer(messageHtml)
723 fromParts = (group.group(1).strip() for group in fromGroups)
724 textGroups = self._smsTextRegex.finditer(messageHtml)
725 textParts = (group.group(1).strip() for group in textGroups)
726 timeGroups = self._smsTimeRegex.finditer(messageHtml)
727 timeParts = (group.group(1).strip() for group in timeGroups)
729 messageParts = itertools.izip(fromParts, textParts, timeParts)
730 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
731 conv.messages = messages
736 def _merge_conversation_sources(parsedMessages, json):
737 for message in parsedMessages:
738 jsonItem = json["messages"][message.id]
739 message.isRead = jsonItem["isRead"]
740 message.isSpam = jsonItem["isSpam"]
741 message.isTrash = jsonItem["isTrash"]
742 message.isArchived = "inbox" not in jsonItem["labels"]
745 def _get_page(self, url, data = None, refererUrl = None):
747 if refererUrl is not None:
748 headers["Referer"] = refererUrl
750 encodedData = urllib.urlencode(data) if data is not None else None
753 page = self._browser.download(url, encodedData, None, headers)
754 except urllib2.URLError, e:
755 _moduleLogger.error("Translating error: %s" % str(e))
756 raise NetworkError("%s is not accesible" % url)
760 def _get_page_with_token(self, url, data = None, refererUrl = None):
763 data['_rnr_se'] = self._token
765 page = self._get_page(url, data, refererUrl)
769 def _parse_with_validation(self, page):
770 json = parse_json(page)
771 validate_response(json)
775 _UNESCAPE_ENTITIES = {
783 plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
787 def google_strptime(time):
789 Hack: Google always returns the time in the same locale. Sadly if the
790 local system's locale is different, there isn't a way to perfectly handle
791 the time. So instead we handle implement some time formatting
793 abbrevTime = time[:-3]
794 parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
795 if time.endswith("PM"):
796 parsedTime += datetime.timedelta(hours=12)
800 def itergroup(iterator, count, padValue = None):
802 Iterate in groups of 'count' values. If there
803 aren't enough values, the last result is padded with
806 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
810 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
814 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
819 >>> for val in itergroup("123456", 3):
823 >>> for val in itergroup("123456", 3):
824 ... print repr("".join(val))
828 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
829 nIterators = (paddedIterator, ) * count
830 return itertools.izip(*nIterators)
834 _TRUE_REGEX = re.compile("true")
835 _FALSE_REGEX = re.compile("false")
836 _COMMENT_REGEX = re.compile("^\s+//.*$", re.M)
837 s = _TRUE_REGEX.sub("True", s)
838 s = _FALSE_REGEX.sub("False", s)
839 s = _COMMENT_REGEX.sub("#", s)
841 results = eval(s, {}, {})
843 _moduleLogger.exception("Oops")
848 def _fake_parse_json(flattened):
849 return safe_eval(flattened)
852 def _actual_parse_json(flattened):
853 return simplejson.loads(flattened)
856 if simplejson is None:
857 parse_json = _fake_parse_json
859 parse_json = _actual_parse_json
862 def extract_payload(flatXml):
863 xmlTree = ElementTree.fromstring(flatXml)
865 jsonElement = xmlTree.getchildren()[0]
866 flatJson = jsonElement.text
867 jsonTree = parse_json(flatJson)
869 htmlElement = xmlTree.getchildren()[1]
870 flatHtml = htmlElement.text
872 return jsonTree, flatHtml
875 def validate_response(response):
877 Validates that the JSON response is A-OK
880 assert response is not None
881 assert 'ok' in response
882 assert response['ok']
883 except AssertionError:
884 raise RuntimeError('There was a problem with GV: %s' % response)
887 def guess_phone_type(number):
888 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
889 return GVoiceBackend.PHONE_TYPE_GIZMO
891 return GVoiceBackend.PHONE_TYPE_MOBILE
894 def get_sane_callback(backend):
896 Try to set a sane default callback number on these preferences
897 1) 1747 numbers ( Gizmo )
898 2) anything with gizmo in the name
899 3) anything with computer in the name
902 numbers = backend.get_callback_numbers()
904 priorityOrderedCriteria = [
914 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
916 descriptionMatcher = None
917 if numberCriteria is not None:
918 numberMatcher = re.compile(numberCriteria)
919 elif descriptionCriteria is not None:
920 descriptionMatcher = re.compile(descriptionCriteria, re.I)
922 for number, description in numbers.iteritems():
923 if numberMatcher is not None and numberMatcher.match(number) is None:
925 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
930 def set_sane_callback(backend):
932 Try to set a sane default callback number on these preferences
933 1) 1747 numbers ( Gizmo )
934 2) anything with gizmo in the name
935 3) anything with computer in the name
938 number = get_sane_callback(backend)
939 backend.set_callback_number(number)
942 def _is_not_special(name):
943 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
947 members = inspect.getmembers(obj)
948 return dict((name, value) for (name, value) in members if _is_not_special(name))
951 def grab_debug_info(username, password):
952 cookieFile = os.path.join(".", "raw_cookies.txt")
954 os.remove(cookieFile)
958 backend = GVoiceBackend(cookieFile)
959 browser = backend._browser
962 ("forward", backend._forwardURL),
963 ("token", backend._tokenURL),
964 ("login", backend._loginURL),
965 ("isdnd", backend._isDndURL),
966 ("account", backend._XML_ACCOUNT_URL),
967 ("contacts", backend._XML_CONTACTS_URL),
969 ("voicemail", backend._XML_VOICEMAIL_URL),
970 ("sms", backend._XML_SMS_URL),
972 ("recent", backend._XML_RECENT_URL),
973 ("placed", backend._XML_PLACED_URL),
974 ("recieved", backend._XML_RECEIVED_URL),
975 ("missed", backend._XML_MISSED_URL),
979 print "Grabbing pre-login pages"
980 for name, url in _TEST_WEBPAGES:
982 page = browser.download(url)
983 except StandardError, e:
986 print "\tWriting to file"
987 with open("not_loggedin_%s.txt" % name, "w") as f:
991 print "Attempting login"
992 galxToken = backend._get_token()
993 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
994 with open("loggingin.txt", "w") as f:
995 print "\tWriting to file"
996 f.write(loginSuccessOrFailurePage)
998 backend._grab_account_info(loginSuccessOrFailurePage)
1000 # Retry in case the redirect failed
1001 # luckily is_authed does everything we need for a retry
1002 loggedIn = backend.is_authed(True)
1007 print "Grabbing post-login pages"
1008 for name, url in _TEST_WEBPAGES:
1010 page = browser.download(url)
1011 except StandardError, e:
1014 print "\tWriting to file"
1015 with open("loggedin_%s.txt" % name, "w") as f:
1019 browser.save_cookies()
1020 print "\tWriting cookies to file"
1021 with open("cookies.txt", "w") as f:
1023 "%s: %s\n" % (c.name, c.value)
1024 for c in browser._cookies
1030 logging.basicConfig(level=logging.DEBUG)
1036 grab_debug_info(username, password)
1039 if __name__ == "__main__":