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
442 def get_callback_number(self):
444 @returns Current callback number or None
446 return self._callbackNumber
448 def get_recent(self):
450 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
453 ("Received", self._XML_RECEIVED_URL),
454 ("Missed", self._XML_MISSED_URL),
455 ("Placed", self._XML_PLACED_URL),
457 flatXml = self._get_page(url)
459 allRecentHtml = self._grab_html(flatXml)
460 allRecentData = self._parse_history(allRecentHtml)
461 for recentCallData in allRecentData:
462 recentCallData["action"] = action
465 def get_contacts(self):
467 @returns Iterable of (contact id, contact name)
469 page = self._get_page(self._XML_CONTACTS_URL)
470 contactsBody = self._contactsBodyRe.search(page)
471 if contactsBody is None:
472 raise RuntimeError("Could not extract contact information")
473 accountData = _fake_parse_json(contactsBody.group(1))
474 for contactId, contactDetails in accountData["contacts"].iteritems():
475 # A zero contact id is the catch all for unknown contacts
477 yield contactId, contactDetails
479 def get_conversations(self):
480 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
481 voicemailHtml = self._grab_html(voicemailPage)
482 voicemailJson = self._grab_json(voicemailPage)
483 parsedVoicemail = self._parse_voicemail(voicemailHtml)
484 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
486 smsPage = self._get_page(self._XML_SMS_URL)
487 smsHtml = self._grab_html(smsPage)
488 smsJson = self._grab_json(smsPage)
489 parsedSms = self._parse_sms(smsHtml)
490 smss = self._merge_conversation_sources(parsedSms, smsJson)
491 decoratedSms = self._decorate_sms(smss)
493 allConversations = itertools.chain(voicemails, decoratedSms)
494 return allConversations
496 def mark_message(self, messageId, asRead):
498 "read": 1 if asRead else 0,
502 markPage = self._get_page(self._markAsReadURL, postData)
504 def archive_message(self, messageId):
509 markPage = self._get_page(self._archiveMessageURL, postData)
511 def _grab_json(self, flatXml):
512 xmlTree = ElementTree.fromstring(flatXml)
513 jsonElement = xmlTree.getchildren()[0]
514 flatJson = jsonElement.text
515 jsonTree = parse_json(flatJson)
518 def _grab_html(self, flatXml):
519 xmlTree = ElementTree.fromstring(flatXml)
520 htmlElement = xmlTree.getchildren()[1]
521 flatHtml = htmlElement.text
524 def _grab_account_info(self, page):
525 tokenGroup = self._tokenRe.search(page)
526 if tokenGroup is None:
527 raise RuntimeError("Could not extract authentication token from GoogleVoice")
528 self._token = tokenGroup.group(1)
530 anGroup = self._accountNumRe.search(page)
531 if anGroup is not None:
532 self._accountNum = anGroup.group(1)
534 _moduleLogger.debug("Could not extract account number from GoogleVoice")
536 self._callbackNumbers = {}
537 for match in self._callbackRe.finditer(page):
538 callbackNumber = match.group(2)
539 callbackName = match.group(1)
540 self._callbackNumbers[callbackNumber] = callbackName
541 if len(self._callbackNumbers) == 0:
542 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
544 def _send_validation(self, number):
545 if not self.is_valid_syntax(number):
546 raise ValueError('Number is not valid: "%s"' % number)
547 elif not self.is_authed():
548 raise RuntimeError("Not Authenticated")
550 if len(number) == 11 and number[0] == 1:
551 # Strip leading 1 from 11 digit dialing
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 = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
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 = datetime.datetime.strptime(exactTimeText, "%m/%d/%y %I:%M %p")
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 = datetime.datetime.strptime(exactTimeText, "%m/%d/%y %I:%M %p")
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
685 def _decorate_sms(self, parsedTexts):
689 def _merge_conversation_sources(parsedMessages, json):
690 for message in parsedMessages:
691 jsonItem = json["messages"][message.id]
692 message.isRead = jsonItem["isRead"]
693 message.isSpam = jsonItem["isSpam"]
694 message.isTrash = jsonItem["isTrash"]
695 message.isArchived = "inbox" not in jsonItem["labels"]
698 def _get_page(self, url, data = None, refererUrl = None):
700 if refererUrl is not None:
701 headers["Referer"] = refererUrl
703 encodedData = urllib.urlencode(data) if data is not None else None
706 page = self._browser.download(url, encodedData, None, headers)
707 except urllib2.URLError, e:
708 _moduleLogger.error("Translating error: %s" % str(e))
709 raise NetworkError("%s is not accesible" % url)
713 def _get_page_with_token(self, url, data = None, refererUrl = None):
716 data['_rnr_se'] = self._token
718 page = self._get_page(url, data, refererUrl)
722 def _parse_with_validation(self, page):
723 json = parse_json(page)
724 validate_response(json)
728 def itergroup(iterator, count, padValue = None):
730 Iterate in groups of 'count' values. If there
731 aren't enough values, the last result is padded with
734 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
738 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
742 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
747 >>> for val in itergroup("123456", 3):
751 >>> for val in itergroup("123456", 3):
752 ... print repr("".join(val))
756 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
757 nIterators = (paddedIterator, ) * count
758 return itertools.izip(*nIterators)
762 _TRUE_REGEX = re.compile("true")
763 _FALSE_REGEX = re.compile("false")
764 s = _TRUE_REGEX.sub("True", s)
765 s = _FALSE_REGEX.sub("False", s)
766 return eval(s, {}, {})
769 def _fake_parse_json(flattened):
770 return safe_eval(flattened)
773 def _actual_parse_json(flattened):
774 return simplejson.loads(flattened)
777 if simplejson is None:
778 parse_json = _fake_parse_json
780 parse_json = _actual_parse_json
783 def extract_payload(flatXml):
784 xmlTree = ElementTree.fromstring(flatXml)
786 jsonElement = xmlTree.getchildren()[0]
787 flatJson = jsonElement.text
788 jsonTree = parse_json(flatJson)
790 htmlElement = xmlTree.getchildren()[1]
791 flatHtml = htmlElement.text
793 return jsonTree, flatHtml
796 def validate_response(response):
798 Validates that the JSON response is A-OK
801 assert 'ok' in response and response['ok']
802 except AssertionError:
803 raise RuntimeError('There was a problem with GV: %s' % response)
806 def guess_phone_type(number):
807 if number.startswith("747") or number.startswith("1747"):
808 return GVoiceBackend.PHONE_TYPE_GIZMO
810 return GVoiceBackend.PHONE_TYPE_MOBILE
813 def set_sane_callback(backend):
815 Try to set a sane default callback number on these preferences
816 1) 1747 numbers ( Gizmo )
817 2) anything with gizmo in the name
818 3) anything with computer in the name
821 numbers = backend.get_callback_numbers()
823 priorityOrderedCriteria = [
831 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
832 for number, description in numbers.iteritems():
833 if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
835 if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
837 backend.set_callback_number(number)
841 def _is_not_special(name):
842 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
846 members = inspect.getmembers(obj)
847 return dict((name, value) for (name, value) in members if _is_not_special(name))
850 def grab_debug_info(username, password):
851 cookieFile = os.path.join(".", "raw_cookies.txt")
853 os.remove(cookieFile)
857 backend = GVoiceBackend(cookieFile)
858 browser = backend._browser
861 ("forward", backend._forwardURL),
862 ("token", backend._tokenURL),
863 ("login", backend._loginURL),
864 ("isdnd", backend._isDndURL),
865 ("account", backend._XML_ACCOUNT_URL),
866 ("contacts", backend._XML_CONTACTS_URL),
868 ("voicemail", backend._XML_VOICEMAIL_URL),
869 ("sms", backend._XML_SMS_URL),
871 ("recent", backend._XML_RECENT_URL),
872 ("placed", backend._XML_PLACED_URL),
873 ("recieved", backend._XML_RECEIVED_URL),
874 ("missed", backend._XML_MISSED_URL),
878 print "Grabbing pre-login pages"
879 for name, url in _TEST_WEBPAGES:
881 page = browser.download(url)
882 except StandardError, e:
885 print "\tWriting to file"
886 with open("not_loggedin_%s.txt" % name, "w") as f:
890 print "Attempting login"
891 galxToken = backend._get_token()
892 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
893 with open("loggingin.txt", "w") as f:
894 print "\tWriting to file"
895 f.write(loginSuccessOrFailurePage)
897 backend._grab_account_info(loginSuccessOrFailurePage)
899 # Retry in case the redirect failed
900 # luckily is_authed does everything we need for a retry
901 loggedIn = backend.is_authed(True)
906 print "Grabbing post-login pages"
907 for name, url in _TEST_WEBPAGES:
909 page = browser.download(url)
910 except StandardError, e:
913 print "\tWriting to file"
914 with open("loggedin_%s.txt" % name, "w") as f:
918 browser.save_cookies()
919 print "\tWriting cookies to file"
920 with open("cookies.txt", "w") as f:
922 "%s: %s\n" % (c.name, c.value)
923 for c in browser._cookies
929 logging.basicConfig(level=logging.DEBUG)
935 grab_debug_info(username, password)
938 if __name__ == "__main__":