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(unicode(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._CSV_CONTACTS_URL = "http://mail.google.com/mail/contacts/data/export"
192 self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
195 'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms',
196 'recorded', 'placed', 'received', 'missed'
198 self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox"
199 self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred"
200 self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all"
201 self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam"
202 self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash"
203 self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/"
204 self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/"
205 self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/"
206 self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/"
207 self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
208 self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
210 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
211 self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
212 self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
213 self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
215 self._contactsBodyRe = re.compile(r"""gcData\s*=\s*({.*?});""", re.MULTILINE | re.DOTALL)
216 self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
217 self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
218 self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
219 self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
220 self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
221 self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
222 self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
223 self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
224 self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
225 self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
226 self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
227 self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
229 def is_quick_login_possible(self):
231 @returns True then is_authed might be enough to login, else full login is required
233 return self._loadedFromCookies or 0.0 < self._lastAuthed
235 def is_authed(self, force = False):
237 Attempts to detect a current session
238 @note Once logged in try not to reauth more than once a minute.
239 @returns If authenticated
242 isRecentledAuthed = (time.time() - self._lastAuthed) < 120
243 isPreviouslyAuthed = self._token is not None
244 if isRecentledAuthed and isPreviouslyAuthed and not force:
248 page = self._get_page(self._forwardURL)
249 self._grab_account_info(page)
251 _moduleLogger.exception(str(e))
254 self._browser.save_cookies()
255 self._lastAuthed = time.time()
258 def _get_token(self):
259 tokenPage = self._get_page(self._tokenURL)
261 galxTokens = self._galxRe.search(tokenPage)
262 if galxTokens is not None:
263 galxToken = galxTokens.group(1)
266 _moduleLogger.debug("Could not grab GALX token")
269 def _login(self, username, password, token):
273 'service': "grandcentral",
276 "PersistentCookie": "yes",
278 "continue": self._forwardURL,
281 loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
282 return loginSuccessOrFailurePage
284 def login(self, username, password):
286 Attempt to login to GoogleVoice
287 @returns Whether login was successful or not
291 galxToken = self._get_token()
292 loginSuccessOrFailurePage = self._login(username, password, galxToken)
295 self._grab_account_info(loginSuccessOrFailurePage)
297 # Retry in case the redirect failed
298 # luckily is_authed does everything we need for a retry
299 loggedIn = self.is_authed(True)
301 _moduleLogger.exception(str(e))
303 _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
305 self._browser.save_cookies()
306 self._lastAuthed = time.time()
310 self._browser.save_cookies()
313 self._browser.save_cookies()
315 self._lastAuthed = 0.0
318 self._browser.clear_cookies()
319 self._browser.save_cookies()
321 self._lastAuthed = 0.0
327 isDndPage = self._get_page(self._isDndURL)
329 dndGroup = self._isDndRe.search(isDndPage)
332 dndStatus = dndGroup.group(1)
333 isDnd = True if dndStatus.strip().lower() == "true" else False
336 def set_dnd(self, doNotDisturb):
341 "doNotDisturb": 1 if doNotDisturb else 0,
344 dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
346 def call(self, outgoingNumber):
348 This is the main function responsible for initating the callback
351 outgoingNumber = self._send_validation(outgoingNumber)
352 subscriberNumber = None
353 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
356 'outgoingNumber': outgoingNumber,
357 'forwardingNumber': self._callbackNumber,
358 'subscriberNumber': subscriberNumber or 'undefined',
359 'phoneType': str(phoneType),
362 _moduleLogger.info("%r" % callData)
364 page = self._get_page_with_token(
368 self._parse_with_validation(page)
371 def cancel(self, outgoingNumber=None):
373 Cancels a call matching outgoing and forwarding numbers (if given).
374 Will raise an error if no matching call is being placed
377 page = self._get_page_with_token(
380 'outgoingNumber': outgoingNumber or 'undefined',
381 'forwardingNumber': self._callbackNumber or 'undefined',
385 self._parse_with_validation(page)
387 def send_sms(self, phoneNumbers, message):
391 validatedPhoneNumbers = [
392 self._send_validation(phoneNumber)
393 for phoneNumber in phoneNumbers
395 flattenedPhoneNumbers = ",".join(validatedPhoneNumbers)
396 page = self._get_page_with_token(
399 'phoneNumber': flattenedPhoneNumbers,
400 'text': unicode(message).encode("utf-8"),
403 self._parse_with_validation(page)
405 def search(self, query):
407 Search your Google Voice Account history for calls, voicemails, and sms
408 Returns ``Folder`` instance containting matching messages
411 page = self._get_page(
412 self._XML_SEARCH_URL,
415 json, html = extract_payload(page)
418 def get_feed(self, feed):
422 actualFeed = "_XML_%s_URL" % feed.upper()
423 feedUrl = getattr(self, actualFeed)
425 page = self._get_page(feedUrl)
426 json, html = extract_payload(page)
430 def download(self, messageId, adir):
432 Download a voicemail or recorded call MP3 matching the given ``msg``
433 which can either be a ``Message`` instance, or a SHA1 identifier.
434 Saves files to ``adir`` (defaults to current directory).
435 Message hashes can be found in ``self.voicemail().messages`` for example.
436 @returns location of saved file.
439 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
440 fn = os.path.join(adir, '%s.mp3' % messageId)
441 with open(fn, 'wb') as fo:
445 def is_valid_syntax(self, number):
447 @returns If This number be called ( syntax validation only )
449 return self._validateRe.match(number) is not None
451 def get_account_number(self):
453 @returns The GoogleVoice phone number
455 return self._accountNum
457 def get_callback_numbers(self):
459 @returns a dictionary mapping call back numbers to descriptions
460 @note These results are cached for 30 minutes.
462 if not self.is_authed():
464 return self._callbackNumbers
466 def set_callback_number(self, callbacknumber):
468 Set the number that GoogleVoice calls
469 @param callbacknumber should be a proper 10 digit number
471 self._callbackNumber = callbacknumber
472 _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
475 def get_callback_number(self):
477 @returns Current callback number or None
479 return self._callbackNumber
481 def get_recent(self):
483 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
487 (action, self._get_page(url))
489 ("Received", self._XML_RECEIVED_URL),
490 ("Missed", self._XML_MISSED_URL),
491 ("Placed", self._XML_PLACED_URL),
494 return self._parse_recent(recentPages)
496 def get_contacts(self):
498 @returns Iterable of (contact id, contact name)
501 page = self._get_page(self._XML_CONTACTS_URL)
502 return self._process_contacts(page)
504 def get_csv_contacts(self):
506 "groupToExport": "mine",
508 "out": "OUTLOOK_CSV",
510 contacts = self._get_page(self._CSV_CONTACTS_URL, data)
513 def get_voicemails(self):
517 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
518 voicemailHtml = self._grab_html(voicemailPage)
519 voicemailJson = self._grab_json(voicemailPage)
520 if voicemailJson is None:
522 parsedVoicemail = self._parse_voicemail(voicemailHtml)
523 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
530 smsPage = self._get_page(self._XML_SMS_URL)
531 smsHtml = self._grab_html(smsPage)
532 smsJson = self._grab_json(smsPage)
535 parsedSms = self._parse_sms(smsHtml)
536 smss = self._merge_conversation_sources(parsedSms, smsJson)
539 def mark_message(self, messageId, asRead):
544 "read": 1 if asRead else 0,
548 markPage = self._get_page(self._markAsReadURL, postData)
550 def archive_message(self, messageId):
558 markPage = self._get_page(self._archiveMessageURL, postData)
560 def _grab_json(self, flatXml):
561 xmlTree = ElementTree.fromstring(flatXml)
562 jsonElement = xmlTree.getchildren()[0]
563 flatJson = jsonElement.text
564 jsonTree = parse_json(flatJson)
567 def _grab_html(self, flatXml):
568 xmlTree = ElementTree.fromstring(flatXml)
569 htmlElement = xmlTree.getchildren()[1]
570 flatHtml = htmlElement.text
573 def _grab_account_info(self, page):
574 tokenGroup = self._tokenRe.search(page)
575 if tokenGroup is None:
576 raise RuntimeError("Could not extract authentication token from GoogleVoice")
577 self._token = tokenGroup.group(1)
579 anGroup = self._accountNumRe.search(page)
580 if anGroup is not None:
581 self._accountNum = anGroup.group(1)
583 _moduleLogger.debug("Could not extract account number from GoogleVoice")
585 self._callbackNumbers = {}
586 for match in self._callbackRe.finditer(page):
587 callbackNumber = match.group(2)
588 callbackName = match.group(1)
589 self._callbackNumbers[callbackNumber] = callbackName
590 if len(self._callbackNumbers) == 0:
591 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
593 def _send_validation(self, number):
594 if not self.is_valid_syntax(number):
595 raise ValueError('Number is not valid: "%s"' % number)
596 elif not self.is_authed():
597 raise RuntimeError("Not Authenticated")
600 def _parse_recent(self, recentPages):
601 for action, flatXml in recentPages:
602 allRecentHtml = self._grab_html(flatXml)
603 allRecentData = self._parse_history(allRecentHtml)
604 for recentCallData in allRecentData:
605 recentCallData["action"] = action
608 def _process_contacts(self, page):
609 contactsBody = self._contactsBodyRe.search(page)
610 if contactsBody is None:
611 raise RuntimeError("Could not extract contact information")
612 accountData = _fake_parse_json(contactsBody.group(1))
613 for contactId, contactDetails in accountData["contacts"].iteritems():
614 # A zero contact id is the catch all for unknown contacts
616 yield contactId, contactDetails
618 def _parse_history(self, historyHtml):
619 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
620 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
621 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
622 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
623 exactTime = google_strptime(exactTime)
624 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
625 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
626 locationGroup = self._voicemailLocationRegex.search(messageHtml)
627 location = locationGroup.group(1).strip() if locationGroup else ""
629 nameGroup = self._voicemailNameRegex.search(messageHtml)
630 name = nameGroup.group(1).strip() if nameGroup else ""
631 numberGroup = self._voicemailNumberRegex.search(messageHtml)
632 number = numberGroup.group(1).strip() if numberGroup else ""
633 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
634 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
635 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
636 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
639 "id": messageId.strip(),
640 "contactId": contactId,
641 "name": unescape(name),
643 "relTime": relativeTime,
644 "prettyNumber": prettyNumber,
646 "location": unescape(location),
650 def _interpret_voicemail_regex(group):
651 quality, content, number = group.group(2), group.group(3), group.group(4)
653 if quality is not None and content is not None:
654 text.accuracy = quality
657 elif number is not None:
658 text.accuracy = MessageText.ACCURACY_HIGH
662 def _parse_voicemail(self, voicemailHtml):
663 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
664 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
665 conv = Conversation()
666 conv.type = Conversation.TYPE_VOICEMAIL
667 conv.id = messageId.strip()
669 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
670 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
671 conv.time = google_strptime(exactTimeText)
672 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
673 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
674 locationGroup = self._voicemailLocationRegex.search(messageHtml)
675 conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "")
677 nameGroup = self._voicemailNameRegex.search(messageHtml)
678 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
679 numberGroup = self._voicemailNumberRegex.search(messageHtml)
680 conv.number = numberGroup.group(1).strip() if numberGroup else ""
681 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
682 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
683 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
684 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
686 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
688 self._interpret_voicemail_regex(group)
689 for group in messageGroups
690 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
692 message.body = messageParts
693 message.whoFrom = conv.name
694 message.when = conv.time.strftime("%I:%M %p")
695 conv.messages = (message, )
700 def _interpret_sms_message_parts(fromPart, textPart, timePart):
702 text.accuracy = MessageText.ACCURACY_MEDIUM
706 message.body = (text, )
707 message.whoFrom = fromPart
708 message.when = timePart
712 def _parse_sms(self, smsHtml):
713 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
714 for messageId, messageHtml in itergroup(splitSms[1:], 2):
715 conv = Conversation()
716 conv.type = Conversation.TYPE_SMS
717 conv.id = messageId.strip()
719 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
720 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
721 conv.time = google_strptime(exactTimeText)
722 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
723 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
726 nameGroup = self._voicemailNameRegex.search(messageHtml)
727 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
728 numberGroup = self._voicemailNumberRegex.search(messageHtml)
729 conv.number = numberGroup.group(1).strip() if numberGroup else ""
730 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
731 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
732 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
733 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
735 fromGroups = self._smsFromRegex.finditer(messageHtml)
736 fromParts = (group.group(1).strip() for group in fromGroups)
737 textGroups = self._smsTextRegex.finditer(messageHtml)
738 textParts = (group.group(1).strip() for group in textGroups)
739 timeGroups = self._smsTimeRegex.finditer(messageHtml)
740 timeParts = (group.group(1).strip() for group in timeGroups)
742 messageParts = itertools.izip(fromParts, textParts, timeParts)
743 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
744 conv.messages = messages
749 def _merge_conversation_sources(parsedMessages, json):
750 for message in parsedMessages:
751 jsonItem = json["messages"][message.id]
752 message.isRead = jsonItem["isRead"]
753 message.isSpam = jsonItem["isSpam"]
754 message.isTrash = jsonItem["isTrash"]
755 message.isArchived = "inbox" not in jsonItem["labels"]
758 def _get_page(self, url, data = None, refererUrl = None):
760 if refererUrl is not None:
761 headers["Referer"] = refererUrl
763 encodedData = urllib.urlencode(data) if data is not None else None
766 page = self._browser.download(url, encodedData, None, headers)
767 except urllib2.URLError, e:
768 _moduleLogger.error("Translating error: %s" % str(e))
769 raise NetworkError("%s is not accesible" % url)
773 def _get_page_with_token(self, url, data = None, refererUrl = None):
776 data['_rnr_se'] = self._token
778 page = self._get_page(url, data, refererUrl)
782 def _parse_with_validation(self, page):
783 json = parse_json(page)
784 validate_response(json)
788 _UNESCAPE_ENTITIES = {
796 plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
800 def google_strptime(time):
802 Hack: Google always returns the time in the same locale. Sadly if the
803 local system's locale is different, there isn't a way to perfectly handle
804 the time. So instead we handle implement some time formatting
806 abbrevTime = time[:-3]
807 parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
808 if time.endswith("PM"):
809 parsedTime += datetime.timedelta(hours=12)
813 def itergroup(iterator, count, padValue = None):
815 Iterate in groups of 'count' values. If there
816 aren't enough values, the last result is padded with
819 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
823 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
827 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
832 >>> for val in itergroup("123456", 3):
836 >>> for val in itergroup("123456", 3):
837 ... print repr("".join(val))
841 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
842 nIterators = (paddedIterator, ) * count
843 return itertools.izip(*nIterators)
847 _TRUE_REGEX = re.compile("true")
848 _FALSE_REGEX = re.compile("false")
849 _COMMENT_REGEX = re.compile("^\s+//.*$", re.M)
850 s = _TRUE_REGEX.sub("True", s)
851 s = _FALSE_REGEX.sub("False", s)
852 s = _COMMENT_REGEX.sub("#", s)
854 results = eval(s, {}, {})
856 _moduleLogger.exception("Oops")
861 def _fake_parse_json(flattened):
862 return safe_eval(flattened)
865 def _actual_parse_json(flattened):
866 return simplejson.loads(flattened)
869 if simplejson is None:
870 parse_json = _fake_parse_json
872 parse_json = _actual_parse_json
875 def extract_payload(flatXml):
876 xmlTree = ElementTree.fromstring(flatXml)
878 jsonElement = xmlTree.getchildren()[0]
879 flatJson = jsonElement.text
880 jsonTree = parse_json(flatJson)
882 htmlElement = xmlTree.getchildren()[1]
883 flatHtml = htmlElement.text
885 return jsonTree, flatHtml
888 def validate_response(response):
890 Validates that the JSON response is A-OK
893 assert response is not None
894 assert 'ok' in response
895 assert response['ok']
896 except AssertionError:
897 raise RuntimeError('There was a problem with GV: %s' % response)
900 def guess_phone_type(number):
901 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
902 return GVoiceBackend.PHONE_TYPE_GIZMO
904 return GVoiceBackend.PHONE_TYPE_MOBILE
907 def get_sane_callback(backend):
909 Try to set a sane default callback number on these preferences
910 1) 1747 numbers ( Gizmo )
911 2) anything with gizmo in the name
912 3) anything with computer in the name
915 numbers = backend.get_callback_numbers()
917 priorityOrderedCriteria = [
927 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
929 descriptionMatcher = None
930 if numberCriteria is not None:
931 numberMatcher = re.compile(numberCriteria)
932 elif descriptionCriteria is not None:
933 descriptionMatcher = re.compile(descriptionCriteria, re.I)
935 for number, description in numbers.iteritems():
936 if numberMatcher is not None and numberMatcher.match(number) is None:
938 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
943 def set_sane_callback(backend):
945 Try to set a sane default callback number on these preferences
946 1) 1747 numbers ( Gizmo )
947 2) anything with gizmo in the name
948 3) anything with computer in the name
951 number = get_sane_callback(backend)
952 backend.set_callback_number(number)
955 def _is_not_special(name):
956 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
960 members = inspect.getmembers(obj)
961 return dict((name, value) for (name, value) in members if _is_not_special(name))
964 def grab_debug_info(username, password):
965 cookieFile = os.path.join(".", "raw_cookies.txt")
967 os.remove(cookieFile)
971 backend = GVoiceBackend(cookieFile)
972 browser = backend._browser
975 ("forward", backend._forwardURL),
976 ("token", backend._tokenURL),
977 ("login", backend._loginURL),
978 ("isdnd", backend._isDndURL),
979 ("account", backend._XML_ACCOUNT_URL),
980 ("contacts", backend._XML_CONTACTS_URL),
981 ("csv", backend._CSV_CONTACTS_URL),
983 ("voicemail", backend._XML_VOICEMAIL_URL),
984 ("sms", backend._XML_SMS_URL),
986 ("recent", backend._XML_RECENT_URL),
987 ("placed", backend._XML_PLACED_URL),
988 ("recieved", backend._XML_RECEIVED_URL),
989 ("missed", backend._XML_MISSED_URL),
993 print "Grabbing pre-login pages"
994 for name, url in _TEST_WEBPAGES:
996 page = browser.download(url)
997 except StandardError, e:
1000 print "\tWriting to file"
1001 with open("not_loggedin_%s.txt" % name, "w") as f:
1005 print "Attempting login"
1006 galxToken = backend._get_token()
1007 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
1008 with open("loggingin.txt", "w") as f:
1009 print "\tWriting to file"
1010 f.write(loginSuccessOrFailurePage)
1012 backend._grab_account_info(loginSuccessOrFailurePage)
1014 # Retry in case the redirect failed
1015 # luckily is_authed does everything we need for a retry
1016 loggedIn = backend.is_authed(True)
1021 print "Grabbing post-login pages"
1022 for name, url in _TEST_WEBPAGES:
1024 page = browser.download(url)
1025 except StandardError, e:
1028 print "\tWriting to file"
1029 with open("loggedin_%s.txt" % name, "w") as f:
1033 browser.save_cookies()
1034 print "\tWriting cookies to file"
1035 with open("cookies.txt", "w") as f:
1037 "%s: %s\n" % (c.name, c.value)
1038 for c in browser._cookies
1044 logging.basicConfig(level=logging.DEBUG)
1050 grab_debug_info(username, password)
1053 if __name__ == "__main__":