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 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 get_unread_counts(self):
541 countPage = self._get_page(self._JSON_SMS_COUNT_URL)
542 counts = parse_json(countPage)
543 counts = counts["unreadCounts"]
546 def mark_message(self, messageId, asRead):
551 "read": 1 if asRead else 0,
555 markPage = self._get_page(self._markAsReadURL, postData)
557 def archive_message(self, messageId):
565 markPage = self._get_page(self._archiveMessageURL, postData)
567 def _grab_json(self, flatXml):
568 xmlTree = ElementTree.fromstring(flatXml)
569 jsonElement = xmlTree.getchildren()[0]
570 flatJson = jsonElement.text
571 jsonTree = parse_json(flatJson)
574 def _grab_html(self, flatXml):
575 xmlTree = ElementTree.fromstring(flatXml)
576 htmlElement = xmlTree.getchildren()[1]
577 flatHtml = htmlElement.text
580 def _grab_account_info(self, page):
581 tokenGroup = self._tokenRe.search(page)
582 if tokenGroup is None:
583 raise RuntimeError("Could not extract authentication token from GoogleVoice")
584 self._token = tokenGroup.group(1)
586 anGroup = self._accountNumRe.search(page)
587 if anGroup is not None:
588 self._accountNum = anGroup.group(1)
590 _moduleLogger.debug("Could not extract account number from GoogleVoice")
592 self._callbackNumbers = {}
593 for match in self._callbackRe.finditer(page):
594 callbackNumber = match.group(2)
595 callbackName = match.group(1)
596 self._callbackNumbers[callbackNumber] = callbackName
597 if len(self._callbackNumbers) == 0:
598 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
600 def _send_validation(self, number):
601 if not self.is_valid_syntax(number):
602 raise ValueError('Number is not valid: "%s"' % number)
603 elif not self.is_authed():
604 raise RuntimeError("Not Authenticated")
607 def _parse_recent(self, recentPages):
608 for action, flatXml in recentPages:
609 allRecentHtml = self._grab_html(flatXml)
610 allRecentData = self._parse_history(allRecentHtml)
611 for recentCallData in allRecentData:
612 recentCallData["action"] = action
615 def _process_contacts(self, page):
616 accountData = parse_json(page)
617 for contactId, contactDetails in accountData["contacts"].iteritems():
618 # A zero contact id is the catch all for unknown contacts
620 yield contactId, contactDetails
622 def _parse_history(self, historyHtml):
623 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
624 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
625 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
626 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
627 exactTime = google_strptime(exactTime)
628 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
629 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
630 locationGroup = self._voicemailLocationRegex.search(messageHtml)
631 location = locationGroup.group(1).strip() if locationGroup else ""
633 nameGroup = self._voicemailNameRegex.search(messageHtml)
634 name = nameGroup.group(1).strip() if nameGroup else ""
635 numberGroup = self._voicemailNumberRegex.search(messageHtml)
636 number = numberGroup.group(1).strip() if numberGroup else ""
637 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
638 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
639 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
640 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
643 "id": messageId.strip(),
644 "contactId": contactId,
645 "name": unescape(name),
647 "relTime": relativeTime,
648 "prettyNumber": prettyNumber,
650 "location": unescape(location),
654 def _interpret_voicemail_regex(group):
655 quality, content, number = group.group(2), group.group(3), group.group(4)
657 if quality is not None and content is not None:
658 text.accuracy = quality
659 text.text = unescape(content)
661 elif number is not None:
662 text.accuracy = MessageText.ACCURACY_HIGH
666 def _parse_voicemail(self, voicemailHtml):
667 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
668 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
669 conv = Conversation()
670 conv.type = Conversation.TYPE_VOICEMAIL
671 conv.id = messageId.strip()
673 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
674 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
675 conv.time = google_strptime(exactTimeText)
676 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
677 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
678 locationGroup = self._voicemailLocationRegex.search(messageHtml)
679 conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "")
681 nameGroup = self._voicemailNameRegex.search(messageHtml)
682 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
683 numberGroup = self._voicemailNumberRegex.search(messageHtml)
684 conv.number = numberGroup.group(1).strip() if numberGroup else ""
685 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
686 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
687 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
688 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
690 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
692 self._interpret_voicemail_regex(group)
693 for group in messageGroups
694 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
696 message.body = messageParts
697 message.whoFrom = conv.name
699 message.when = conv.time.strftime("%I:%M %p")
701 _moduleLogger.exception("Confusing time provided: %r" % conv.time)
702 message.when = "Unknown"
703 conv.messages = (message, )
708 def _interpret_sms_message_parts(fromPart, textPart, timePart):
710 text.accuracy = MessageText.ACCURACY_MEDIUM
711 text.text = unescape(textPart)
714 message.body = (text, )
715 message.whoFrom = fromPart
716 message.when = timePart
720 def _parse_sms(self, smsHtml):
721 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
722 for messageId, messageHtml in itergroup(splitSms[1:], 2):
723 conv = Conversation()
724 conv.type = Conversation.TYPE_SMS
725 conv.id = messageId.strip()
727 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
728 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
729 conv.time = google_strptime(exactTimeText)
730 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
731 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
734 nameGroup = self._voicemailNameRegex.search(messageHtml)
735 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
736 numberGroup = self._voicemailNumberRegex.search(messageHtml)
737 conv.number = numberGroup.group(1).strip() if numberGroup else ""
738 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
739 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
740 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
741 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
743 fromGroups = self._smsFromRegex.finditer(messageHtml)
744 fromParts = (group.group(1).strip() for group in fromGroups)
745 textGroups = self._smsTextRegex.finditer(messageHtml)
746 textParts = (group.group(1).strip() for group in textGroups)
747 timeGroups = self._smsTimeRegex.finditer(messageHtml)
748 timeParts = (group.group(1).strip() for group in timeGroups)
750 messageParts = itertools.izip(fromParts, textParts, timeParts)
751 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
752 conv.messages = messages
757 def _merge_conversation_sources(parsedMessages, json):
758 for message in parsedMessages:
759 jsonItem = json["messages"][message.id]
760 message.isRead = jsonItem["isRead"]
761 message.isSpam = jsonItem["isSpam"]
762 message.isTrash = jsonItem["isTrash"]
763 message.isArchived = "inbox" not in jsonItem["labels"]
766 def _get_page(self, url, data = None, refererUrl = None):
768 if refererUrl is not None:
769 headers["Referer"] = refererUrl
771 encodedData = urllib.urlencode(data) if data is not None else None
774 page = self._browser.download(url, encodedData, None, headers)
775 except urllib2.URLError, e:
776 _moduleLogger.error("Translating error: %s" % str(e))
777 raise NetworkError("%s is not accesible" % url)
781 def _get_page_with_token(self, url, data = None, refererUrl = None):
784 data['_rnr_se'] = self._token
786 page = self._get_page(url, data, refererUrl)
790 def _parse_with_validation(self, page):
791 json = parse_json(page)
792 self._validate_response(json)
795 def _validate_response(self, response):
797 Validates that the JSON response is A-OK
800 assert response is not None, "Response not provided"
801 assert 'ok' in response, "Response lacks status"
802 assert response['ok'], "Response not good"
803 except AssertionError:
805 if response["data"]["code"] == 20:
807 """Ambiguous error 20 returned by Google Voice.
808 Please verify you have configured your callback number (currently "%s"). If it is configured some other suspected causes are: non-verified callback numbers, and Gizmo5 callback numbers.""" % self._callbackNumber)
811 raise RuntimeError('There was a problem with GV: %s' % response)
814 _UNESCAPE_ENTITIES = {
822 plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
826 def google_strptime(time):
828 Hack: Google always returns the time in the same locale. Sadly if the
829 local system's locale is different, there isn't a way to perfectly handle
830 the time. So instead we handle implement some time formatting
832 abbrevTime = time[:-3]
833 parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
834 if time.endswith("PM"):
835 parsedTime += datetime.timedelta(hours=12)
839 def itergroup(iterator, count, padValue = None):
841 Iterate in groups of 'count' values. If there
842 aren't enough values, the last result is padded with
845 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
849 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
853 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
858 >>> for val in itergroup("123456", 3):
862 >>> for val in itergroup("123456", 3):
863 ... print repr("".join(val))
867 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
868 nIterators = (paddedIterator, ) * count
869 return itertools.izip(*nIterators)
873 _TRUE_REGEX = re.compile("true")
874 _FALSE_REGEX = re.compile("false")
875 _COMMENT_REGEX = re.compile("^\s+//.*$", re.M)
876 s = _TRUE_REGEX.sub("True", s)
877 s = _FALSE_REGEX.sub("False", s)
878 s = _COMMENT_REGEX.sub("#", s)
880 results = eval(s, {}, {})
882 _moduleLogger.exception("Oops")
887 def _fake_parse_json(flattened):
888 return safe_eval(flattened)
891 def _actual_parse_json(flattened):
892 return simplejson.loads(flattened)
895 if simplejson is None:
896 parse_json = _fake_parse_json
898 parse_json = _actual_parse_json
901 def extract_payload(flatXml):
902 xmlTree = ElementTree.fromstring(flatXml)
904 jsonElement = xmlTree.getchildren()[0]
905 flatJson = jsonElement.text
906 jsonTree = parse_json(flatJson)
908 htmlElement = xmlTree.getchildren()[1]
909 flatHtml = htmlElement.text
911 return jsonTree, flatHtml
914 def guess_phone_type(number):
915 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
916 return GVoiceBackend.PHONE_TYPE_GIZMO
918 return GVoiceBackend.PHONE_TYPE_MOBILE
921 def get_sane_callback(backend):
923 Try to set a sane default callback number on these preferences
924 1) 1747 numbers ( Gizmo )
925 2) anything with gizmo in the name
926 3) anything with computer in the name
929 numbers = backend.get_callback_numbers()
931 priorityOrderedCriteria = [
941 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
943 descriptionMatcher = None
944 if numberCriteria is not None:
945 numberMatcher = re.compile(numberCriteria)
946 elif descriptionCriteria is not None:
947 descriptionMatcher = re.compile(descriptionCriteria, re.I)
949 for number, description in numbers.iteritems():
950 if numberMatcher is not None and numberMatcher.match(number) is None:
952 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
957 def set_sane_callback(backend):
959 Try to set a sane default callback number on these preferences
960 1) 1747 numbers ( Gizmo )
961 2) anything with gizmo in the name
962 3) anything with computer in the name
965 number = get_sane_callback(backend)
966 backend.set_callback_number(number)
969 def _is_not_special(name):
970 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
974 members = inspect.getmembers(obj)
975 return dict((name, value) for (name, value) in members if _is_not_special(name))
978 def grab_debug_info(username, password):
979 cookieFile = os.path.join(".", "raw_cookies.txt")
981 os.remove(cookieFile)
985 backend = GVoiceBackend(cookieFile)
986 browser = backend._browser
989 ("forward", backend._forwardURL),
990 ("token", backend._tokenURL),
991 ("login", backend._loginURL),
992 ("isdnd", backend._isDndURL),
993 ("account", backend._XML_ACCOUNT_URL),
994 ("html_contacts", backend._XML_CONTACTS_URL),
995 ("contacts", backend._JSON_CONTACTS_URL),
996 ("csv", backend._CSV_CONTACTS_URL),
998 ("voicemail", backend._XML_VOICEMAIL_URL),
999 ("html_sms", backend._XML_SMS_URL),
1000 ("sms", backend._JSON_SMS_URL),
1001 ("count", backend._JSON_SMS_COUNT_URL),
1003 ("recent", backend._XML_RECENT_URL),
1004 ("placed", backend._XML_PLACED_URL),
1005 ("recieved", backend._XML_RECEIVED_URL),
1006 ("missed", backend._XML_MISSED_URL),
1010 print "Grabbing pre-login pages"
1011 for name, url in _TEST_WEBPAGES:
1013 page = browser.download(url)
1014 except StandardError, e:
1017 print "\tWriting to file"
1018 with open("not_loggedin_%s.txt" % name, "w") as f:
1022 print "Attempting login"
1023 galxToken = backend._get_token()
1024 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
1025 with open("loggingin.txt", "w") as f:
1026 print "\tWriting to file"
1027 f.write(loginSuccessOrFailurePage)
1029 backend._grab_account_info(loginSuccessOrFailurePage)
1031 # Retry in case the redirect failed
1032 # luckily is_authed does everything we need for a retry
1033 loggedIn = backend.is_authed(True)
1038 print "Grabbing post-login pages"
1039 for name, url in _TEST_WEBPAGES:
1041 page = browser.download(url)
1042 except StandardError, e:
1045 print "\tWriting to file"
1046 with open("loggedin_%s.txt" % name, "w") as f:
1050 browser.save_cookies()
1051 print "\tWriting cookies to file"
1052 with open("cookies.txt", "w") as f:
1054 "%s: %s\n" % (c.name, c.value)
1055 for c in browser._cookies
1061 logging.basicConfig(level=logging.DEBUG)
1067 grab_debug_info(username, password)
1070 if __name__ == "__main__":