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._tokenURL = SECURE_URL_BASE + "m"
174 self._callUrl = SECURE_URL_BASE + "call/connect"
175 self._callCancelURL = SECURE_URL_BASE + "call/cancel"
176 self._sendSmsURL = SECURE_URL_BASE + "sms/send"
178 self._isDndURL = "https://www.google.com/voice/m/donotdisturb"
179 self._isDndRe = re.compile(r"""<input.*?id="doNotDisturb".*?checked="(.*?)"\s*/>""")
180 self._setDndURL = "https://www.google.com/voice/m/savednd"
182 self._downloadVoicemailURL = SECURE_URL_BASE + "media/send_voicemail/"
183 self._markAsReadURL = SECURE_URL_BASE + "m/mark"
184 self._archiveMessageURL = SECURE_URL_BASE + "m/archive"
186 self._XML_SEARCH_URL = SECURE_URL_BASE + "inbox/search/"
187 self._XML_ACCOUNT_URL = SECURE_URL_BASE + "contacts/"
188 # HACK really this redirects to the main pge and we are grabbing some javascript
189 self._XML_CONTACTS_URL = "http://www.google.com/voice/inbox/search/contact"
190 self._CSV_CONTACTS_URL = "http://mail.google.com/mail/contacts/data/export"
191 self._JSON_CONTACTS_URL = SECURE_URL_BASE + "b/0/request/user"
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._JSON_SMS_URL = SECURE_URL_BASE + "b/0/request/messages/"
206 self._JSON_SMS_COUNT_URL = SECURE_URL_BASE + "b/0/request/unread"
207 self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/"
208 self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/"
209 self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
210 self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
212 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
214 self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
215 self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
216 self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
217 self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
218 self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
219 self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
220 self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
221 self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
222 self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
223 self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
224 self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
225 self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
227 def is_quick_login_possible(self):
229 @returns True then refresh_account_info might be enough to login, else full login is required
231 return self._loadedFromCookies or 0.0 < self._lastAuthed
233 def refresh_account_info(self):
235 page = self._get_page(self._JSON_CONTACTS_URL)
236 accountData = self._grab_account_info(page)
238 _moduleLogger.exception(str(e))
241 self._browser.save_cookies()
242 self._lastAuthed = time.time()
245 def _get_token(self):
246 tokenPage = self._get_page(self._tokenURL)
248 galxTokens = self._galxRe.search(tokenPage)
249 if galxTokens is not None:
250 galxToken = galxTokens.group(1)
253 _moduleLogger.debug("Could not grab GALX token")
256 def _login(self, username, password, token):
260 'service': "grandcentral",
263 "PersistentCookie": "yes",
265 "continue": self._JSON_CONTACTS_URL,
268 loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
269 return loginSuccessOrFailurePage
271 def login(self, username, password):
273 Attempt to login to GoogleVoice
274 @returns Whether login was successful or not
278 galxToken = self._get_token()
279 loginSuccessOrFailurePage = self._login(username, password, galxToken)
282 accountData = self._grab_account_info(loginSuccessOrFailurePage)
284 # Retry in case the redirect failed
285 # luckily refresh_account_info does everything we need for a retry
286 accountData = self.refresh_account_info()
287 if accountData is None:
288 _moduleLogger.exception(str(e))
290 _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
292 self._browser.save_cookies()
293 self._lastAuthed = time.time()
297 self._browser.save_cookies()
300 self._browser.save_cookies()
302 self._lastAuthed = 0.0
305 self._browser.clear_cookies()
306 self._browser.save_cookies()
308 self._lastAuthed = 0.0
314 isDndPage = self._get_page(self._isDndURL)
316 dndGroup = self._isDndRe.search(isDndPage)
319 dndStatus = dndGroup.group(1)
320 isDnd = True if dndStatus.strip().lower() == "true" else False
323 def set_dnd(self, doNotDisturb):
328 "doNotDisturb": 1 if doNotDisturb else 0,
331 dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
333 def call(self, outgoingNumber):
335 This is the main function responsible for initating the callback
338 outgoingNumber = self._send_validation(outgoingNumber)
339 subscriberNumber = None
340 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
343 'outgoingNumber': outgoingNumber,
344 'forwardingNumber': self._callbackNumber,
345 'subscriberNumber': subscriberNumber or 'undefined',
346 'phoneType': str(phoneType),
349 _moduleLogger.info("%r" % callData)
351 page = self._get_page_with_token(
355 self._parse_with_validation(page)
358 def cancel(self, outgoingNumber=None):
360 Cancels a call matching outgoing and forwarding numbers (if given).
361 Will raise an error if no matching call is being placed
364 page = self._get_page_with_token(
367 'outgoingNumber': outgoingNumber or 'undefined',
368 'forwardingNumber': self._callbackNumber or 'undefined',
372 self._parse_with_validation(page)
374 def send_sms(self, phoneNumbers, message):
378 validatedPhoneNumbers = [
379 self._send_validation(phoneNumber)
380 for phoneNumber in phoneNumbers
382 flattenedPhoneNumbers = ",".join(validatedPhoneNumbers)
383 page = self._get_page_with_token(
386 'phoneNumber': flattenedPhoneNumbers,
387 'text': unicode(message).encode("utf-8"),
390 self._parse_with_validation(page)
392 def search(self, query):
394 Search your Google Voice Account history for calls, voicemails, and sms
395 Returns ``Folder`` instance containting matching messages
398 page = self._get_page(
399 self._XML_SEARCH_URL,
402 json, html = extract_payload(page)
405 def get_feed(self, feed):
409 actualFeed = "_XML_%s_URL" % feed.upper()
410 feedUrl = getattr(self, actualFeed)
412 page = self._get_page(feedUrl)
413 json, html = extract_payload(page)
417 def download(self, messageId, adir):
419 Download a voicemail or recorded call MP3 matching the given ``msg``
420 which can either be a ``Message`` instance, or a SHA1 identifier.
421 Saves files to ``adir`` (defaults to current directory).
422 Message hashes can be found in ``self.voicemail().messages`` for example.
423 @returns location of saved file.
426 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
427 fn = os.path.join(adir, '%s.mp3' % messageId)
428 with open(fn, 'wb') as fo:
432 def is_valid_syntax(self, number):
434 @returns If This number be called ( syntax validation only )
436 return self._validateRe.match(number) is not None
438 def get_account_number(self):
440 @returns The GoogleVoice phone number
442 return self._accountNum
444 def get_callback_numbers(self):
446 @returns a dictionary mapping call back numbers to descriptions
447 @note These results are cached for 30 minutes.
449 return self._callbackNumbers
451 def set_callback_number(self, callbacknumber):
453 Set the number that GoogleVoice calls
454 @param callbacknumber should be a proper 10 digit number
456 self._callbackNumber = callbacknumber
457 _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
460 def get_callback_number(self):
462 @returns Current callback number or None
464 return self._callbackNumber
466 def get_recent(self):
468 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
472 (action, self._get_page(url))
474 ("Received", self._XML_RECEIVED_URL),
475 ("Missed", self._XML_MISSED_URL),
476 ("Placed", self._XML_PLACED_URL),
479 return self._parse_recent(recentPages)
481 def get_contacts(self):
483 @returns Iterable of (contact id, contact name)
486 page = self._get_page(self._JSON_CONTACTS_URL)
487 return self._process_contacts(page)
489 def get_csv_contacts(self):
491 "groupToExport": "mine",
493 "out": "OUTLOOK_CSV",
495 encodedData = urllib.urlencode(data)
496 contacts = self._get_page(self._CSV_CONTACTS_URL+"?"+encodedData)
499 def get_voicemails(self):
503 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
504 voicemailHtml = self._grab_html(voicemailPage)
505 voicemailJson = self._grab_json(voicemailPage)
506 if voicemailJson is None:
508 parsedVoicemail = self._parse_voicemail(voicemailHtml)
509 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
516 smsPage = self._get_page(self._XML_SMS_URL)
517 smsHtml = self._grab_html(smsPage)
518 smsJson = self._grab_json(smsPage)
521 parsedSms = self._parse_sms(smsHtml)
522 smss = self._merge_conversation_sources(parsedSms, smsJson)
525 def get_unread_counts(self):
526 countPage = self._get_page(self._JSON_SMS_COUNT_URL)
527 counts = parse_json(countPage)
528 counts = counts["unreadCounts"]
531 def mark_message(self, messageId, asRead):
536 "read": 1 if asRead else 0,
540 markPage = self._get_page(self._markAsReadURL, postData)
542 def archive_message(self, messageId):
550 markPage = self._get_page(self._archiveMessageURL, postData)
552 def _grab_json(self, flatXml):
553 xmlTree = ElementTree.fromstring(flatXml)
554 jsonElement = xmlTree.getchildren()[0]
555 flatJson = jsonElement.text
556 jsonTree = parse_json(flatJson)
559 def _grab_html(self, flatXml):
560 xmlTree = ElementTree.fromstring(flatXml)
561 htmlElement = xmlTree.getchildren()[1]
562 flatHtml = htmlElement.text
565 def _grab_account_info(self, page):
566 accountData = parse_json(page)
567 self._token = accountData["r"]
568 self._accountNum = accountData["number"]["raw"]
569 for callback in accountData["phones"].itervalues():
570 self._callbackNumbers[callback["phoneNumber"]] = callback["name"]
571 if len(self._callbackNumbers) == 0:
572 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
575 def _send_validation(self, number):
576 if not self.is_valid_syntax(number):
577 raise ValueError('Number is not valid: "%s"' % number)
580 def _parse_recent(self, recentPages):
581 for action, flatXml in recentPages:
582 allRecentHtml = self._grab_html(flatXml)
583 allRecentData = self._parse_history(allRecentHtml)
584 for recentCallData in allRecentData:
585 recentCallData["action"] = action
588 def _process_contacts(self, page):
589 accountData = parse_json(page)
590 for contactId, contactDetails in accountData["contacts"].iteritems():
591 # A zero contact id is the catch all for unknown contacts
593 yield contactId, contactDetails
595 def _parse_history(self, historyHtml):
596 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
597 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
598 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
599 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
600 exactTime = google_strptime(exactTime)
601 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
602 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
603 locationGroup = self._voicemailLocationRegex.search(messageHtml)
604 location = locationGroup.group(1).strip() if locationGroup else ""
606 nameGroup = self._voicemailNameRegex.search(messageHtml)
607 name = nameGroup.group(1).strip() if nameGroup else ""
608 numberGroup = self._voicemailNumberRegex.search(messageHtml)
609 number = numberGroup.group(1).strip() if numberGroup else ""
610 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
611 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
612 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
613 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
616 "id": messageId.strip(),
617 "contactId": contactId,
618 "name": unescape(name),
620 "relTime": relativeTime,
621 "prettyNumber": prettyNumber,
623 "location": unescape(location),
627 def _interpret_voicemail_regex(group):
628 quality, content, number = group.group(2), group.group(3), group.group(4)
630 if quality is not None and content is not None:
631 text.accuracy = quality
632 text.text = unescape(content)
634 elif number is not None:
635 text.accuracy = MessageText.ACCURACY_HIGH
639 def _parse_voicemail(self, voicemailHtml):
640 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
641 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
642 conv = Conversation()
643 conv.type = Conversation.TYPE_VOICEMAIL
644 conv.id = messageId.strip()
646 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
647 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
648 conv.time = google_strptime(exactTimeText)
649 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
650 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
651 locationGroup = self._voicemailLocationRegex.search(messageHtml)
652 conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "")
654 nameGroup = self._voicemailNameRegex.search(messageHtml)
655 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
656 numberGroup = self._voicemailNumberRegex.search(messageHtml)
657 conv.number = numberGroup.group(1).strip() if numberGroup else ""
658 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
659 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
660 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
661 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
663 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
665 self._interpret_voicemail_regex(group)
666 for group in messageGroups
667 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
669 message.body = messageParts
670 message.whoFrom = conv.name
672 message.when = conv.time.strftime("%I:%M %p")
674 _moduleLogger.exception("Confusing time provided: %r" % conv.time)
675 message.when = "Unknown"
676 conv.messages = (message, )
681 def _interpret_sms_message_parts(fromPart, textPart, timePart):
683 text.accuracy = MessageText.ACCURACY_MEDIUM
684 text.text = unescape(textPart)
687 message.body = (text, )
688 message.whoFrom = fromPart
689 message.when = timePart
693 def _parse_sms(self, smsHtml):
694 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
695 for messageId, messageHtml in itergroup(splitSms[1:], 2):
696 conv = Conversation()
697 conv.type = Conversation.TYPE_SMS
698 conv.id = messageId.strip()
700 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
701 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
702 conv.time = google_strptime(exactTimeText)
703 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
704 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
707 nameGroup = self._voicemailNameRegex.search(messageHtml)
708 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
709 numberGroup = self._voicemailNumberRegex.search(messageHtml)
710 conv.number = numberGroup.group(1).strip() if numberGroup else ""
711 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
712 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
713 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
714 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
716 fromGroups = self._smsFromRegex.finditer(messageHtml)
717 fromParts = (group.group(1).strip() for group in fromGroups)
718 textGroups = self._smsTextRegex.finditer(messageHtml)
719 textParts = (group.group(1).strip() for group in textGroups)
720 timeGroups = self._smsTimeRegex.finditer(messageHtml)
721 timeParts = (group.group(1).strip() for group in timeGroups)
723 messageParts = itertools.izip(fromParts, textParts, timeParts)
724 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
725 conv.messages = messages
730 def _merge_conversation_sources(parsedMessages, json):
731 for message in parsedMessages:
732 jsonItem = json["messages"][message.id]
733 message.isRead = jsonItem["isRead"]
734 message.isSpam = jsonItem["isSpam"]
735 message.isTrash = jsonItem["isTrash"]
736 message.isArchived = "inbox" not in jsonItem["labels"]
739 def _get_page(self, url, data = None, refererUrl = None):
741 if refererUrl is not None:
742 headers["Referer"] = refererUrl
744 encodedData = urllib.urlencode(data) if data is not None else None
747 page = self._browser.download(url, encodedData, None, headers)
748 except urllib2.URLError, e:
749 _moduleLogger.error("Translating error: %s" % str(e))
750 raise NetworkError("%s is not accesible" % url)
754 def _get_page_with_token(self, url, data = None, refererUrl = None):
757 data['_rnr_se'] = self._token
759 page = self._get_page(url, data, refererUrl)
763 def _parse_with_validation(self, page):
764 json = parse_json(page)
765 self._validate_response(json)
768 def _validate_response(self, response):
770 Validates that the JSON response is A-OK
773 assert response is not None, "Response not provided"
774 assert 'ok' in response, "Response lacks status"
775 assert response['ok'], "Response not good"
776 except AssertionError:
778 if response["data"]["code"] == 20:
780 """Ambiguous error 20 returned by Google Voice.
781 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)
784 raise RuntimeError('There was a problem with GV: %s' % response)
787 _UNESCAPE_ENTITIES = {
795 plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
799 def google_strptime(time):
801 Hack: Google always returns the time in the same locale. Sadly if the
802 local system's locale is different, there isn't a way to perfectly handle
803 the time. So instead we handle implement some time formatting
805 abbrevTime = time[:-3]
806 parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
807 if time.endswith("PM"):
808 parsedTime += datetime.timedelta(hours=12)
812 def itergroup(iterator, count, padValue = None):
814 Iterate in groups of 'count' values. If there
815 aren't enough values, the last result is padded with
818 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
822 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
826 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
831 >>> for val in itergroup("123456", 3):
835 >>> for val in itergroup("123456", 3):
836 ... print repr("".join(val))
840 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
841 nIterators = (paddedIterator, ) * count
842 return itertools.izip(*nIterators)
846 _TRUE_REGEX = re.compile("true")
847 _FALSE_REGEX = re.compile("false")
848 _COMMENT_REGEX = re.compile("^\s+//.*$", re.M)
849 s = _TRUE_REGEX.sub("True", s)
850 s = _FALSE_REGEX.sub("False", s)
851 s = _COMMENT_REGEX.sub("#", s)
853 results = eval(s, {}, {})
855 _moduleLogger.exception("Oops")
860 def _fake_parse_json(flattened):
861 return safe_eval(flattened)
864 def _actual_parse_json(flattened):
865 return simplejson.loads(flattened)
868 if simplejson is None:
869 parse_json = _fake_parse_json
871 parse_json = _actual_parse_json
874 def extract_payload(flatXml):
875 xmlTree = ElementTree.fromstring(flatXml)
877 jsonElement = xmlTree.getchildren()[0]
878 flatJson = jsonElement.text
879 jsonTree = parse_json(flatJson)
881 htmlElement = xmlTree.getchildren()[1]
882 flatHtml = htmlElement.text
884 return jsonTree, flatHtml
887 def guess_phone_type(number):
888 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
889 return GVoiceBackend.PHONE_TYPE_GIZMO
891 return GVoiceBackend.PHONE_TYPE_MOBILE
894 def get_sane_callback(backend):
896 Try to set a sane default callback number on these preferences
897 1) 1747 numbers ( Gizmo )
898 2) anything with gizmo in the name
899 3) anything with computer in the name
902 numbers = backend.get_callback_numbers()
904 priorityOrderedCriteria = [
914 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
916 descriptionMatcher = None
917 if numberCriteria is not None:
918 numberMatcher = re.compile(numberCriteria)
919 elif descriptionCriteria is not None:
920 descriptionMatcher = re.compile(descriptionCriteria, re.I)
922 for number, description in numbers.iteritems():
923 if numberMatcher is not None and numberMatcher.match(number) is None:
925 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
930 def set_sane_callback(backend):
932 Try to set a sane default callback number on these preferences
933 1) 1747 numbers ( Gizmo )
934 2) anything with gizmo in the name
935 3) anything with computer in the name
938 number = get_sane_callback(backend)
939 backend.set_callback_number(number)
942 def _is_not_special(name):
943 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
947 members = inspect.getmembers(obj)
948 return dict((name, value) for (name, value) in members if _is_not_special(name))
951 def grab_debug_info(username, password):
952 cookieFile = os.path.join(".", "raw_cookies.txt")
954 os.remove(cookieFile)
958 backend = GVoiceBackend(cookieFile)
959 browser = backend._browser
962 ("token", backend._tokenURL),
963 ("login", backend._loginURL),
964 ("isdnd", backend._isDndURL),
965 ("account", backend._XML_ACCOUNT_URL),
966 ("html_contacts", backend._XML_CONTACTS_URL),
967 ("contacts", backend._JSON_CONTACTS_URL),
968 ("csv", backend._CSV_CONTACTS_URL),
970 ("voicemail", backend._XML_VOICEMAIL_URL),
971 ("html_sms", backend._XML_SMS_URL),
972 ("sms", backend._JSON_SMS_URL),
973 ("count", backend._JSON_SMS_COUNT_URL),
975 ("recent", backend._XML_RECENT_URL),
976 ("placed", backend._XML_PLACED_URL),
977 ("recieved", backend._XML_RECEIVED_URL),
978 ("missed", backend._XML_MISSED_URL),
982 print "Grabbing pre-login pages"
983 for name, url in _TEST_WEBPAGES:
985 page = browser.download(url)
986 except StandardError, e:
989 print "\tWriting to file"
990 with open("not_loggedin_%s.txt" % name, "w") as f:
994 print "Attempting login"
995 galxToken = backend._get_token()
996 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
997 with open("loggingin.txt", "w") as f:
998 print "\tWriting to file"
999 f.write(loginSuccessOrFailurePage)
1001 backend._grab_account_info(loginSuccessOrFailurePage)
1003 # Retry in case the redirect failed
1004 # luckily refresh_account_info does everything we need for a retry
1005 loggedIn = backend.refresh_account_info() is not None
1010 print "Grabbing post-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("loggedin_%s.txt" % name, "w") as f:
1022 browser.save_cookies()
1023 print "\tWriting cookies to file"
1024 with open("cookies.txt", "w") as f:
1026 "%s: %s\n" % (c.name, c.value)
1027 for c in browser._cookies
1033 logging.basicConfig(level=logging.DEBUG)
1039 grab_debug_info(username, password)
1042 if __name__ == "__main__":