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("gvoice.backend")
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
239 isRecentledAuthed = (time.time() - self._lastAuthed) < 120
240 isPreviouslyAuthed = self._token is not None
241 if isRecentledAuthed and isPreviouslyAuthed and not force:
245 page = self._get_page(self._forwardURL)
246 self._grab_account_info(page)
248 _moduleLogger.exception(str(e))
251 self._browser.save_cookies()
252 self._lastAuthed = time.time()
255 def _get_token(self):
256 tokenPage = self._get_page(self._tokenURL)
258 galxTokens = self._galxRe.search(tokenPage)
259 if galxTokens is not None:
260 galxToken = galxTokens.group(1)
263 _moduleLogger.debug("Could not grab GALX token")
266 def _login(self, username, password, token):
270 'service': "grandcentral",
273 "PersistentCookie": "yes",
275 "continue": self._forwardURL,
278 loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
279 return loginSuccessOrFailurePage
281 def login(self, username, password):
283 Attempt to login to GoogleVoice
284 @returns Whether login was successful or not
287 galxToken = self._get_token()
288 loginSuccessOrFailurePage = self._login(username, password, galxToken)
291 self._grab_account_info(loginSuccessOrFailurePage)
293 # Retry in case the redirect failed
294 # luckily is_authed does everything we need for a retry
295 loggedIn = self.is_authed(True)
297 _moduleLogger.exception(str(e))
299 _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
301 self._browser.save_cookies()
302 self._lastAuthed = time.time()
306 self._browser.clear_cookies()
307 self._browser.save_cookies()
309 self._lastAuthed = 0.0
312 isDndPage = self._get_page(self._isDndURL)
314 dndGroup = self._isDndRe.search(isDndPage)
317 dndStatus = dndGroup.group(1)
318 isDnd = True if dndStatus.strip().lower() == "true" else False
321 def set_dnd(self, doNotDisturb):
323 "doNotDisturb": 1 if doNotDisturb else 0,
326 dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
328 def call(self, outgoingNumber):
330 This is the main function responsible for initating the callback
332 outgoingNumber = self._send_validation(outgoingNumber)
333 subscriberNumber = None
334 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
337 'outgoingNumber': outgoingNumber,
338 'forwardingNumber': self._callbackNumber,
339 'subscriberNumber': subscriberNumber or 'undefined',
340 'phoneType': str(phoneType),
343 _moduleLogger.info("%r" % callData)
345 page = self._get_page_with_token(
349 self._parse_with_validation(page)
352 def cancel(self, outgoingNumber=None):
354 Cancels a call matching outgoing and forwarding numbers (if given).
355 Will raise an error if no matching call is being placed
357 page = self._get_page_with_token(
360 'outgoingNumber': outgoingNumber or 'undefined',
361 'forwardingNumber': self._callbackNumber or 'undefined',
365 self._parse_with_validation(page)
367 def send_sms(self, phoneNumber, message):
368 phoneNumber = self._send_validation(phoneNumber)
369 page = self._get_page_with_token(
372 'phoneNumber': phoneNumber,
376 self._parse_with_validation(page)
378 def search(self, query):
380 Search your Google Voice Account history for calls, voicemails, and sms
381 Returns ``Folder`` instance containting matching messages
383 page = self._get_page(
384 self._XML_SEARCH_URL,
387 json, html = extract_payload(page)
390 def get_feed(self, feed):
391 actualFeed = "_XML_%s_URL" % feed.upper()
392 feedUrl = getattr(self, actualFeed)
394 page = self._get_page(feedUrl)
395 json, html = extract_payload(page)
399 def download(self, messageId, adir):
401 Download a voicemail or recorded call MP3 matching the given ``msg``
402 which can either be a ``Message`` instance, or a SHA1 identifier.
403 Saves files to ``adir`` (defaults to current directory).
404 Message hashes can be found in ``self.voicemail().messages`` for example.
405 Returns location of saved file.
407 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
408 fn = os.path.join(adir, '%s.mp3' % messageId)
409 with open(fn, 'wb') as fo:
413 def is_valid_syntax(self, number):
415 @returns If This number be called ( syntax validation only )
417 return self._validateRe.match(number) is not None
419 def get_account_number(self):
421 @returns The GoogleVoice phone number
423 return self._accountNum
425 def get_callback_numbers(self):
427 @returns a dictionary mapping call back numbers to descriptions
428 @note These results are cached for 30 minutes.
430 if not self.is_authed():
432 return self._callbackNumbers
434 def set_callback_number(self, callbacknumber):
436 Set the number that GoogleVoice calls
437 @param callbacknumber should be a proper 10 digit number
439 self._callbackNumber = callbacknumber
440 _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
443 def get_callback_number(self):
445 @returns Current callback number or None
447 return self._callbackNumber
449 def get_recent(self):
451 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
454 ("Received", self._XML_RECEIVED_URL),
455 ("Missed", self._XML_MISSED_URL),
456 ("Placed", self._XML_PLACED_URL),
458 flatXml = self._get_page(url)
460 allRecentHtml = self._grab_html(flatXml)
461 allRecentData = self._parse_history(allRecentHtml)
462 for recentCallData in allRecentData:
463 recentCallData["action"] = action
466 def get_contacts(self):
468 @returns Iterable of (contact id, contact name)
470 page = self._get_page(self._XML_CONTACTS_URL)
471 contactsBody = self._contactsBodyRe.search(page)
472 if contactsBody is None:
473 raise RuntimeError("Could not extract contact information")
474 accountData = _fake_parse_json(contactsBody.group(1))
475 for contactId, contactDetails in accountData["contacts"].iteritems():
476 # A zero contact id is the catch all for unknown contacts
478 yield contactId, contactDetails
480 def get_conversations(self):
481 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
482 voicemailHtml = self._grab_html(voicemailPage)
483 voicemailJson = self._grab_json(voicemailPage)
484 parsedVoicemail = self._parse_voicemail(voicemailHtml)
485 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
487 smsPage = self._get_page(self._XML_SMS_URL)
488 smsHtml = self._grab_html(smsPage)
489 smsJson = self._grab_json(smsPage)
490 parsedSms = self._parse_sms(smsHtml)
491 smss = self._merge_conversation_sources(parsedSms, smsJson)
492 decoratedSms = self._decorate_sms(smss)
494 allConversations = itertools.chain(voicemails, decoratedSms)
495 return allConversations
497 def mark_message(self, messageId, asRead):
499 "read": 1 if asRead else 0,
503 markPage = self._get_page(self._markAsReadURL, postData)
505 def archive_message(self, messageId):
510 markPage = self._get_page(self._archiveMessageURL, postData)
512 def _grab_json(self, flatXml):
513 xmlTree = ElementTree.fromstring(flatXml)
514 jsonElement = xmlTree.getchildren()[0]
515 flatJson = jsonElement.text
516 jsonTree = parse_json(flatJson)
519 def _grab_html(self, flatXml):
520 xmlTree = ElementTree.fromstring(flatXml)
521 htmlElement = xmlTree.getchildren()[1]
522 flatHtml = htmlElement.text
525 def _grab_account_info(self, page):
526 tokenGroup = self._tokenRe.search(page)
527 if tokenGroup is None:
528 raise RuntimeError("Could not extract authentication token from GoogleVoice")
529 self._token = tokenGroup.group(1)
531 anGroup = self._accountNumRe.search(page)
532 if anGroup is not None:
533 self._accountNum = anGroup.group(1)
535 _moduleLogger.debug("Could not extract account number from GoogleVoice")
537 self._callbackNumbers = {}
538 for match in self._callbackRe.finditer(page):
539 callbackNumber = match.group(2)
540 callbackName = match.group(1)
541 self._callbackNumbers[callbackNumber] = callbackName
542 if len(self._callbackNumbers) == 0:
543 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
545 def _send_validation(self, number):
546 if not self.is_valid_syntax(number):
547 raise ValueError('Number is not valid: "%s"' % number)
548 elif not self.is_authed():
549 raise RuntimeError("Not Authenticated")
551 if len(number) == 11 and number[0] == 1:
552 # Strip leading 1 from 11 digit dialing
556 def _parse_history(self, historyHtml):
557 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
558 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
559 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
560 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
561 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
562 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
563 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
564 locationGroup = self._voicemailLocationRegex.search(messageHtml)
565 location = locationGroup.group(1).strip() if locationGroup else ""
567 nameGroup = self._voicemailNameRegex.search(messageHtml)
568 name = nameGroup.group(1).strip() if nameGroup else ""
569 numberGroup = self._voicemailNumberRegex.search(messageHtml)
570 number = numberGroup.group(1).strip() if numberGroup else ""
571 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
572 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
573 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
574 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
577 "id": messageId.strip(),
578 "contactId": contactId,
581 "relTime": relativeTime,
582 "prettyNumber": prettyNumber,
584 "location": location,
588 def _interpret_voicemail_regex(group):
589 quality, content, number = group.group(2), group.group(3), group.group(4)
591 if quality is not None and content is not None:
592 text.accuracy = quality
595 elif number is not None:
596 text.accuracy = MessageText.ACCURACY_HIGH
600 def _parse_voicemail(self, voicemailHtml):
601 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
602 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
603 conv = Conversation()
604 conv.type = Conversation.TYPE_VOICEMAIL
605 conv.id = messageId.strip()
607 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
608 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
609 conv.time = datetime.datetime.strptime(exactTimeText, "%m/%d/%y %I:%M %p")
610 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
611 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
612 locationGroup = self._voicemailLocationRegex.search(messageHtml)
613 conv.location = locationGroup.group(1).strip() if locationGroup else ""
615 nameGroup = self._voicemailNameRegex.search(messageHtml)
616 conv.name = nameGroup.group(1).strip() if nameGroup else ""
617 numberGroup = self._voicemailNumberRegex.search(messageHtml)
618 conv.number = numberGroup.group(1).strip() if numberGroup else ""
619 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
620 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
621 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
622 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
624 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
626 self._interpret_voicemail_regex(group)
627 for group in messageGroups
628 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
630 message.body = messageParts
631 message.whoFrom = conv.name
632 message.when = conv.time.strftime("%I:%M %p")
633 conv.messages = (message, )
638 def _interpret_sms_message_parts(fromPart, textPart, timePart):
640 text.accuracy = MessageText.ACCURACY_MEDIUM
644 message.body = (text, )
645 message.whoFrom = fromPart
646 message.when = timePart
650 def _parse_sms(self, smsHtml):
651 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
652 for messageId, messageHtml in itergroup(splitSms[1:], 2):
653 conv = Conversation()
654 conv.type = Conversation.TYPE_SMS
655 conv.id = messageId.strip()
657 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
658 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
659 conv.time = datetime.datetime.strptime(exactTimeText, "%m/%d/%y %I:%M %p")
660 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
661 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
664 nameGroup = self._voicemailNameRegex.search(messageHtml)
665 conv.name = nameGroup.group(1).strip() if nameGroup else ""
666 numberGroup = self._voicemailNumberRegex.search(messageHtml)
667 conv.number = numberGroup.group(1).strip() if numberGroup else ""
668 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
669 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
670 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
671 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
673 fromGroups = self._smsFromRegex.finditer(messageHtml)
674 fromParts = (group.group(1).strip() for group in fromGroups)
675 textGroups = self._smsTextRegex.finditer(messageHtml)
676 textParts = (group.group(1).strip() for group in textGroups)
677 timeGroups = self._smsTimeRegex.finditer(messageHtml)
678 timeParts = (group.group(1).strip() for group in timeGroups)
680 messageParts = itertools.izip(fromParts, textParts, timeParts)
681 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
682 conv.messages = messages
686 def _decorate_sms(self, parsedTexts):
690 def _merge_conversation_sources(parsedMessages, json):
691 for message in parsedMessages:
692 jsonItem = json["messages"][message.id]
693 message.isRead = jsonItem["isRead"]
694 message.isSpam = jsonItem["isSpam"]
695 message.isTrash = jsonItem["isTrash"]
696 message.isArchived = "inbox" not in jsonItem["labels"]
699 def _get_page(self, url, data = None, refererUrl = None):
701 if refererUrl is not None:
702 headers["Referer"] = refererUrl
704 encodedData = urllib.urlencode(data) if data is not None else None
707 page = self._browser.download(url, encodedData, None, headers)
708 except urllib2.URLError, e:
709 _moduleLogger.error("Translating error: %s" % str(e))
710 raise NetworkError("%s is not accesible" % url)
714 def _get_page_with_token(self, url, data = None, refererUrl = None):
717 data['_rnr_se'] = self._token
719 page = self._get_page(url, data, refererUrl)
723 def _parse_with_validation(self, page):
724 json = parse_json(page)
725 validate_response(json)
729 def itergroup(iterator, count, padValue = None):
731 Iterate in groups of 'count' values. If there
732 aren't enough values, the last result is padded with
735 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
739 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
743 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
748 >>> for val in itergroup("123456", 3):
752 >>> for val in itergroup("123456", 3):
753 ... print repr("".join(val))
757 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
758 nIterators = (paddedIterator, ) * count
759 return itertools.izip(*nIterators)
763 _TRUE_REGEX = re.compile("true")
764 _FALSE_REGEX = re.compile("false")
765 s = _TRUE_REGEX.sub("True", s)
766 s = _FALSE_REGEX.sub("False", s)
767 return eval(s, {}, {})
770 def _fake_parse_json(flattened):
771 return safe_eval(flattened)
774 def _actual_parse_json(flattened):
775 return simplejson.loads(flattened)
778 if simplejson is None:
779 parse_json = _fake_parse_json
781 parse_json = _actual_parse_json
784 def extract_payload(flatXml):
785 xmlTree = ElementTree.fromstring(flatXml)
787 jsonElement = xmlTree.getchildren()[0]
788 flatJson = jsonElement.text
789 jsonTree = parse_json(flatJson)
791 htmlElement = xmlTree.getchildren()[1]
792 flatHtml = htmlElement.text
794 return jsonTree, flatHtml
797 def validate_response(response):
799 Validates that the JSON response is A-OK
802 assert 'ok' in response and response['ok']
803 except AssertionError:
804 raise RuntimeError('There was a problem with GV: %s' % response)
807 def guess_phone_type(number):
808 if number.startswith("747") or number.startswith("1747"):
809 return GVoiceBackend.PHONE_TYPE_GIZMO
811 return GVoiceBackend.PHONE_TYPE_MOBILE
814 def set_sane_callback(backend):
816 Try to set a sane default callback number on these preferences
817 1) 1747 numbers ( Gizmo )
818 2) anything with gizmo in the name
819 3) anything with computer in the name
822 numbers = backend.get_callback_numbers()
824 priorityOrderedCriteria = [
832 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
833 for number, description in numbers.iteritems():
834 if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
836 if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
838 backend.set_callback_number(number)
842 def _is_not_special(name):
843 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
847 members = inspect.getmembers(obj)
848 return dict((name, value) for (name, value) in members if _is_not_special(name))
851 def grab_debug_info(username, password):
852 cookieFile = os.path.join(".", "raw_cookies.txt")
854 os.remove(cookieFile)
858 backend = GVoiceBackend(cookieFile)
859 browser = backend._browser
862 ("forward", backend._forwardURL),
863 ("token", backend._tokenURL),
864 ("login", backend._loginURL),
865 ("isdnd", backend._isDndURL),
866 ("account", backend._XML_ACCOUNT_URL),
867 ("contacts", backend._XML_CONTACTS_URL),
869 ("voicemail", backend._XML_VOICEMAIL_URL),
870 ("sms", backend._XML_SMS_URL),
872 ("recent", backend._XML_RECENT_URL),
873 ("placed", backend._XML_PLACED_URL),
874 ("recieved", backend._XML_RECEIVED_URL),
875 ("missed", backend._XML_MISSED_URL),
879 print "Grabbing pre-login pages"
880 for name, url in _TEST_WEBPAGES:
882 page = browser.download(url)
883 except StandardError, e:
886 print "\tWriting to file"
887 with open("not_loggedin_%s.txt" % name, "w") as f:
891 print "Attempting login"
892 galxToken = backend._get_token()
893 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
894 with open("loggingin.txt", "w") as f:
895 print "\tWriting to file"
896 f.write(loginSuccessOrFailurePage)
898 backend._grab_account_info(loginSuccessOrFailurePage)
900 # Retry in case the redirect failed
901 # luckily is_authed does everything we need for a retry
902 loggedIn = backend.is_authed(True)
907 print "Grabbing post-login pages"
908 for name, url in _TEST_WEBPAGES:
910 page = browser.download(url)
911 except StandardError, e:
914 print "\tWriting to file"
915 with open("loggedin_%s.txt" % name, "w") as f:
919 browser.save_cookies()
920 print "\tWriting cookies to file"
921 with open("cookies.txt", "w") as f:
923 "%s: %s\n" % (c.name, c.value)
924 for c in browser._cookies
930 logging.basicConfig(level=logging.DEBUG)
936 grab_debug_info(username, password)
939 if __name__ == "__main__":