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
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, phoneNumbers, message):
368 validatedPhoneNumbers = [
369 self._send_validation(phoneNumber)
370 for phoneNumber in phoneNumbers
372 flattenedPhoneNumbers = ",".join(validatedPhoneNumbers)
373 page = self._get_page_with_token(
376 'phoneNumber': flattenedPhoneNumbers,
380 self._parse_with_validation(page)
382 def search(self, query):
384 Search your Google Voice Account history for calls, voicemails, and sms
385 Returns ``Folder`` instance containting matching messages
387 page = self._get_page(
388 self._XML_SEARCH_URL,
391 json, html = extract_payload(page)
394 def get_feed(self, feed):
395 actualFeed = "_XML_%s_URL" % feed.upper()
396 feedUrl = getattr(self, actualFeed)
398 page = self._get_page(feedUrl)
399 json, html = extract_payload(page)
403 def download(self, messageId, adir):
405 Download a voicemail or recorded call MP3 matching the given ``msg``
406 which can either be a ``Message`` instance, or a SHA1 identifier.
407 Saves files to ``adir`` (defaults to current directory).
408 Message hashes can be found in ``self.voicemail().messages`` for example.
409 Returns location of saved file.
411 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
412 fn = os.path.join(adir, '%s.mp3' % messageId)
413 with open(fn, 'wb') as fo:
417 def is_valid_syntax(self, number):
419 @returns If This number be called ( syntax validation only )
421 return self._validateRe.match(number) is not None
423 def get_account_number(self):
425 @returns The GoogleVoice phone number
427 return self._accountNum
429 def get_callback_numbers(self):
431 @returns a dictionary mapping call back numbers to descriptions
432 @note These results are cached for 30 minutes.
434 if not self.is_authed():
436 return self._callbackNumbers
438 def set_callback_number(self, callbacknumber):
440 Set the number that GoogleVoice calls
441 @param callbacknumber should be a proper 10 digit number
443 self._callbackNumber = callbacknumber
444 _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
447 def get_callback_number(self):
449 @returns Current callback number or None
451 return self._callbackNumber
453 def get_recent(self):
455 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
458 ("Received", self._XML_RECEIVED_URL),
459 ("Missed", self._XML_MISSED_URL),
460 ("Placed", self._XML_PLACED_URL),
462 flatXml = self._get_page(url)
464 allRecentHtml = self._grab_html(flatXml)
465 allRecentData = self._parse_history(allRecentHtml)
466 for recentCallData in allRecentData:
467 recentCallData["action"] = action
470 def get_contacts(self):
472 @returns Iterable of (contact id, contact name)
474 page = self._get_page(self._XML_CONTACTS_URL)
475 contactsBody = self._contactsBodyRe.search(page)
476 if contactsBody is None:
477 raise RuntimeError("Could not extract contact information")
478 accountData = _fake_parse_json(contactsBody.group(1))
479 for contactId, contactDetails in accountData["contacts"].iteritems():
480 # A zero contact id is the catch all for unknown contacts
482 yield contactId, contactDetails
484 def get_voicemails(self):
485 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
486 voicemailHtml = self._grab_html(voicemailPage)
487 voicemailJson = self._grab_json(voicemailPage)
488 parsedVoicemail = self._parse_voicemail(voicemailHtml)
489 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
493 smsPage = self._get_page(self._XML_SMS_URL)
494 smsHtml = self._grab_html(smsPage)
495 smsJson = self._grab_json(smsPage)
496 parsedSms = self._parse_sms(smsHtml)
497 smss = self._merge_conversation_sources(parsedSms, smsJson)
500 def mark_message(self, messageId, asRead):
502 "read": 1 if asRead else 0,
506 markPage = self._get_page(self._markAsReadURL, postData)
508 def archive_message(self, messageId):
513 markPage = self._get_page(self._archiveMessageURL, postData)
515 def _grab_json(self, flatXml):
516 xmlTree = ElementTree.fromstring(flatXml)
517 jsonElement = xmlTree.getchildren()[0]
518 flatJson = jsonElement.text
519 jsonTree = parse_json(flatJson)
522 def _grab_html(self, flatXml):
523 xmlTree = ElementTree.fromstring(flatXml)
524 htmlElement = xmlTree.getchildren()[1]
525 flatHtml = htmlElement.text
528 def _grab_account_info(self, page):
529 tokenGroup = self._tokenRe.search(page)
530 if tokenGroup is None:
531 raise RuntimeError("Could not extract authentication token from GoogleVoice")
532 self._token = tokenGroup.group(1)
534 anGroup = self._accountNumRe.search(page)
535 if anGroup is not None:
536 self._accountNum = anGroup.group(1)
538 _moduleLogger.debug("Could not extract account number from GoogleVoice")
540 self._callbackNumbers = {}
541 for match in self._callbackRe.finditer(page):
542 callbackNumber = match.group(2)
543 callbackName = match.group(1)
544 self._callbackNumbers[callbackNumber] = callbackName
545 if len(self._callbackNumbers) == 0:
546 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
548 def _send_validation(self, number):
549 if not self.is_valid_syntax(number):
550 raise ValueError('Number is not valid: "%s"' % number)
551 elif not self.is_authed():
552 raise RuntimeError("Not Authenticated")
555 def _parse_history(self, historyHtml):
556 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
557 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
558 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
559 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
560 exactTime = google_strptime(exactTime)
561 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
562 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
563 locationGroup = self._voicemailLocationRegex.search(messageHtml)
564 location = locationGroup.group(1).strip() if locationGroup else ""
566 nameGroup = self._voicemailNameRegex.search(messageHtml)
567 name = nameGroup.group(1).strip() if nameGroup else ""
568 numberGroup = self._voicemailNumberRegex.search(messageHtml)
569 number = numberGroup.group(1).strip() if numberGroup else ""
570 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
571 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
572 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
573 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
576 "id": messageId.strip(),
577 "contactId": contactId,
580 "relTime": relativeTime,
581 "prettyNumber": prettyNumber,
583 "location": location,
587 def _interpret_voicemail_regex(group):
588 quality, content, number = group.group(2), group.group(3), group.group(4)
590 if quality is not None and content is not None:
591 text.accuracy = quality
594 elif number is not None:
595 text.accuracy = MessageText.ACCURACY_HIGH
599 def _parse_voicemail(self, voicemailHtml):
600 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
601 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
602 conv = Conversation()
603 conv.type = Conversation.TYPE_VOICEMAIL
604 conv.id = messageId.strip()
606 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
607 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
608 conv.time = google_strptime(exactTimeText)
609 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
610 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
611 locationGroup = self._voicemailLocationRegex.search(messageHtml)
612 conv.location = locationGroup.group(1).strip() if locationGroup else ""
614 nameGroup = self._voicemailNameRegex.search(messageHtml)
615 conv.name = nameGroup.group(1).strip() if nameGroup else ""
616 numberGroup = self._voicemailNumberRegex.search(messageHtml)
617 conv.number = numberGroup.group(1).strip() if numberGroup else ""
618 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
619 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
620 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
621 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
623 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
625 self._interpret_voicemail_regex(group)
626 for group in messageGroups
627 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
629 message.body = messageParts
630 message.whoFrom = conv.name
631 message.when = conv.time.strftime("%I:%M %p")
632 conv.messages = (message, )
637 def _interpret_sms_message_parts(fromPart, textPart, timePart):
639 text.accuracy = MessageText.ACCURACY_MEDIUM
643 message.body = (text, )
644 message.whoFrom = fromPart
645 message.when = timePart
649 def _parse_sms(self, smsHtml):
650 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
651 for messageId, messageHtml in itergroup(splitSms[1:], 2):
652 conv = Conversation()
653 conv.type = Conversation.TYPE_SMS
654 conv.id = messageId.strip()
656 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
657 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
658 conv.time = google_strptime(exactTimeText)
659 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
660 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
663 nameGroup = self._voicemailNameRegex.search(messageHtml)
664 conv.name = nameGroup.group(1).strip() if nameGroup else ""
665 numberGroup = self._voicemailNumberRegex.search(messageHtml)
666 conv.number = numberGroup.group(1).strip() if numberGroup else ""
667 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
668 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
669 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
670 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
672 fromGroups = self._smsFromRegex.finditer(messageHtml)
673 fromParts = (group.group(1).strip() for group in fromGroups)
674 textGroups = self._smsTextRegex.finditer(messageHtml)
675 textParts = (group.group(1).strip() for group in textGroups)
676 timeGroups = self._smsTimeRegex.finditer(messageHtml)
677 timeParts = (group.group(1).strip() for group in timeGroups)
679 messageParts = itertools.izip(fromParts, textParts, timeParts)
680 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
681 conv.messages = messages
686 def _merge_conversation_sources(parsedMessages, json):
687 for message in parsedMessages:
688 jsonItem = json["messages"][message.id]
689 message.isRead = jsonItem["isRead"]
690 message.isSpam = jsonItem["isSpam"]
691 message.isTrash = jsonItem["isTrash"]
692 message.isArchived = "inbox" not in jsonItem["labels"]
695 def _get_page(self, url, data = None, refererUrl = None):
697 if refererUrl is not None:
698 headers["Referer"] = refererUrl
700 encodedData = urllib.urlencode(data) if data is not None else None
703 page = self._browser.download(url, encodedData, None, headers)
704 except urllib2.URLError, e:
705 _moduleLogger.error("Translating error: %s" % str(e))
706 raise NetworkError("%s is not accesible" % url)
710 def _get_page_with_token(self, url, data = None, refererUrl = None):
713 data['_rnr_se'] = self._token
715 page = self._get_page(url, data, refererUrl)
719 def _parse_with_validation(self, page):
720 json = parse_json(page)
721 validate_response(json)
725 def google_strptime(time):
727 Hack: Google always returns the time in the same locale. Sadly if the
728 local system's locale is different, there isn't a way to perfectly handle
729 the time. So instead we handle implement some time formatting
731 abbrevTime = time[:-3]
732 parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
734 parsedTime += datetime.timedelta(hours=12)
738 def itergroup(iterator, count, padValue = None):
740 Iterate in groups of 'count' values. If there
741 aren't enough values, the last result is padded with
744 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
748 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
752 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
757 >>> for val in itergroup("123456", 3):
761 >>> for val in itergroup("123456", 3):
762 ... print repr("".join(val))
766 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
767 nIterators = (paddedIterator, ) * count
768 return itertools.izip(*nIterators)
772 _TRUE_REGEX = re.compile("true")
773 _FALSE_REGEX = re.compile("false")
774 s = _TRUE_REGEX.sub("True", s)
775 s = _FALSE_REGEX.sub("False", s)
776 return eval(s, {}, {})
779 def _fake_parse_json(flattened):
780 return safe_eval(flattened)
783 def _actual_parse_json(flattened):
784 return simplejson.loads(flattened)
787 if simplejson is None:
788 parse_json = _fake_parse_json
790 parse_json = _actual_parse_json
793 def extract_payload(flatXml):
794 xmlTree = ElementTree.fromstring(flatXml)
796 jsonElement = xmlTree.getchildren()[0]
797 flatJson = jsonElement.text
798 jsonTree = parse_json(flatJson)
800 htmlElement = xmlTree.getchildren()[1]
801 flatHtml = htmlElement.text
803 return jsonTree, flatHtml
806 def validate_response(response):
808 Validates that the JSON response is A-OK
811 assert 'ok' in response and response['ok']
812 except AssertionError:
813 raise RuntimeError('There was a problem with GV: %s' % response)
816 def guess_phone_type(number):
817 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
818 return GVoiceBackend.PHONE_TYPE_GIZMO
820 return GVoiceBackend.PHONE_TYPE_MOBILE
823 def get_sane_callback(backend):
825 Try to set a sane default callback number on these preferences
826 1) 1747 numbers ( Gizmo )
827 2) anything with gizmo in the name
828 3) anything with computer in the name
831 numbers = backend.get_callback_numbers()
833 priorityOrderedCriteria = [
843 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
845 descriptionMatcher = None
846 if numberCriteria is not None:
847 numberMatcher = re.compile(numberCriteria)
848 elif descriptionCriteria is not None:
849 descriptionMatcher = re.compile(descriptionCriteria, re.I)
851 for number, description in numbers.iteritems():
852 if numberMatcher is not None and numberMatcher.match(number) is None:
854 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
859 def set_sane_callback(backend):
861 Try to set a sane default callback number on these preferences
862 1) 1747 numbers ( Gizmo )
863 2) anything with gizmo in the name
864 3) anything with computer in the name
867 number = get_sane_callback(backend)
868 backend.set_callback_number(number)
871 def _is_not_special(name):
872 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
876 members = inspect.getmembers(obj)
877 return dict((name, value) for (name, value) in members if _is_not_special(name))
880 def grab_debug_info(username, password):
881 cookieFile = os.path.join(".", "raw_cookies.txt")
883 os.remove(cookieFile)
887 backend = GVoiceBackend(cookieFile)
888 browser = backend._browser
891 ("forward", backend._forwardURL),
892 ("token", backend._tokenURL),
893 ("login", backend._loginURL),
894 ("isdnd", backend._isDndURL),
895 ("account", backend._XML_ACCOUNT_URL),
896 ("contacts", backend._XML_CONTACTS_URL),
898 ("voicemail", backend._XML_VOICEMAIL_URL),
899 ("sms", backend._XML_SMS_URL),
901 ("recent", backend._XML_RECENT_URL),
902 ("placed", backend._XML_PLACED_URL),
903 ("recieved", backend._XML_RECEIVED_URL),
904 ("missed", backend._XML_MISSED_URL),
908 print "Grabbing pre-login pages"
909 for name, url in _TEST_WEBPAGES:
911 page = browser.download(url)
912 except StandardError, e:
915 print "\tWriting to file"
916 with open("not_loggedin_%s.txt" % name, "w") as f:
920 print "Attempting login"
921 galxToken = backend._get_token()
922 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
923 with open("loggingin.txt", "w") as f:
924 print "\tWriting to file"
925 f.write(loginSuccessOrFailurePage)
927 backend._grab_account_info(loginSuccessOrFailurePage)
929 # Retry in case the redirect failed
930 # luckily is_authed does everything we need for a retry
931 loggedIn = backend.is_authed(True)
936 print "Grabbing post-login pages"
937 for name, url in _TEST_WEBPAGES:
939 page = browser.download(url)
940 except StandardError, e:
943 print "\tWriting to file"
944 with open("loggedin_%s.txt" % name, "w") as f:
948 browser.save_cookies()
949 print "\tWriting cookies to file"
950 with open("cookies.txt", "w") as f:
952 "%s: %s\n" % (c.name, c.value)
953 for c in browser._cookies
959 logging.basicConfig(level=logging.DEBUG)
965 grab_debug_info(username, password)
968 if __name__ == "__main__":