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_voicemails(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)
489 smsPage = self._get_page(self._XML_SMS_URL)
490 smsHtml = self._grab_html(smsPage)
491 smsJson = self._grab_json(smsPage)
492 parsedSms = self._parse_sms(smsHtml)
493 smss = self._merge_conversation_sources(parsedSms, smsJson)
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")
551 def _parse_history(self, historyHtml):
552 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
553 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
554 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
555 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
556 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
557 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
558 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
559 locationGroup = self._voicemailLocationRegex.search(messageHtml)
560 location = locationGroup.group(1).strip() if locationGroup else ""
562 nameGroup = self._voicemailNameRegex.search(messageHtml)
563 name = nameGroup.group(1).strip() if nameGroup else ""
564 numberGroup = self._voicemailNumberRegex.search(messageHtml)
565 number = numberGroup.group(1).strip() if numberGroup else ""
566 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
567 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
568 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
569 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
572 "id": messageId.strip(),
573 "contactId": contactId,
576 "relTime": relativeTime,
577 "prettyNumber": prettyNumber,
579 "location": location,
583 def _interpret_voicemail_regex(group):
584 quality, content, number = group.group(2), group.group(3), group.group(4)
586 if quality is not None and content is not None:
587 text.accuracy = quality
590 elif number is not None:
591 text.accuracy = MessageText.ACCURACY_HIGH
595 def _parse_voicemail(self, voicemailHtml):
596 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
597 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
598 conv = Conversation()
599 conv.type = Conversation.TYPE_VOICEMAIL
600 conv.id = messageId.strip()
602 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
603 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
604 conv.time = datetime.datetime.strptime(exactTimeText, "%m/%d/%y %I:%M %p")
605 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
606 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
607 locationGroup = self._voicemailLocationRegex.search(messageHtml)
608 conv.location = locationGroup.group(1).strip() if locationGroup else ""
610 nameGroup = self._voicemailNameRegex.search(messageHtml)
611 conv.name = nameGroup.group(1).strip() if nameGroup else ""
612 numberGroup = self._voicemailNumberRegex.search(messageHtml)
613 conv.number = numberGroup.group(1).strip() if numberGroup else ""
614 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
615 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
616 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
617 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
619 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
621 self._interpret_voicemail_regex(group)
622 for group in messageGroups
623 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
625 message.body = messageParts
626 message.whoFrom = conv.name
627 message.when = conv.time.strftime("%I:%M %p")
628 conv.messages = (message, )
633 def _interpret_sms_message_parts(fromPart, textPart, timePart):
635 text.accuracy = MessageText.ACCURACY_MEDIUM
639 message.body = (text, )
640 message.whoFrom = fromPart
641 message.when = timePart
645 def _parse_sms(self, smsHtml):
646 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
647 for messageId, messageHtml in itergroup(splitSms[1:], 2):
648 conv = Conversation()
649 conv.type = Conversation.TYPE_SMS
650 conv.id = messageId.strip()
652 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
653 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
654 conv.time = datetime.datetime.strptime(exactTimeText, "%m/%d/%y %I:%M %p")
655 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
656 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
659 nameGroup = self._voicemailNameRegex.search(messageHtml)
660 conv.name = nameGroup.group(1).strip() if nameGroup else ""
661 numberGroup = self._voicemailNumberRegex.search(messageHtml)
662 conv.number = numberGroup.group(1).strip() if numberGroup else ""
663 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
664 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
665 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
666 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
668 fromGroups = self._smsFromRegex.finditer(messageHtml)
669 fromParts = (group.group(1).strip() for group in fromGroups)
670 textGroups = self._smsTextRegex.finditer(messageHtml)
671 textParts = (group.group(1).strip() for group in textGroups)
672 timeGroups = self._smsTimeRegex.finditer(messageHtml)
673 timeParts = (group.group(1).strip() for group in timeGroups)
675 messageParts = itertools.izip(fromParts, textParts, timeParts)
676 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
677 conv.messages = messages
682 def _merge_conversation_sources(parsedMessages, json):
683 for message in parsedMessages:
684 jsonItem = json["messages"][message.id]
685 message.isRead = jsonItem["isRead"]
686 message.isSpam = jsonItem["isSpam"]
687 message.isTrash = jsonItem["isTrash"]
688 message.isArchived = "inbox" not in jsonItem["labels"]
691 def _get_page(self, url, data = None, refererUrl = None):
693 if refererUrl is not None:
694 headers["Referer"] = refererUrl
696 encodedData = urllib.urlencode(data) if data is not None else None
699 page = self._browser.download(url, encodedData, None, headers)
700 except urllib2.URLError, e:
701 _moduleLogger.error("Translating error: %s" % str(e))
702 raise NetworkError("%s is not accesible" % url)
706 def _get_page_with_token(self, url, data = None, refererUrl = None):
709 data['_rnr_se'] = self._token
711 page = self._get_page(url, data, refererUrl)
715 def _parse_with_validation(self, page):
716 json = parse_json(page)
717 validate_response(json)
721 def itergroup(iterator, count, padValue = None):
723 Iterate in groups of 'count' values. If there
724 aren't enough values, the last result is padded with
727 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
731 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
735 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
740 >>> for val in itergroup("123456", 3):
744 >>> for val in itergroup("123456", 3):
745 ... print repr("".join(val))
749 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
750 nIterators = (paddedIterator, ) * count
751 return itertools.izip(*nIterators)
755 _TRUE_REGEX = re.compile("true")
756 _FALSE_REGEX = re.compile("false")
757 s = _TRUE_REGEX.sub("True", s)
758 s = _FALSE_REGEX.sub("False", s)
759 return eval(s, {}, {})
762 def _fake_parse_json(flattened):
763 return safe_eval(flattened)
766 def _actual_parse_json(flattened):
767 return simplejson.loads(flattened)
770 if simplejson is None:
771 parse_json = _fake_parse_json
773 parse_json = _actual_parse_json
776 def extract_payload(flatXml):
777 xmlTree = ElementTree.fromstring(flatXml)
779 jsonElement = xmlTree.getchildren()[0]
780 flatJson = jsonElement.text
781 jsonTree = parse_json(flatJson)
783 htmlElement = xmlTree.getchildren()[1]
784 flatHtml = htmlElement.text
786 return jsonTree, flatHtml
789 def validate_response(response):
791 Validates that the JSON response is A-OK
794 assert 'ok' in response and response['ok']
795 except AssertionError:
796 raise RuntimeError('There was a problem with GV: %s' % response)
799 def guess_phone_type(number):
800 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
801 return GVoiceBackend.PHONE_TYPE_GIZMO
803 return GVoiceBackend.PHONE_TYPE_MOBILE
806 def get_sane_callback(backend):
808 Try to set a sane default callback number on these preferences
809 1) 1747 numbers ( Gizmo )
810 2) anything with gizmo in the name
811 3) anything with computer in the name
814 numbers = backend.get_callback_numbers()
816 priorityOrderedCriteria = [
826 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
828 descriptionMatcher = None
829 if numberCriteria is not None:
830 numberMatcher = re.compile(numberCriteria)
831 elif descriptionCriteria is not None:
832 descriptionMatcher = re.compile(descriptionCriteria, re.I)
834 for number, description in numbers.iteritems():
835 if numberMatcher is not None and numberMatcher.match(number) is None:
837 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
842 def set_sane_callback(backend):
844 Try to set a sane default callback number on these preferences
845 1) 1747 numbers ( Gizmo )
846 2) anything with gizmo in the name
847 3) anything with computer in the name
850 number = get_sane_callback(backend)
851 backend.set_callback_number(number)
854 def _is_not_special(name):
855 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
859 members = inspect.getmembers(obj)
860 return dict((name, value) for (name, value) in members if _is_not_special(name))
863 def grab_debug_info(username, password):
864 cookieFile = os.path.join(".", "raw_cookies.txt")
866 os.remove(cookieFile)
870 backend = GVoiceBackend(cookieFile)
871 browser = backend._browser
874 ("forward", backend._forwardURL),
875 ("token", backend._tokenURL),
876 ("login", backend._loginURL),
877 ("isdnd", backend._isDndURL),
878 ("account", backend._XML_ACCOUNT_URL),
879 ("contacts", backend._XML_CONTACTS_URL),
881 ("voicemail", backend._XML_VOICEMAIL_URL),
882 ("sms", backend._XML_SMS_URL),
884 ("recent", backend._XML_RECENT_URL),
885 ("placed", backend._XML_PLACED_URL),
886 ("recieved", backend._XML_RECEIVED_URL),
887 ("missed", backend._XML_MISSED_URL),
891 print "Grabbing pre-login pages"
892 for name, url in _TEST_WEBPAGES:
894 page = browser.download(url)
895 except StandardError, e:
898 print "\tWriting to file"
899 with open("not_loggedin_%s.txt" % name, "w") as f:
903 print "Attempting login"
904 galxToken = backend._get_token()
905 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
906 with open("loggingin.txt", "w") as f:
907 print "\tWriting to file"
908 f.write(loginSuccessOrFailurePage)
910 backend._grab_account_info(loginSuccessOrFailurePage)
912 # Retry in case the redirect failed
913 # luckily is_authed does everything we need for a retry
914 loggedIn = backend.is_authed(True)
919 print "Grabbing post-login pages"
920 for name, url in _TEST_WEBPAGES:
922 page = browser.download(url)
923 except StandardError, e:
926 print "\tWriting to file"
927 with open("loggedin_%s.txt" % name, "w") as f:
931 browser.save_cookies()
932 print "\tWriting cookies to file"
933 with open("cookies.txt", "w") as f:
935 "%s: %s\n" % (c.name, c.value)
936 for c in browser._cookies
942 logging.basicConfig(level=logging.DEBUG)
948 grab_debug_info(username, password)
951 if __name__ == "__main__":