4 DialCentral - Front end for Google's GoogleVoice service.
5 Copyright (C) 2008 Eric Warnke ericew AT gmail DOT com
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 Google Voice backend code
24 http://thatsmith.com/2009/03/google-voice-addon-for-firefox/
25 http://posttopic.com/topic/google-voice-add-on-development
28 from __future__ import with_statement
40 from xml.etree import ElementTree
43 import simplejson as _simplejson
44 simplejson = _simplejson
51 _moduleLogger = logging.getLogger(__name__)
54 class NetworkError(RuntimeError):
58 class MessageText(object):
61 ACCURACY_MEDIUM = "med2"
62 ACCURACY_HIGH = "high"
74 def __eq__(self, other):
75 return self.accuracy == other.accuracy and self.text == other.text
78 class Message(object):
86 return "%s (%s): %s" % (
89 "".join(str(part) for part in self.body)
93 selfDict = to_dict(self)
94 selfDict["body"] = [text.to_dict() for text in self.body] if self.body is not None else None
97 def __eq__(self, other):
98 return self.whoFrom == other.whoFrom and self.when == other.when and self.body == other.body
101 class Conversation(object):
103 TYPE_VOICEMAIL = "Voicemail"
109 self.contactId = None
112 self.prettyNumber = None
121 self.isArchived = None
123 def __cmp__(self, other):
124 cmpValue = cmp(self.contactId, other.contactId)
128 cmpValue = cmp(self.time, other.time)
132 cmpValue = cmp(self.id, other.id)
137 selfDict = to_dict(self)
138 selfDict["messages"] = [message.to_dict() for message in self.messages] if self.messages is not None else None
142 class GVoiceBackend(object):
144 This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
145 the functions include login, setting up a callback number, and initalting a callback
149 PHONE_TYPE_MOBILE = 2
153 def __init__(self, cookieFile = None):
154 # Important items in this function are the setup of the browser emulation and cookie file
155 self._browser = browser_emu.MozillaEmulator(1)
156 self._loadedFromCookies = self._browser.load_cookies(cookieFile)
159 self._accountNum = ""
160 self._lastAuthed = 0.0
161 self._callbackNumber = ""
162 self._callbackNumbers = {}
164 # Suprisingly, moving all of these from class to self sped up startup time
166 self._validateRe = re.compile("^\+?[0-9]{10,}$")
168 self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
170 SECURE_URL_BASE = "https://www.google.com/voice/"
171 SECURE_MOBILE_URL_BASE = SECURE_URL_BASE + "mobile/"
172 self._forwardURL = SECURE_MOBILE_URL_BASE + "phones"
173 self._tokenURL = SECURE_URL_BASE + "m"
174 self._callUrl = SECURE_URL_BASE + "call/connect"
175 self._callCancelURL = SECURE_URL_BASE + "call/cancel"
176 self._sendSmsURL = SECURE_URL_BASE + "sms/send"
178 self._isDndURL = "https://www.google.com/voice/m/donotdisturb"
179 self._isDndRe = re.compile(r"""<input.*?id="doNotDisturb".*?checked="(.*?)"\s*/>""")
180 self._setDndURL = "https://www.google.com/voice/m/savednd"
182 self._downloadVoicemailURL = SECURE_URL_BASE + "media/send_voicemail/"
183 self._markAsReadURL = SECURE_URL_BASE + "m/mark"
184 self._archiveMessageURL = SECURE_URL_BASE + "m/archive"
186 self._XML_SEARCH_URL = SECURE_URL_BASE + "inbox/search/"
187 self._XML_ACCOUNT_URL = SECURE_URL_BASE + "contacts/"
188 # HACK really this redirects to the main pge and we are grabbing some javascript
189 self._XML_CONTACTS_URL = "http://www.google.com/voice/inbox/search/contact"
190 self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
193 'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms',
194 'recorded', 'placed', 'received', 'missed'
196 self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox"
197 self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred"
198 self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all"
199 self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam"
200 self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash"
201 self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/"
202 self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/"
203 self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/"
204 self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/"
205 self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
206 self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
208 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
209 self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
210 self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
211 self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
213 self._contactsBodyRe = re.compile(r"""gcData\s*=\s*({.*?});""", re.MULTILINE | re.DOTALL)
214 self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
215 self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
216 self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
217 self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
218 self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
219 self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
220 self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
221 self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
222 self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
223 self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
224 self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
225 self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
227 def is_quick_login_possible(self):
229 @returns True then is_authed might be enough to login, else full login is required
231 return self._loadedFromCookies or 0.0 < self._lastAuthed
233 def is_authed(self, force = False):
235 Attempts to detect a current session
236 @note Once logged in try not to reauth more than once a minute.
237 @returns If authenticated
240 isRecentledAuthed = (time.time() - self._lastAuthed) < 120
241 isPreviouslyAuthed = self._token is not None
242 if isRecentledAuthed and isPreviouslyAuthed and not force:
246 page = self._get_page(self._forwardURL)
247 self._grab_account_info(page)
249 _moduleLogger.exception(str(e))
252 self._browser.save_cookies()
253 self._lastAuthed = time.time()
256 def _get_token(self):
257 tokenPage = self._get_page(self._tokenURL)
259 galxTokens = self._galxRe.search(tokenPage)
260 if galxTokens is not None:
261 galxToken = galxTokens.group(1)
264 _moduleLogger.debug("Could not grab GALX token")
267 def _login(self, username, password, token):
271 'service': "grandcentral",
274 "PersistentCookie": "yes",
276 "continue": self._forwardURL,
279 loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
280 return loginSuccessOrFailurePage
282 def login(self, username, password):
284 Attempt to login to GoogleVoice
285 @returns Whether login was successful or not
289 galxToken = self._get_token()
290 loginSuccessOrFailurePage = self._login(username, password, galxToken)
293 self._grab_account_info(loginSuccessOrFailurePage)
295 # Retry in case the redirect failed
296 # luckily is_authed does everything we need for a retry
297 loggedIn = self.is_authed(True)
299 _moduleLogger.exception(str(e))
301 _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
303 self._browser.save_cookies()
304 self._lastAuthed = time.time()
308 self._browser.clear_cookies()
309 self._browser.save_cookies()
311 self._lastAuthed = 0.0
317 isDndPage = self._get_page(self._isDndURL)
319 dndGroup = self._isDndRe.search(isDndPage)
322 dndStatus = dndGroup.group(1)
323 isDnd = True if dndStatus.strip().lower() == "true" else False
326 def set_dnd(self, doNotDisturb):
331 "doNotDisturb": 1 if doNotDisturb else 0,
334 dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
336 def call(self, outgoingNumber):
338 This is the main function responsible for initating the callback
341 outgoingNumber = self._send_validation(outgoingNumber)
342 subscriberNumber = None
343 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
346 'outgoingNumber': outgoingNumber,
347 'forwardingNumber': self._callbackNumber,
348 'subscriberNumber': subscriberNumber or 'undefined',
349 'phoneType': str(phoneType),
352 _moduleLogger.info("%r" % callData)
354 page = self._get_page_with_token(
358 self._parse_with_validation(page)
361 def cancel(self, outgoingNumber=None):
363 Cancels a call matching outgoing and forwarding numbers (if given).
364 Will raise an error if no matching call is being placed
367 page = self._get_page_with_token(
370 'outgoingNumber': outgoingNumber or 'undefined',
371 'forwardingNumber': self._callbackNumber or 'undefined',
375 self._parse_with_validation(page)
377 def send_sms(self, phoneNumbers, message):
381 validatedPhoneNumbers = [
382 self._send_validation(phoneNumber)
383 for phoneNumber in phoneNumbers
385 flattenedPhoneNumbers = ",".join(validatedPhoneNumbers)
386 page = self._get_page_with_token(
389 'phoneNumber': flattenedPhoneNumbers,
393 self._parse_with_validation(page)
395 def search(self, query):
397 Search your Google Voice Account history for calls, voicemails, and sms
398 Returns ``Folder`` instance containting matching messages
401 page = self._get_page(
402 self._XML_SEARCH_URL,
405 json, html = extract_payload(page)
408 def get_feed(self, feed):
412 actualFeed = "_XML_%s_URL" % feed.upper()
413 feedUrl = getattr(self, actualFeed)
415 page = self._get_page(feedUrl)
416 json, html = extract_payload(page)
420 def download(self, messageId, adir):
422 Download a voicemail or recorded call MP3 matching the given ``msg``
423 which can either be a ``Message`` instance, or a SHA1 identifier.
424 Saves files to ``adir`` (defaults to current directory).
425 Message hashes can be found in ``self.voicemail().messages`` for example.
426 @returns location of saved file.
429 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
430 fn = os.path.join(adir, '%s.mp3' % messageId)
431 with open(fn, 'wb') as fo:
435 def is_valid_syntax(self, number):
437 @returns If This number be called ( syntax validation only )
439 return self._validateRe.match(number) is not None
441 def get_account_number(self):
443 @returns The GoogleVoice phone number
445 return self._accountNum
447 def get_callback_numbers(self):
449 @returns a dictionary mapping call back numbers to descriptions
450 @note These results are cached for 30 minutes.
452 if not self.is_authed():
454 return self._callbackNumbers
456 def set_callback_number(self, callbacknumber):
458 Set the number that GoogleVoice calls
459 @param callbacknumber should be a proper 10 digit number
461 self._callbackNumber = callbacknumber
462 _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
465 def get_callback_number(self):
467 @returns Current callback number or None
469 return self._callbackNumber
471 def get_recent(self):
473 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
477 (action, self._get_page(url))
479 ("Received", self._XML_RECEIVED_URL),
480 ("Missed", self._XML_MISSED_URL),
481 ("Placed", self._XML_PLACED_URL),
484 return self._parse_recent(recentPages)
486 def get_contacts(self):
488 @returns Iterable of (contact id, contact name)
491 page = self._get_page(self._XML_CONTACTS_URL)
492 return self._process_contacts(page)
494 def get_voicemails(self):
498 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
499 voicemailHtml = self._grab_html(voicemailPage)
500 voicemailJson = self._grab_json(voicemailPage)
501 parsedVoicemail = self._parse_voicemail(voicemailHtml)
502 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
509 smsPage = self._get_page(self._XML_SMS_URL)
510 smsHtml = self._grab_html(smsPage)
511 smsJson = self._grab_json(smsPage)
512 parsedSms = self._parse_sms(smsHtml)
513 smss = self._merge_conversation_sources(parsedSms, smsJson)
516 def mark_message(self, messageId, asRead):
521 "read": 1 if asRead else 0,
525 markPage = self._get_page(self._markAsReadURL, postData)
527 def archive_message(self, messageId):
535 markPage = self._get_page(self._archiveMessageURL, postData)
537 def _grab_json(self, flatXml):
538 xmlTree = ElementTree.fromstring(flatXml)
539 jsonElement = xmlTree.getchildren()[0]
540 flatJson = jsonElement.text
541 jsonTree = parse_json(flatJson)
544 def _grab_html(self, flatXml):
545 xmlTree = ElementTree.fromstring(flatXml)
546 htmlElement = xmlTree.getchildren()[1]
547 flatHtml = htmlElement.text
550 def _grab_account_info(self, page):
551 tokenGroup = self._tokenRe.search(page)
552 if tokenGroup is None:
553 raise RuntimeError("Could not extract authentication token from GoogleVoice")
554 self._token = tokenGroup.group(1)
556 anGroup = self._accountNumRe.search(page)
557 if anGroup is not None:
558 self._accountNum = anGroup.group(1)
560 _moduleLogger.debug("Could not extract account number from GoogleVoice")
562 self._callbackNumbers = {}
563 for match in self._callbackRe.finditer(page):
564 callbackNumber = match.group(2)
565 callbackName = match.group(1)
566 self._callbackNumbers[callbackNumber] = callbackName
567 if len(self._callbackNumbers) == 0:
568 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
570 def _send_validation(self, number):
571 if not self.is_valid_syntax(number):
572 raise ValueError('Number is not valid: "%s"' % number)
573 elif not self.is_authed():
574 raise RuntimeError("Not Authenticated")
577 def _parse_recent(self, recentPages):
578 for action, flatXml in recentPages:
579 allRecentHtml = self._grab_html(flatXml)
580 allRecentData = self._parse_history(allRecentHtml)
581 for recentCallData in allRecentData:
582 recentCallData["action"] = action
585 def _process_contacts(self, page):
586 contactsBody = self._contactsBodyRe.search(page)
587 if contactsBody is None:
588 raise RuntimeError("Could not extract contact information")
589 accountData = _fake_parse_json(contactsBody.group(1))
590 for contactId, contactDetails in accountData["contacts"].iteritems():
591 # A zero contact id is the catch all for unknown contacts
593 yield contactId, contactDetails
595 def _parse_history(self, historyHtml):
596 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
597 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
598 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
599 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
600 exactTime = google_strptime(exactTime)
601 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
602 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
603 locationGroup = self._voicemailLocationRegex.search(messageHtml)
604 location = locationGroup.group(1).strip() if locationGroup else ""
606 nameGroup = self._voicemailNameRegex.search(messageHtml)
607 name = nameGroup.group(1).strip() if nameGroup else ""
608 numberGroup = self._voicemailNumberRegex.search(messageHtml)
609 number = numberGroup.group(1).strip() if numberGroup else ""
610 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
611 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
612 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
613 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
616 "id": messageId.strip(),
617 "contactId": contactId,
620 "relTime": relativeTime,
621 "prettyNumber": prettyNumber,
623 "location": location,
627 def _interpret_voicemail_regex(group):
628 quality, content, number = group.group(2), group.group(3), group.group(4)
630 if quality is not None and content is not None:
631 text.accuracy = quality
634 elif number is not None:
635 text.accuracy = MessageText.ACCURACY_HIGH
639 def _parse_voicemail(self, voicemailHtml):
640 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
641 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
642 conv = Conversation()
643 conv.type = Conversation.TYPE_VOICEMAIL
644 conv.id = messageId.strip()
646 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
647 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
648 conv.time = google_strptime(exactTimeText)
649 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
650 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
651 locationGroup = self._voicemailLocationRegex.search(messageHtml)
652 conv.location = locationGroup.group(1).strip() if locationGroup else ""
654 nameGroup = self._voicemailNameRegex.search(messageHtml)
655 conv.name = nameGroup.group(1).strip() if nameGroup else ""
656 numberGroup = self._voicemailNumberRegex.search(messageHtml)
657 conv.number = numberGroup.group(1).strip() if numberGroup else ""
658 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
659 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
660 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
661 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
663 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
665 self._interpret_voicemail_regex(group)
666 for group in messageGroups
667 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
669 message.body = messageParts
670 message.whoFrom = conv.name
671 message.when = conv.time.strftime("%I:%M %p")
672 conv.messages = (message, )
677 def _interpret_sms_message_parts(fromPart, textPart, timePart):
679 text.accuracy = MessageText.ACCURACY_MEDIUM
683 message.body = (text, )
684 message.whoFrom = fromPart
685 message.when = timePart
689 def _parse_sms(self, smsHtml):
690 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
691 for messageId, messageHtml in itergroup(splitSms[1:], 2):
692 conv = Conversation()
693 conv.type = Conversation.TYPE_SMS
694 conv.id = messageId.strip()
696 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
697 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
698 conv.time = google_strptime(exactTimeText)
699 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
700 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
703 nameGroup = self._voicemailNameRegex.search(messageHtml)
704 conv.name = nameGroup.group(1).strip() if nameGroup else ""
705 numberGroup = self._voicemailNumberRegex.search(messageHtml)
706 conv.number = numberGroup.group(1).strip() if numberGroup else ""
707 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
708 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
709 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
710 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
712 fromGroups = self._smsFromRegex.finditer(messageHtml)
713 fromParts = (group.group(1).strip() for group in fromGroups)
714 textGroups = self._smsTextRegex.finditer(messageHtml)
715 textParts = (group.group(1).strip() for group in textGroups)
716 timeGroups = self._smsTimeRegex.finditer(messageHtml)
717 timeParts = (group.group(1).strip() for group in timeGroups)
719 messageParts = itertools.izip(fromParts, textParts, timeParts)
720 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
721 conv.messages = messages
726 def _merge_conversation_sources(parsedMessages, json):
727 for message in parsedMessages:
728 jsonItem = json["messages"][message.id]
729 message.isRead = jsonItem["isRead"]
730 message.isSpam = jsonItem["isSpam"]
731 message.isTrash = jsonItem["isTrash"]
732 message.isArchived = "inbox" not in jsonItem["labels"]
735 def _get_page(self, url, data = None, refererUrl = None):
737 if refererUrl is not None:
738 headers["Referer"] = refererUrl
740 encodedData = urllib.urlencode(data) if data is not None else None
743 page = self._browser.download(url, encodedData, None, headers)
744 except urllib2.URLError, e:
745 _moduleLogger.error("Translating error: %s" % str(e))
746 raise NetworkError("%s is not accesible" % url)
750 def _get_page_with_token(self, url, data = None, refererUrl = None):
753 data['_rnr_se'] = self._token
755 page = self._get_page(url, data, refererUrl)
759 def _parse_with_validation(self, page):
760 json = parse_json(page)
761 validate_response(json)
765 def google_strptime(time):
767 Hack: Google always returns the time in the same locale. Sadly if the
768 local system's locale is different, there isn't a way to perfectly handle
769 the time. So instead we handle implement some time formatting
771 abbrevTime = time[:-3]
772 parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
774 parsedTime += datetime.timedelta(hours=12)
778 def itergroup(iterator, count, padValue = None):
780 Iterate in groups of 'count' values. If there
781 aren't enough values, the last result is padded with
784 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
788 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
792 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
797 >>> for val in itergroup("123456", 3):
801 >>> for val in itergroup("123456", 3):
802 ... print repr("".join(val))
806 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
807 nIterators = (paddedIterator, ) * count
808 return itertools.izip(*nIterators)
812 _TRUE_REGEX = re.compile("true")
813 _FALSE_REGEX = re.compile("false")
814 s = _TRUE_REGEX.sub("True", s)
815 s = _FALSE_REGEX.sub("False", s)
816 return eval(s, {}, {})
819 def _fake_parse_json(flattened):
820 return safe_eval(flattened)
823 def _actual_parse_json(flattened):
824 return simplejson.loads(flattened)
827 if simplejson is None:
828 parse_json = _fake_parse_json
830 parse_json = _actual_parse_json
833 def extract_payload(flatXml):
834 xmlTree = ElementTree.fromstring(flatXml)
836 jsonElement = xmlTree.getchildren()[0]
837 flatJson = jsonElement.text
838 jsonTree = parse_json(flatJson)
840 htmlElement = xmlTree.getchildren()[1]
841 flatHtml = htmlElement.text
843 return jsonTree, flatHtml
846 def validate_response(response):
848 Validates that the JSON response is A-OK
851 assert 'ok' in response and response['ok']
852 except AssertionError:
853 raise RuntimeError('There was a problem with GV: %s' % response)
856 def guess_phone_type(number):
857 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
858 return GVoiceBackend.PHONE_TYPE_GIZMO
860 return GVoiceBackend.PHONE_TYPE_MOBILE
863 def get_sane_callback(backend):
865 Try to set a sane default callback number on these preferences
866 1) 1747 numbers ( Gizmo )
867 2) anything with gizmo in the name
868 3) anything with computer in the name
871 numbers = backend.get_callback_numbers()
873 priorityOrderedCriteria = [
883 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
885 descriptionMatcher = None
886 if numberCriteria is not None:
887 numberMatcher = re.compile(numberCriteria)
888 elif descriptionCriteria is not None:
889 descriptionMatcher = re.compile(descriptionCriteria, re.I)
891 for number, description in numbers.iteritems():
892 if numberMatcher is not None and numberMatcher.match(number) is None:
894 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
899 def set_sane_callback(backend):
901 Try to set a sane default callback number on these preferences
902 1) 1747 numbers ( Gizmo )
903 2) anything with gizmo in the name
904 3) anything with computer in the name
907 number = get_sane_callback(backend)
908 backend.set_callback_number(number)
911 def _is_not_special(name):
912 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
916 members = inspect.getmembers(obj)
917 return dict((name, value) for (name, value) in members if _is_not_special(name))
920 def grab_debug_info(username, password):
921 cookieFile = os.path.join(".", "raw_cookies.txt")
923 os.remove(cookieFile)
927 backend = GVoiceBackend(cookieFile)
928 browser = backend._browser
931 ("forward", backend._forwardURL),
932 ("token", backend._tokenURL),
933 ("login", backend._loginURL),
934 ("isdnd", backend._isDndURL),
935 ("account", backend._XML_ACCOUNT_URL),
936 ("contacts", backend._XML_CONTACTS_URL),
938 ("voicemail", backend._XML_VOICEMAIL_URL),
939 ("sms", backend._XML_SMS_URL),
941 ("recent", backend._XML_RECENT_URL),
942 ("placed", backend._XML_PLACED_URL),
943 ("recieved", backend._XML_RECEIVED_URL),
944 ("missed", backend._XML_MISSED_URL),
948 print "Grabbing pre-login pages"
949 for name, url in _TEST_WEBPAGES:
951 page = browser.download(url)
952 except StandardError, e:
955 print "\tWriting to file"
956 with open("not_loggedin_%s.txt" % name, "w") as f:
960 print "Attempting login"
961 galxToken = backend._get_token()
962 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
963 with open("loggingin.txt", "w") as f:
964 print "\tWriting to file"
965 f.write(loginSuccessOrFailurePage)
967 backend._grab_account_info(loginSuccessOrFailurePage)
969 # Retry in case the redirect failed
970 # luckily is_authed does everything we need for a retry
971 loggedIn = backend.is_authed(True)
976 print "Grabbing post-login pages"
977 for name, url in _TEST_WEBPAGES:
979 page = browser.download(url)
980 except StandardError, e:
983 print "\tWriting to file"
984 with open("loggedin_%s.txt" % name, "w") as f:
988 browser.save_cookies()
989 print "\tWriting cookies to file"
990 with open("cookies.txt", "w") as f:
992 "%s: %s\n" % (c.name, c.value)
993 for c in browser._cookies
999 logging.basicConfig(level=logging.DEBUG)
1005 grab_debug_info(username, password)
1008 if __name__ == "__main__":