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._JSON_CONTACTS_URL = SECURE_URL_BASE + "b/0/request/user"
193 self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
196 'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms',
197 'recorded', 'placed', 'received', 'missed'
199 self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox"
200 self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred"
201 self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all"
202 self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam"
203 self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash"
204 self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/"
205 self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/"
206 self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/"
207 self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/"
208 self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
209 self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
211 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
212 self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
213 self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
214 self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
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._JSON_CONTACTS_URL)
502 return self._process_contacts(page)
504 def get_csv_contacts(self):
506 "groupToExport": "mine",
508 "out": "OUTLOOK_CSV",
510 encodedData = urllib.urlencode(data)
511 contacts = self._get_page(self._CSV_CONTACTS_URL+"?"+encodedData)
514 def get_voicemails(self):
518 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
519 voicemailHtml = self._grab_html(voicemailPage)
520 voicemailJson = self._grab_json(voicemailPage)
521 if voicemailJson is None:
523 parsedVoicemail = self._parse_voicemail(voicemailHtml)
524 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
531 smsPage = self._get_page(self._XML_SMS_URL)
532 smsHtml = self._grab_html(smsPage)
533 smsJson = self._grab_json(smsPage)
536 parsedSms = self._parse_sms(smsHtml)
537 smss = self._merge_conversation_sources(parsedSms, smsJson)
540 def mark_message(self, messageId, asRead):
545 "read": 1 if asRead else 0,
549 markPage = self._get_page(self._markAsReadURL, postData)
551 def archive_message(self, messageId):
559 markPage = self._get_page(self._archiveMessageURL, postData)
561 def _grab_json(self, flatXml):
562 xmlTree = ElementTree.fromstring(flatXml)
563 jsonElement = xmlTree.getchildren()[0]
564 flatJson = jsonElement.text
565 jsonTree = parse_json(flatJson)
568 def _grab_html(self, flatXml):
569 xmlTree = ElementTree.fromstring(flatXml)
570 htmlElement = xmlTree.getchildren()[1]
571 flatHtml = htmlElement.text
574 def _grab_account_info(self, page):
575 tokenGroup = self._tokenRe.search(page)
576 if tokenGroup is None:
577 raise RuntimeError("Could not extract authentication token from GoogleVoice")
578 self._token = tokenGroup.group(1)
580 anGroup = self._accountNumRe.search(page)
581 if anGroup is not None:
582 self._accountNum = anGroup.group(1)
584 _moduleLogger.debug("Could not extract account number from GoogleVoice")
586 self._callbackNumbers = {}
587 for match in self._callbackRe.finditer(page):
588 callbackNumber = match.group(2)
589 callbackName = match.group(1)
590 self._callbackNumbers[callbackNumber] = callbackName
591 if len(self._callbackNumbers) == 0:
592 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
594 def _send_validation(self, number):
595 if not self.is_valid_syntax(number):
596 raise ValueError('Number is not valid: "%s"' % number)
597 elif not self.is_authed():
598 raise RuntimeError("Not Authenticated")
601 def _parse_recent(self, recentPages):
602 for action, flatXml in recentPages:
603 allRecentHtml = self._grab_html(flatXml)
604 allRecentData = self._parse_history(allRecentHtml)
605 for recentCallData in allRecentData:
606 recentCallData["action"] = action
609 def _process_contacts(self, page):
610 accountData = parse_json(page)
611 for contactId, contactDetails in accountData["contacts"].iteritems():
612 # A zero contact id is the catch all for unknown contacts
614 yield contactId, contactDetails
616 def _parse_history(self, historyHtml):
617 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
618 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
619 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
620 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
621 exactTime = google_strptime(exactTime)
622 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
623 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
624 locationGroup = self._voicemailLocationRegex.search(messageHtml)
625 location = locationGroup.group(1).strip() if locationGroup else ""
627 nameGroup = self._voicemailNameRegex.search(messageHtml)
628 name = nameGroup.group(1).strip() if nameGroup else ""
629 numberGroup = self._voicemailNumberRegex.search(messageHtml)
630 number = numberGroup.group(1).strip() if numberGroup else ""
631 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
632 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
633 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
634 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
637 "id": messageId.strip(),
638 "contactId": contactId,
639 "name": unescape(name),
641 "relTime": relativeTime,
642 "prettyNumber": prettyNumber,
644 "location": unescape(location),
648 def _interpret_voicemail_regex(group):
649 quality, content, number = group.group(2), group.group(3), group.group(4)
651 if quality is not None and content is not None:
652 text.accuracy = quality
653 text.text = unescape(content)
655 elif number is not None:
656 text.accuracy = MessageText.ACCURACY_HIGH
660 def _parse_voicemail(self, voicemailHtml):
661 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
662 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
663 conv = Conversation()
664 conv.type = Conversation.TYPE_VOICEMAIL
665 conv.id = messageId.strip()
667 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
668 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
669 conv.time = google_strptime(exactTimeText)
670 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
671 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
672 locationGroup = self._voicemailLocationRegex.search(messageHtml)
673 conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "")
675 nameGroup = self._voicemailNameRegex.search(messageHtml)
676 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
677 numberGroup = self._voicemailNumberRegex.search(messageHtml)
678 conv.number = numberGroup.group(1).strip() if numberGroup else ""
679 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
680 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
681 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
682 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
684 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
686 self._interpret_voicemail_regex(group)
687 for group in messageGroups
688 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
690 message.body = messageParts
691 message.whoFrom = conv.name
692 message.when = conv.time.strftime("%I:%M %p")
693 conv.messages = (message, )
698 def _interpret_sms_message_parts(fromPart, textPart, timePart):
700 text.accuracy = MessageText.ACCURACY_MEDIUM
701 text.text = unescape(textPart)
704 message.body = (text, )
705 message.whoFrom = fromPart
706 message.when = timePart
710 def _parse_sms(self, smsHtml):
711 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
712 for messageId, messageHtml in itergroup(splitSms[1:], 2):
713 conv = Conversation()
714 conv.type = Conversation.TYPE_SMS
715 conv.id = messageId.strip()
717 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
718 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
719 conv.time = google_strptime(exactTimeText)
720 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
721 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
724 nameGroup = self._voicemailNameRegex.search(messageHtml)
725 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
726 numberGroup = self._voicemailNumberRegex.search(messageHtml)
727 conv.number = numberGroup.group(1).strip() if numberGroup else ""
728 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
729 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
730 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
731 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
733 fromGroups = self._smsFromRegex.finditer(messageHtml)
734 fromParts = (group.group(1).strip() for group in fromGroups)
735 textGroups = self._smsTextRegex.finditer(messageHtml)
736 textParts = (group.group(1).strip() for group in textGroups)
737 timeGroups = self._smsTimeRegex.finditer(messageHtml)
738 timeParts = (group.group(1).strip() for group in timeGroups)
740 messageParts = itertools.izip(fromParts, textParts, timeParts)
741 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
742 conv.messages = messages
747 def _merge_conversation_sources(parsedMessages, json):
748 for message in parsedMessages:
749 jsonItem = json["messages"][message.id]
750 message.isRead = jsonItem["isRead"]
751 message.isSpam = jsonItem["isSpam"]
752 message.isTrash = jsonItem["isTrash"]
753 message.isArchived = "inbox" not in jsonItem["labels"]
756 def _get_page(self, url, data = None, refererUrl = None):
758 if refererUrl is not None:
759 headers["Referer"] = refererUrl
761 encodedData = urllib.urlencode(data) if data is not None else None
764 page = self._browser.download(url, encodedData, None, headers)
765 except urllib2.URLError, e:
766 _moduleLogger.error("Translating error: %s" % str(e))
767 raise NetworkError("%s is not accesible" % url)
771 def _get_page_with_token(self, url, data = None, refererUrl = None):
774 data['_rnr_se'] = self._token
776 page = self._get_page(url, data, refererUrl)
780 def _parse_with_validation(self, page):
781 json = parse_json(page)
782 validate_response(json)
786 _UNESCAPE_ENTITIES = {
794 plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
798 def google_strptime(time):
800 Hack: Google always returns the time in the same locale. Sadly if the
801 local system's locale is different, there isn't a way to perfectly handle
802 the time. So instead we handle implement some time formatting
804 abbrevTime = time[:-3]
805 parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
806 if time.endswith("PM"):
807 parsedTime += datetime.timedelta(hours=12)
811 def itergroup(iterator, count, padValue = None):
813 Iterate in groups of 'count' values. If there
814 aren't enough values, the last result is padded with
817 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
821 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
825 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
830 >>> for val in itergroup("123456", 3):
834 >>> for val in itergroup("123456", 3):
835 ... print repr("".join(val))
839 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
840 nIterators = (paddedIterator, ) * count
841 return itertools.izip(*nIterators)
845 _TRUE_REGEX = re.compile("true")
846 _FALSE_REGEX = re.compile("false")
847 _COMMENT_REGEX = re.compile("^\s+//.*$", re.M)
848 s = _TRUE_REGEX.sub("True", s)
849 s = _FALSE_REGEX.sub("False", s)
850 s = _COMMENT_REGEX.sub("#", s)
852 results = eval(s, {}, {})
854 _moduleLogger.exception("Oops")
859 def _fake_parse_json(flattened):
860 return safe_eval(flattened)
863 def _actual_parse_json(flattened):
864 return simplejson.loads(flattened)
867 if simplejson is None:
868 parse_json = _fake_parse_json
870 parse_json = _actual_parse_json
873 def extract_payload(flatXml):
874 xmlTree = ElementTree.fromstring(flatXml)
876 jsonElement = xmlTree.getchildren()[0]
877 flatJson = jsonElement.text
878 jsonTree = parse_json(flatJson)
880 htmlElement = xmlTree.getchildren()[1]
881 flatHtml = htmlElement.text
883 return jsonTree, flatHtml
886 def validate_response(response):
888 Validates that the JSON response is A-OK
891 assert response is not None
892 assert 'ok' in response
893 assert response['ok']
894 except AssertionError:
895 raise RuntimeError('There was a problem with GV: %s' % response)
898 def guess_phone_type(number):
899 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
900 return GVoiceBackend.PHONE_TYPE_GIZMO
902 return GVoiceBackend.PHONE_TYPE_MOBILE
905 def get_sane_callback(backend):
907 Try to set a sane default callback number on these preferences
908 1) 1747 numbers ( Gizmo )
909 2) anything with gizmo in the name
910 3) anything with computer in the name
913 numbers = backend.get_callback_numbers()
915 priorityOrderedCriteria = [
925 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
927 descriptionMatcher = None
928 if numberCriteria is not None:
929 numberMatcher = re.compile(numberCriteria)
930 elif descriptionCriteria is not None:
931 descriptionMatcher = re.compile(descriptionCriteria, re.I)
933 for number, description in numbers.iteritems():
934 if numberMatcher is not None and numberMatcher.match(number) is None:
936 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
941 def set_sane_callback(backend):
943 Try to set a sane default callback number on these preferences
944 1) 1747 numbers ( Gizmo )
945 2) anything with gizmo in the name
946 3) anything with computer in the name
949 number = get_sane_callback(backend)
950 backend.set_callback_number(number)
953 def _is_not_special(name):
954 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
958 members = inspect.getmembers(obj)
959 return dict((name, value) for (name, value) in members if _is_not_special(name))
962 def grab_debug_info(username, password):
963 cookieFile = os.path.join(".", "raw_cookies.txt")
965 os.remove(cookieFile)
969 backend = GVoiceBackend(cookieFile)
970 browser = backend._browser
973 ("forward", backend._forwardURL),
974 ("token", backend._tokenURL),
975 ("login", backend._loginURL),
976 ("isdnd", backend._isDndURL),
977 ("account", backend._XML_ACCOUNT_URL),
978 ("contacts", backend._XML_CONTACTS_URL),
979 ("csv", backend._CSV_CONTACTS_URL),
981 ("voicemail", backend._XML_VOICEMAIL_URL),
982 ("sms", backend._XML_SMS_URL),
984 ("recent", backend._XML_RECENT_URL),
985 ("placed", backend._XML_PLACED_URL),
986 ("recieved", backend._XML_RECEIVED_URL),
987 ("missed", backend._XML_MISSED_URL),
991 print "Grabbing pre-login pages"
992 for name, url in _TEST_WEBPAGES:
994 page = browser.download(url)
995 except StandardError, e:
998 print "\tWriting to file"
999 with open("not_loggedin_%s.txt" % name, "w") as f:
1003 print "Attempting login"
1004 galxToken = backend._get_token()
1005 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
1006 with open("loggingin.txt", "w") as f:
1007 print "\tWriting to file"
1008 f.write(loginSuccessOrFailurePage)
1010 backend._grab_account_info(loginSuccessOrFailurePage)
1012 # Retry in case the redirect failed
1013 # luckily is_authed does everything we need for a retry
1014 loggedIn = backend.is_authed(True)
1019 print "Grabbing post-login pages"
1020 for name, url in _TEST_WEBPAGES:
1022 page = browser.download(url)
1023 except StandardError, e:
1026 print "\tWriting to file"
1027 with open("loggedin_%s.txt" % name, "w") as f:
1031 browser.save_cookies()
1032 print "\tWriting cookies to file"
1033 with open("cookies.txt", "w") as f:
1035 "%s: %s\n" % (c.name, c.value)
1036 for c in browser._cookies
1042 logging.basicConfig(level=logging.DEBUG)
1048 grab_debug_info(username, password)
1051 if __name__ == "__main__":