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._JSON_SMS_URL = SECURE_URL_BASE + "b/0/request/messages/"
207 self._JSON_SMS_COUNT_URL = SECURE_URL_BASE + "b/0/request/unread"
208 self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/"
209 self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/"
210 self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
211 self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
213 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
214 self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
215 self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
216 self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
218 self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
219 self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
220 self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
221 self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
222 self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
223 self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
224 self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
225 self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
226 self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
227 self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
228 self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
229 self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
231 def is_quick_login_possible(self):
233 @returns True then is_authed might be enough to login, else full login is required
235 return self._loadedFromCookies or 0.0 < self._lastAuthed
237 def is_authed(self, force = False):
239 Attempts to detect a current session
240 @note Once logged in try not to reauth more than once a minute.
241 @returns If authenticated
244 isRecentledAuthed = (time.time() - self._lastAuthed) < 120
245 isPreviouslyAuthed = self._token is not None
246 if isRecentledAuthed and isPreviouslyAuthed and not force:
250 page = self._get_page(self._forwardURL)
251 self._grab_account_info(page)
253 _moduleLogger.exception(str(e))
256 self._browser.save_cookies()
257 self._lastAuthed = time.time()
260 def _get_token(self):
261 tokenPage = self._get_page(self._tokenURL)
263 galxTokens = self._galxRe.search(tokenPage)
264 if galxTokens is not None:
265 galxToken = galxTokens.group(1)
268 _moduleLogger.debug("Could not grab GALX token")
271 def _login(self, username, password, token):
275 'service': "grandcentral",
278 "PersistentCookie": "yes",
280 "continue": self._forwardURL,
283 loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
284 return loginSuccessOrFailurePage
286 def login(self, username, password):
288 Attempt to login to GoogleVoice
289 @returns Whether login was successful or not
293 galxToken = self._get_token()
294 loginSuccessOrFailurePage = self._login(username, password, galxToken)
297 self._grab_account_info(loginSuccessOrFailurePage)
299 # Retry in case the redirect failed
300 # luckily is_authed does everything we need for a retry
301 loggedIn = self.is_authed(True)
303 _moduleLogger.exception(str(e))
305 _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
307 self._browser.save_cookies()
308 self._lastAuthed = time.time()
312 self._browser.save_cookies()
315 self._browser.save_cookies()
317 self._lastAuthed = 0.0
320 self._browser.clear_cookies()
321 self._browser.save_cookies()
323 self._lastAuthed = 0.0
329 isDndPage = self._get_page(self._isDndURL)
331 dndGroup = self._isDndRe.search(isDndPage)
334 dndStatus = dndGroup.group(1)
335 isDnd = True if dndStatus.strip().lower() == "true" else False
338 def set_dnd(self, doNotDisturb):
343 "doNotDisturb": 1 if doNotDisturb else 0,
346 dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
348 def call(self, outgoingNumber):
350 This is the main function responsible for initating the callback
353 outgoingNumber = self._send_validation(outgoingNumber)
354 subscriberNumber = None
355 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
358 'outgoingNumber': outgoingNumber,
359 'forwardingNumber': self._callbackNumber,
360 'subscriberNumber': subscriberNumber or 'undefined',
361 'phoneType': str(phoneType),
364 _moduleLogger.info("%r" % callData)
366 page = self._get_page_with_token(
370 self._parse_with_validation(page)
373 def cancel(self, outgoingNumber=None):
375 Cancels a call matching outgoing and forwarding numbers (if given).
376 Will raise an error if no matching call is being placed
379 page = self._get_page_with_token(
382 'outgoingNumber': outgoingNumber or 'undefined',
383 'forwardingNumber': self._callbackNumber or 'undefined',
387 self._parse_with_validation(page)
389 def send_sms(self, phoneNumbers, message):
393 validatedPhoneNumbers = [
394 self._send_validation(phoneNumber)
395 for phoneNumber in phoneNumbers
397 flattenedPhoneNumbers = ",".join(validatedPhoneNumbers)
398 page = self._get_page_with_token(
401 'phoneNumber': flattenedPhoneNumbers,
402 'text': unicode(message).encode("utf-8"),
405 self._parse_with_validation(page)
407 def search(self, query):
409 Search your Google Voice Account history for calls, voicemails, and sms
410 Returns ``Folder`` instance containting matching messages
413 page = self._get_page(
414 self._XML_SEARCH_URL,
417 json, html = extract_payload(page)
420 def get_feed(self, feed):
424 actualFeed = "_XML_%s_URL" % feed.upper()
425 feedUrl = getattr(self, actualFeed)
427 page = self._get_page(feedUrl)
428 json, html = extract_payload(page)
432 def download(self, messageId, adir):
434 Download a voicemail or recorded call MP3 matching the given ``msg``
435 which can either be a ``Message`` instance, or a SHA1 identifier.
436 Saves files to ``adir`` (defaults to current directory).
437 Message hashes can be found in ``self.voicemail().messages`` for example.
438 @returns location of saved file.
441 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
442 fn = os.path.join(adir, '%s.mp3' % messageId)
443 with open(fn, 'wb') as fo:
447 def is_valid_syntax(self, number):
449 @returns If This number be called ( syntax validation only )
451 return self._validateRe.match(number) is not None
453 def get_account_number(self):
455 @returns The GoogleVoice phone number
457 return self._accountNum
459 def get_callback_numbers(self):
461 @returns a dictionary mapping call back numbers to descriptions
462 @note These results are cached for 30 minutes.
464 if not self.is_authed():
466 return self._callbackNumbers
468 def set_callback_number(self, callbacknumber):
470 Set the number that GoogleVoice calls
471 @param callbacknumber should be a proper 10 digit number
473 self._callbackNumber = callbacknumber
474 _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
477 def get_callback_number(self):
479 @returns Current callback number or None
481 return self._callbackNumber
483 def get_recent(self):
485 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
489 (action, self._get_page(url))
491 ("Received", self._XML_RECEIVED_URL),
492 ("Missed", self._XML_MISSED_URL),
493 ("Placed", self._XML_PLACED_URL),
496 return self._parse_recent(recentPages)
498 def get_contacts(self):
500 @returns Iterable of (contact id, contact name)
503 page = self._get_page(self._JSON_CONTACTS_URL)
504 return self._process_contacts(page)
506 def get_csv_contacts(self):
508 "groupToExport": "mine",
510 "out": "OUTLOOK_CSV",
512 encodedData = urllib.urlencode(data)
513 contacts = self._get_page(self._CSV_CONTACTS_URL+"?"+encodedData)
516 def get_voicemails(self):
520 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
521 voicemailHtml = self._grab_html(voicemailPage)
522 voicemailJson = self._grab_json(voicemailPage)
523 if voicemailJson is None:
525 parsedVoicemail = self._parse_voicemail(voicemailHtml)
526 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
533 smsPage = self._get_page(self._XML_SMS_URL)
534 smsHtml = self._grab_html(smsPage)
535 smsJson = self._grab_json(smsPage)
538 parsedSms = self._parse_sms(smsHtml)
539 smss = self._merge_conversation_sources(parsedSms, smsJson)
542 def get_unread_counts(self):
543 countPage = self._get_page(self._JSON_SMS_COUNT_URL)
544 counts = parse_json(countPage)
545 counts = counts["unreadCounts"]
548 def mark_message(self, messageId, asRead):
553 "read": 1 if asRead else 0,
557 markPage = self._get_page(self._markAsReadURL, postData)
559 def archive_message(self, messageId):
567 markPage = self._get_page(self._archiveMessageURL, postData)
569 def _grab_json(self, flatXml):
570 xmlTree = ElementTree.fromstring(flatXml)
571 jsonElement = xmlTree.getchildren()[0]
572 flatJson = jsonElement.text
573 jsonTree = parse_json(flatJson)
576 def _grab_html(self, flatXml):
577 xmlTree = ElementTree.fromstring(flatXml)
578 htmlElement = xmlTree.getchildren()[1]
579 flatHtml = htmlElement.text
582 def _grab_account_info(self, page):
583 tokenGroup = self._tokenRe.search(page)
584 if tokenGroup is None:
585 raise RuntimeError("Could not extract authentication token from GoogleVoice")
586 self._token = tokenGroup.group(1)
588 anGroup = self._accountNumRe.search(page)
589 if anGroup is not None:
590 self._accountNum = anGroup.group(1)
592 _moduleLogger.debug("Could not extract account number from GoogleVoice")
594 self._callbackNumbers = {}
595 for match in self._callbackRe.finditer(page):
596 callbackNumber = match.group(2)
597 callbackName = match.group(1)
598 self._callbackNumbers[callbackNumber] = callbackName
599 if len(self._callbackNumbers) == 0:
600 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
602 def _send_validation(self, number):
603 if not self.is_valid_syntax(number):
604 raise ValueError('Number is not valid: "%s"' % number)
605 elif not self.is_authed():
606 raise RuntimeError("Not Authenticated")
609 def _parse_recent(self, recentPages):
610 for action, flatXml in recentPages:
611 allRecentHtml = self._grab_html(flatXml)
612 allRecentData = self._parse_history(allRecentHtml)
613 for recentCallData in allRecentData:
614 recentCallData["action"] = action
617 def _process_contacts(self, page):
618 accountData = parse_json(page)
619 for contactId, contactDetails in accountData["contacts"].iteritems():
620 # A zero contact id is the catch all for unknown contacts
622 yield contactId, contactDetails
624 def _parse_history(self, historyHtml):
625 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
626 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
627 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
628 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
629 exactTime = google_strptime(exactTime)
630 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
631 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
632 locationGroup = self._voicemailLocationRegex.search(messageHtml)
633 location = locationGroup.group(1).strip() if locationGroup else ""
635 nameGroup = self._voicemailNameRegex.search(messageHtml)
636 name = nameGroup.group(1).strip() if nameGroup else ""
637 numberGroup = self._voicemailNumberRegex.search(messageHtml)
638 number = numberGroup.group(1).strip() if numberGroup else ""
639 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
640 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
641 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
642 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
645 "id": messageId.strip(),
646 "contactId": contactId,
647 "name": unescape(name),
649 "relTime": relativeTime,
650 "prettyNumber": prettyNumber,
652 "location": unescape(location),
656 def _interpret_voicemail_regex(group):
657 quality, content, number = group.group(2), group.group(3), group.group(4)
659 if quality is not None and content is not None:
660 text.accuracy = quality
661 text.text = unescape(content)
663 elif number is not None:
664 text.accuracy = MessageText.ACCURACY_HIGH
668 def _parse_voicemail(self, voicemailHtml):
669 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
670 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
671 conv = Conversation()
672 conv.type = Conversation.TYPE_VOICEMAIL
673 conv.id = messageId.strip()
675 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
676 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
677 conv.time = google_strptime(exactTimeText)
678 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
679 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
680 locationGroup = self._voicemailLocationRegex.search(messageHtml)
681 conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "")
683 nameGroup = self._voicemailNameRegex.search(messageHtml)
684 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
685 numberGroup = self._voicemailNumberRegex.search(messageHtml)
686 conv.number = numberGroup.group(1).strip() if numberGroup else ""
687 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
688 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
689 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
690 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
692 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
694 self._interpret_voicemail_regex(group)
695 for group in messageGroups
696 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
698 message.body = messageParts
699 message.whoFrom = conv.name
701 message.when = conv.time.strftime("%I:%M %p")
703 _moduleLogger.exception("Confusing time provided: %r" % conv.time)
704 message.when = "Unknown"
705 conv.messages = (message, )
710 def _interpret_sms_message_parts(fromPart, textPart, timePart):
712 text.accuracy = MessageText.ACCURACY_MEDIUM
713 text.text = unescape(textPart)
716 message.body = (text, )
717 message.whoFrom = fromPart
718 message.when = timePart
722 def _parse_sms(self, smsHtml):
723 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
724 for messageId, messageHtml in itergroup(splitSms[1:], 2):
725 conv = Conversation()
726 conv.type = Conversation.TYPE_SMS
727 conv.id = messageId.strip()
729 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
730 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
731 conv.time = google_strptime(exactTimeText)
732 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
733 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
736 nameGroup = self._voicemailNameRegex.search(messageHtml)
737 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
738 numberGroup = self._voicemailNumberRegex.search(messageHtml)
739 conv.number = numberGroup.group(1).strip() if numberGroup else ""
740 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
741 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
742 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
743 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
745 fromGroups = self._smsFromRegex.finditer(messageHtml)
746 fromParts = (group.group(1).strip() for group in fromGroups)
747 textGroups = self._smsTextRegex.finditer(messageHtml)
748 textParts = (group.group(1).strip() for group in textGroups)
749 timeGroups = self._smsTimeRegex.finditer(messageHtml)
750 timeParts = (group.group(1).strip() for group in timeGroups)
752 messageParts = itertools.izip(fromParts, textParts, timeParts)
753 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
754 conv.messages = messages
759 def _merge_conversation_sources(parsedMessages, json):
760 for message in parsedMessages:
761 jsonItem = json["messages"][message.id]
762 message.isRead = jsonItem["isRead"]
763 message.isSpam = jsonItem["isSpam"]
764 message.isTrash = jsonItem["isTrash"]
765 message.isArchived = "inbox" not in jsonItem["labels"]
768 def _get_page(self, url, data = None, refererUrl = None):
770 if refererUrl is not None:
771 headers["Referer"] = refererUrl
773 encodedData = urllib.urlencode(data) if data is not None else None
776 page = self._browser.download(url, encodedData, None, headers)
777 except urllib2.URLError, e:
778 _moduleLogger.error("Translating error: %s" % str(e))
779 raise NetworkError("%s is not accesible" % url)
783 def _get_page_with_token(self, url, data = None, refererUrl = None):
786 data['_rnr_se'] = self._token
788 page = self._get_page(url, data, refererUrl)
792 def _parse_with_validation(self, page):
793 json = parse_json(page)
794 validate_response(json)
798 _UNESCAPE_ENTITIES = {
806 plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
810 def google_strptime(time):
812 Hack: Google always returns the time in the same locale. Sadly if the
813 local system's locale is different, there isn't a way to perfectly handle
814 the time. So instead we handle implement some time formatting
816 abbrevTime = time[:-3]
817 parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
818 if time.endswith("PM"):
819 parsedTime += datetime.timedelta(hours=12)
823 def itergroup(iterator, count, padValue = None):
825 Iterate in groups of 'count' values. If there
826 aren't enough values, the last result is padded with
829 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
833 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
837 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
842 >>> for val in itergroup("123456", 3):
846 >>> for val in itergroup("123456", 3):
847 ... print repr("".join(val))
851 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
852 nIterators = (paddedIterator, ) * count
853 return itertools.izip(*nIterators)
857 _TRUE_REGEX = re.compile("true")
858 _FALSE_REGEX = re.compile("false")
859 _COMMENT_REGEX = re.compile("^\s+//.*$", re.M)
860 s = _TRUE_REGEX.sub("True", s)
861 s = _FALSE_REGEX.sub("False", s)
862 s = _COMMENT_REGEX.sub("#", s)
864 results = eval(s, {}, {})
866 _moduleLogger.exception("Oops")
871 def _fake_parse_json(flattened):
872 return safe_eval(flattened)
875 def _actual_parse_json(flattened):
876 return simplejson.loads(flattened)
879 if simplejson is None:
880 parse_json = _fake_parse_json
882 parse_json = _actual_parse_json
885 def extract_payload(flatXml):
886 xmlTree = ElementTree.fromstring(flatXml)
888 jsonElement = xmlTree.getchildren()[0]
889 flatJson = jsonElement.text
890 jsonTree = parse_json(flatJson)
892 htmlElement = xmlTree.getchildren()[1]
893 flatHtml = htmlElement.text
895 return jsonTree, flatHtml
898 def validate_response(response):
900 Validates that the JSON response is A-OK
903 assert response is not None, "Response not provided"
904 assert 'ok' in response, "Response lacks status"
905 assert response['ok'], "Response not good"
906 except AssertionError:
907 raise RuntimeError('There was a problem with GV: %s' % response)
910 def guess_phone_type(number):
911 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
912 return GVoiceBackend.PHONE_TYPE_GIZMO
914 return GVoiceBackend.PHONE_TYPE_MOBILE
917 def get_sane_callback(backend):
919 Try to set a sane default callback number on these preferences
920 1) 1747 numbers ( Gizmo )
921 2) anything with gizmo in the name
922 3) anything with computer in the name
925 numbers = backend.get_callback_numbers()
927 priorityOrderedCriteria = [
937 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
939 descriptionMatcher = None
940 if numberCriteria is not None:
941 numberMatcher = re.compile(numberCriteria)
942 elif descriptionCriteria is not None:
943 descriptionMatcher = re.compile(descriptionCriteria, re.I)
945 for number, description in numbers.iteritems():
946 if numberMatcher is not None and numberMatcher.match(number) is None:
948 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
953 def set_sane_callback(backend):
955 Try to set a sane default callback number on these preferences
956 1) 1747 numbers ( Gizmo )
957 2) anything with gizmo in the name
958 3) anything with computer in the name
961 number = get_sane_callback(backend)
962 backend.set_callback_number(number)
965 def _is_not_special(name):
966 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
970 members = inspect.getmembers(obj)
971 return dict((name, value) for (name, value) in members if _is_not_special(name))
974 def grab_debug_info(username, password):
975 cookieFile = os.path.join(".", "raw_cookies.txt")
977 os.remove(cookieFile)
981 backend = GVoiceBackend(cookieFile)
982 browser = backend._browser
985 ("forward", backend._forwardURL),
986 ("token", backend._tokenURL),
987 ("login", backend._loginURL),
988 ("isdnd", backend._isDndURL),
989 ("account", backend._XML_ACCOUNT_URL),
990 ("html_contacts", backend._XML_CONTACTS_URL),
991 ("contacts", backend._JSON_CONTACTS_URL),
992 ("csv", backend._CSV_CONTACTS_URL),
994 ("voicemail", backend._XML_VOICEMAIL_URL),
995 ("html_sms", backend._XML_SMS_URL),
996 ("sms", backend._JSON_SMS_URL),
997 ("count", backend._JSON_SMS_COUNT_URL),
999 ("recent", backend._XML_RECENT_URL),
1000 ("placed", backend._XML_PLACED_URL),
1001 ("recieved", backend._XML_RECEIVED_URL),
1002 ("missed", backend._XML_MISSED_URL),
1006 print "Grabbing pre-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("not_loggedin_%s.txt" % name, "w") as f:
1018 print "Attempting login"
1019 galxToken = backend._get_token()
1020 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
1021 with open("loggingin.txt", "w") as f:
1022 print "\tWriting to file"
1023 f.write(loginSuccessOrFailurePage)
1025 backend._grab_account_info(loginSuccessOrFailurePage)
1027 # Retry in case the redirect failed
1028 # luckily is_authed does everything we need for a retry
1029 loggedIn = backend.is_authed(True)
1034 print "Grabbing post-login pages"
1035 for name, url in _TEST_WEBPAGES:
1037 page = browser.download(url)
1038 except StandardError, e:
1041 print "\tWriting to file"
1042 with open("loggedin_%s.txt" % name, "w") as f:
1046 browser.save_cookies()
1047 print "\tWriting cookies to file"
1048 with open("cookies.txt", "w") as f:
1050 "%s: %s\n" % (c.name, c.value)
1051 for c in browser._cookies
1057 logging.basicConfig(level=logging.DEBUG)
1063 grab_debug_info(username, password)
1066 if __name__ == "__main__":