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.etree import ElementTree
43 import simplejson as _simplejson
44 simplejson = _simplejson
51 _moduleLogger = logging.getLogger(__name__)
54 class NetworkError(RuntimeError):
58 class MessageText(object):
61 ACCURACY_MEDIUM = "med2"
62 ACCURACY_HIGH = "high"
74 def __eq__(self, other):
75 return self.accuracy == other.accuracy and self.text == other.text
78 class Message(object):
86 return "%s (%s): %s" % (
89 "".join(str(part) for part in self.body)
93 selfDict = to_dict(self)
94 selfDict["body"] = [text.to_dict() for text in self.body] if self.body is not None else None
97 def __eq__(self, other):
98 return self.whoFrom == other.whoFrom and self.when == other.when and self.body == other.body
101 class Conversation(object):
103 TYPE_VOICEMAIL = "Voicemail"
109 self.contactId = None
112 self.prettyNumber = None
121 self.isArchived = None
123 def __cmp__(self, other):
124 cmpValue = cmp(self.contactId, other.contactId)
128 cmpValue = cmp(self.time, other.time)
132 cmpValue = cmp(self.id, other.id)
137 selfDict = to_dict(self)
138 selfDict["messages"] = [message.to_dict() for message in self.messages] if self.messages is not None else None
142 class GVoiceBackend(object):
144 This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
145 the functions include login, setting up a callback number, and initalting a callback
149 PHONE_TYPE_MOBILE = 2
153 def __init__(self, cookieFile = None):
154 # Important items in this function are the setup of the browser emulation and cookie file
155 self._browser = browser_emu.MozillaEmulator(1)
156 self._loadedFromCookies = self._browser.load_cookies(cookieFile)
159 self._accountNum = ""
160 self._lastAuthed = 0.0
161 self._callbackNumber = ""
162 self._callbackNumbers = {}
164 # Suprisingly, moving all of these from class to self sped up startup time
166 self._validateRe = re.compile("^\+?[0-9]{10,}$")
168 self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
170 SECURE_URL_BASE = "https://www.google.com/voice/"
171 SECURE_MOBILE_URL_BASE = SECURE_URL_BASE + "mobile/"
172 self._forwardURL = SECURE_MOBILE_URL_BASE + "phones"
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._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
193 'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms',
194 'recorded', 'placed', 'received', 'missed'
196 self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox"
197 self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred"
198 self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all"
199 self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam"
200 self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash"
201 self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/"
202 self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/"
203 self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/"
204 self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/"
205 self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
206 self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
208 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
209 self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
210 self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
211 self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
213 self._contactsBodyRe = re.compile(r"""gcData\s*=\s*({.*?});""", 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 is_authed might be enough to login, else full login is required
231 return self._loadedFromCookies or 0.0 < self._lastAuthed
233 def is_authed(self, force = False):
235 Attempts to detect a current session
236 @note Once logged in try not to reauth more than once a minute.
237 @returns If authenticated
240 isRecentledAuthed = (time.time() - self._lastAuthed) < 120
241 isPreviouslyAuthed = self._token is not None
242 if isRecentledAuthed and isPreviouslyAuthed and not force:
246 page = self._get_page(self._forwardURL)
247 self._grab_account_info(page)
249 _moduleLogger.exception(str(e))
252 self._browser.save_cookies()
253 self._lastAuthed = time.time()
256 def _get_token(self):
257 tokenPage = self._get_page(self._tokenURL)
259 galxTokens = self._galxRe.search(tokenPage)
260 if galxTokens is not None:
261 galxToken = galxTokens.group(1)
264 _moduleLogger.debug("Could not grab GALX token")
267 def _login(self, username, password, token):
271 'service': "grandcentral",
274 "PersistentCookie": "yes",
276 "continue": self._forwardURL,
279 loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
280 return loginSuccessOrFailurePage
282 def login(self, username, password):
284 Attempt to login to GoogleVoice
285 @returns Whether login was successful or not
289 galxToken = self._get_token()
290 loginSuccessOrFailurePage = self._login(username, password, galxToken)
293 self._grab_account_info(loginSuccessOrFailurePage)
295 # Retry in case the redirect failed
296 # luckily is_authed does everything we need for a retry
297 loggedIn = self.is_authed(True)
299 _moduleLogger.exception(str(e))
301 _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
303 self._browser.save_cookies()
304 self._lastAuthed = time.time()
308 self._browser.clear_cookies()
309 self._browser.save_cookies()
311 self._lastAuthed = 0.0
317 isDndPage = self._get_page(self._isDndURL)
319 dndGroup = self._isDndRe.search(isDndPage)
322 dndStatus = dndGroup.group(1)
323 isDnd = True if dndStatus.strip().lower() == "true" else False
326 def set_dnd(self, doNotDisturb):
331 "doNotDisturb": 1 if doNotDisturb else 0,
334 dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
336 def call(self, outgoingNumber):
338 This is the main function responsible for initating the callback
341 outgoingNumber = self._send_validation(outgoingNumber)
342 subscriberNumber = None
343 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
346 'outgoingNumber': outgoingNumber,
347 'forwardingNumber': self._callbackNumber,
348 'subscriberNumber': subscriberNumber or 'undefined',
349 'phoneType': str(phoneType),
352 _moduleLogger.info("%r" % callData)
354 page = self._get_page_with_token(
358 self._parse_with_validation(page)
361 def cancel(self, outgoingNumber=None):
363 Cancels a call matching outgoing and forwarding numbers (if given).
364 Will raise an error if no matching call is being placed
367 page = self._get_page_with_token(
370 'outgoingNumber': outgoingNumber or 'undefined',
371 'forwardingNumber': self._callbackNumber or 'undefined',
375 self._parse_with_validation(page)
377 def send_sms(self, phoneNumbers, message):
381 validatedPhoneNumbers = [
382 self._send_validation(phoneNumber)
383 for phoneNumber in phoneNumbers
385 flattenedPhoneNumbers = ",".join(validatedPhoneNumbers)
386 page = self._get_page_with_token(
389 'phoneNumber': flattenedPhoneNumbers,
393 self._parse_with_validation(page)
395 def search(self, query):
397 Search your Google Voice Account history for calls, voicemails, and sms
398 Returns ``Folder`` instance containting matching messages
401 page = self._get_page(
402 self._XML_SEARCH_URL,
405 json, html = extract_payload(page)
408 def get_feed(self, feed):
412 actualFeed = "_XML_%s_URL" % feed.upper()
413 feedUrl = getattr(self, actualFeed)
415 page = self._get_page(feedUrl)
416 json, html = extract_payload(page)
420 def download(self, messageId, adir):
422 Download a voicemail or recorded call MP3 matching the given ``msg``
423 which can either be a ``Message`` instance, or a SHA1 identifier.
424 Saves files to ``adir`` (defaults to current directory).
425 Message hashes can be found in ``self.voicemail().messages`` for example.
426 @returns location of saved file.
429 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
430 fn = os.path.join(adir, '%s.mp3' % messageId)
431 with open(fn, 'wb') as fo:
435 def is_valid_syntax(self, number):
437 @returns If This number be called ( syntax validation only )
439 return self._validateRe.match(number) is not None
441 def get_account_number(self):
443 @returns The GoogleVoice phone number
445 return self._accountNum
447 def get_callback_numbers(self):
449 @returns a dictionary mapping call back numbers to descriptions
450 @note These results are cached for 30 minutes.
452 if not self.is_authed():
454 return self._callbackNumbers
456 def set_callback_number(self, callbacknumber):
458 Set the number that GoogleVoice calls
459 @param callbacknumber should be a proper 10 digit number
461 self._callbackNumber = callbacknumber
462 _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
465 def get_callback_number(self):
467 @returns Current callback number or None
469 return self._callbackNumber
471 def get_recent(self):
473 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
477 ("Received", self._XML_RECEIVED_URL),
478 ("Missed", self._XML_MISSED_URL),
479 ("Placed", self._XML_PLACED_URL),
481 flatXml = self._get_page(url)
483 allRecentHtml = self._grab_html(flatXml)
484 allRecentData = self._parse_history(allRecentHtml)
485 for recentCallData in allRecentData:
486 recentCallData["action"] = action
489 def get_contacts(self):
491 @returns Iterable of (contact id, contact name)
494 page = self._get_page(self._XML_CONTACTS_URL)
495 contactsBody = self._contactsBodyRe.search(page)
496 if contactsBody is None:
497 raise RuntimeError("Could not extract contact information")
498 accountData = _fake_parse_json(contactsBody.group(1))
499 for contactId, contactDetails in accountData["contacts"].iteritems():
500 # A zero contact id is the catch all for unknown contacts
502 yield contactId, contactDetails
504 def get_voicemails(self):
508 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
509 voicemailHtml = self._grab_html(voicemailPage)
510 voicemailJson = self._grab_json(voicemailPage)
511 parsedVoicemail = self._parse_voicemail(voicemailHtml)
512 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
519 smsPage = self._get_page(self._XML_SMS_URL)
520 smsHtml = self._grab_html(smsPage)
521 smsJson = self._grab_json(smsPage)
522 parsedSms = self._parse_sms(smsHtml)
523 smss = self._merge_conversation_sources(parsedSms, smsJson)
526 def mark_message(self, messageId, asRead):
531 "read": 1 if asRead else 0,
535 markPage = self._get_page(self._markAsReadURL, postData)
537 def archive_message(self, messageId):
545 markPage = self._get_page(self._archiveMessageURL, postData)
547 def _grab_json(self, flatXml):
548 xmlTree = ElementTree.fromstring(flatXml)
549 jsonElement = xmlTree.getchildren()[0]
550 flatJson = jsonElement.text
551 jsonTree = parse_json(flatJson)
554 def _grab_html(self, flatXml):
555 xmlTree = ElementTree.fromstring(flatXml)
556 htmlElement = xmlTree.getchildren()[1]
557 flatHtml = htmlElement.text
560 def _grab_account_info(self, page):
561 tokenGroup = self._tokenRe.search(page)
562 if tokenGroup is None:
563 raise RuntimeError("Could not extract authentication token from GoogleVoice")
564 self._token = tokenGroup.group(1)
566 anGroup = self._accountNumRe.search(page)
567 if anGroup is not None:
568 self._accountNum = anGroup.group(1)
570 _moduleLogger.debug("Could not extract account number from GoogleVoice")
572 self._callbackNumbers = {}
573 for match in self._callbackRe.finditer(page):
574 callbackNumber = match.group(2)
575 callbackName = match.group(1)
576 self._callbackNumbers[callbackNumber] = callbackName
577 if len(self._callbackNumbers) == 0:
578 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
580 def _send_validation(self, number):
581 if not self.is_valid_syntax(number):
582 raise ValueError('Number is not valid: "%s"' % number)
583 elif not self.is_authed():
584 raise RuntimeError("Not Authenticated")
587 def _parse_history(self, historyHtml):
588 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
589 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
590 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
591 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
592 exactTime = google_strptime(exactTime)
593 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
594 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
595 locationGroup = self._voicemailLocationRegex.search(messageHtml)
596 location = locationGroup.group(1).strip() if locationGroup else ""
598 nameGroup = self._voicemailNameRegex.search(messageHtml)
599 name = nameGroup.group(1).strip() if nameGroup else ""
600 numberGroup = self._voicemailNumberRegex.search(messageHtml)
601 number = numberGroup.group(1).strip() if numberGroup else ""
602 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
603 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
604 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
605 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
608 "id": messageId.strip(),
609 "contactId": contactId,
612 "relTime": relativeTime,
613 "prettyNumber": prettyNumber,
615 "location": location,
619 def _interpret_voicemail_regex(group):
620 quality, content, number = group.group(2), group.group(3), group.group(4)
622 if quality is not None and content is not None:
623 text.accuracy = quality
626 elif number is not None:
627 text.accuracy = MessageText.ACCURACY_HIGH
631 def _parse_voicemail(self, voicemailHtml):
632 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
633 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
634 conv = Conversation()
635 conv.type = Conversation.TYPE_VOICEMAIL
636 conv.id = messageId.strip()
638 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
639 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
640 conv.time = google_strptime(exactTimeText)
641 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
642 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
643 locationGroup = self._voicemailLocationRegex.search(messageHtml)
644 conv.location = locationGroup.group(1).strip() if locationGroup else ""
646 nameGroup = self._voicemailNameRegex.search(messageHtml)
647 conv.name = nameGroup.group(1).strip() if nameGroup else ""
648 numberGroup = self._voicemailNumberRegex.search(messageHtml)
649 conv.number = numberGroup.group(1).strip() if numberGroup else ""
650 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
651 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
652 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
653 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
655 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
657 self._interpret_voicemail_regex(group)
658 for group in messageGroups
659 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
661 message.body = messageParts
662 message.whoFrom = conv.name
663 message.when = conv.time.strftime("%I:%M %p")
664 conv.messages = (message, )
669 def _interpret_sms_message_parts(fromPart, textPart, timePart):
671 text.accuracy = MessageText.ACCURACY_MEDIUM
675 message.body = (text, )
676 message.whoFrom = fromPart
677 message.when = timePart
681 def _parse_sms(self, smsHtml):
682 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
683 for messageId, messageHtml in itergroup(splitSms[1:], 2):
684 conv = Conversation()
685 conv.type = Conversation.TYPE_SMS
686 conv.id = messageId.strip()
688 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
689 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
690 conv.time = google_strptime(exactTimeText)
691 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
692 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
695 nameGroup = self._voicemailNameRegex.search(messageHtml)
696 conv.name = nameGroup.group(1).strip() if nameGroup else ""
697 numberGroup = self._voicemailNumberRegex.search(messageHtml)
698 conv.number = numberGroup.group(1).strip() if numberGroup else ""
699 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
700 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
701 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
702 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
704 fromGroups = self._smsFromRegex.finditer(messageHtml)
705 fromParts = (group.group(1).strip() for group in fromGroups)
706 textGroups = self._smsTextRegex.finditer(messageHtml)
707 textParts = (group.group(1).strip() for group in textGroups)
708 timeGroups = self._smsTimeRegex.finditer(messageHtml)
709 timeParts = (group.group(1).strip() for group in timeGroups)
711 messageParts = itertools.izip(fromParts, textParts, timeParts)
712 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
713 conv.messages = messages
718 def _merge_conversation_sources(parsedMessages, json):
719 for message in parsedMessages:
720 jsonItem = json["messages"][message.id]
721 message.isRead = jsonItem["isRead"]
722 message.isSpam = jsonItem["isSpam"]
723 message.isTrash = jsonItem["isTrash"]
724 message.isArchived = "inbox" not in jsonItem["labels"]
727 def _get_page(self, url, data = None, refererUrl = None):
729 if refererUrl is not None:
730 headers["Referer"] = refererUrl
732 encodedData = urllib.urlencode(data) if data is not None else None
735 page = self._browser.download(url, encodedData, None, headers)
736 except urllib2.URLError, e:
737 _moduleLogger.error("Translating error: %s" % str(e))
738 raise NetworkError("%s is not accesible" % url)
742 def _get_page_with_token(self, url, data = None, refererUrl = None):
745 data['_rnr_se'] = self._token
747 page = self._get_page(url, data, refererUrl)
751 def _parse_with_validation(self, page):
752 json = parse_json(page)
753 validate_response(json)
757 def google_strptime(time):
759 Hack: Google always returns the time in the same locale. Sadly if the
760 local system's locale is different, there isn't a way to perfectly handle
761 the time. So instead we handle implement some time formatting
763 abbrevTime = time[:-3]
764 parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
766 parsedTime += datetime.timedelta(hours=12)
770 def itergroup(iterator, count, padValue = None):
772 Iterate in groups of 'count' values. If there
773 aren't enough values, the last result is padded with
776 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
780 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
784 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
789 >>> for val in itergroup("123456", 3):
793 >>> for val in itergroup("123456", 3):
794 ... print repr("".join(val))
798 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
799 nIterators = (paddedIterator, ) * count
800 return itertools.izip(*nIterators)
804 _TRUE_REGEX = re.compile("true")
805 _FALSE_REGEX = re.compile("false")
806 s = _TRUE_REGEX.sub("True", s)
807 s = _FALSE_REGEX.sub("False", s)
808 return eval(s, {}, {})
811 def _fake_parse_json(flattened):
812 return safe_eval(flattened)
815 def _actual_parse_json(flattened):
816 return simplejson.loads(flattened)
819 if simplejson is None:
820 parse_json = _fake_parse_json
822 parse_json = _actual_parse_json
825 def extract_payload(flatXml):
826 xmlTree = ElementTree.fromstring(flatXml)
828 jsonElement = xmlTree.getchildren()[0]
829 flatJson = jsonElement.text
830 jsonTree = parse_json(flatJson)
832 htmlElement = xmlTree.getchildren()[1]
833 flatHtml = htmlElement.text
835 return jsonTree, flatHtml
838 def validate_response(response):
840 Validates that the JSON response is A-OK
843 assert 'ok' in response and response['ok']
844 except AssertionError:
845 raise RuntimeError('There was a problem with GV: %s' % response)
848 def guess_phone_type(number):
849 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
850 return GVoiceBackend.PHONE_TYPE_GIZMO
852 return GVoiceBackend.PHONE_TYPE_MOBILE
855 def get_sane_callback(backend):
857 Try to set a sane default callback number on these preferences
858 1) 1747 numbers ( Gizmo )
859 2) anything with gizmo in the name
860 3) anything with computer in the name
863 numbers = backend.get_callback_numbers()
865 priorityOrderedCriteria = [
875 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
877 descriptionMatcher = None
878 if numberCriteria is not None:
879 numberMatcher = re.compile(numberCriteria)
880 elif descriptionCriteria is not None:
881 descriptionMatcher = re.compile(descriptionCriteria, re.I)
883 for number, description in numbers.iteritems():
884 if numberMatcher is not None and numberMatcher.match(number) is None:
886 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
891 def set_sane_callback(backend):
893 Try to set a sane default callback number on these preferences
894 1) 1747 numbers ( Gizmo )
895 2) anything with gizmo in the name
896 3) anything with computer in the name
899 number = get_sane_callback(backend)
900 backend.set_callback_number(number)
903 def _is_not_special(name):
904 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
908 members = inspect.getmembers(obj)
909 return dict((name, value) for (name, value) in members if _is_not_special(name))
912 def grab_debug_info(username, password):
913 cookieFile = os.path.join(".", "raw_cookies.txt")
915 os.remove(cookieFile)
919 backend = GVoiceBackend(cookieFile)
920 browser = backend._browser
923 ("forward", backend._forwardURL),
924 ("token", backend._tokenURL),
925 ("login", backend._loginURL),
926 ("isdnd", backend._isDndURL),
927 ("account", backend._XML_ACCOUNT_URL),
928 ("contacts", backend._XML_CONTACTS_URL),
930 ("voicemail", backend._XML_VOICEMAIL_URL),
931 ("sms", backend._XML_SMS_URL),
933 ("recent", backend._XML_RECENT_URL),
934 ("placed", backend._XML_PLACED_URL),
935 ("recieved", backend._XML_RECEIVED_URL),
936 ("missed", backend._XML_MISSED_URL),
940 print "Grabbing pre-login pages"
941 for name, url in _TEST_WEBPAGES:
943 page = browser.download(url)
944 except StandardError, e:
947 print "\tWriting to file"
948 with open("not_loggedin_%s.txt" % name, "w") as f:
952 print "Attempting login"
953 galxToken = backend._get_token()
954 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
955 with open("loggingin.txt", "w") as f:
956 print "\tWriting to file"
957 f.write(loginSuccessOrFailurePage)
959 backend._grab_account_info(loginSuccessOrFailurePage)
961 # Retry in case the redirect failed
962 # luckily is_authed does everything we need for a retry
963 loggedIn = backend.is_authed(True)
968 print "Grabbing post-login pages"
969 for name, url in _TEST_WEBPAGES:
971 page = browser.download(url)
972 except StandardError, e:
975 print "\tWriting to file"
976 with open("loggedin_%s.txt" % name, "w") as f:
980 browser.save_cookies()
981 print "\tWriting cookies to file"
982 with open("cookies.txt", "w") as f:
984 "%s: %s\n" % (c.name, c.value)
985 for c in browser._cookies
991 logging.basicConfig(level=logging.DEBUG)
997 grab_debug_info(username, password)
1000 if __name__ == "__main__":