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.sax import saxutils
41 from xml.etree import ElementTree
44 import simplejson as _simplejson
45 simplejson = _simplejson
52 _moduleLogger = logging.getLogger(__name__)
55 class NetworkError(RuntimeError):
59 class MessageText(object):
62 ACCURACY_MEDIUM = "med2"
63 ACCURACY_HIGH = "high"
75 def __eq__(self, other):
76 return self.accuracy == other.accuracy and self.text == other.text
79 class Message(object):
87 return "%s (%s): %s" % (
90 "".join(str(part) for part in self.body)
94 selfDict = to_dict(self)
95 selfDict["body"] = [text.to_dict() for text in self.body] if self.body is not None else None
98 def __eq__(self, other):
99 return self.whoFrom == other.whoFrom and self.when == other.when and self.body == other.body
102 class Conversation(object):
104 TYPE_VOICEMAIL = "Voicemail"
110 self.contactId = None
113 self.prettyNumber = None
122 self.isArchived = None
124 def __cmp__(self, other):
125 cmpValue = cmp(self.contactId, other.contactId)
129 cmpValue = cmp(self.time, other.time)
133 cmpValue = cmp(self.id, other.id)
138 selfDict = to_dict(self)
139 selfDict["messages"] = [message.to_dict() for message in self.messages] if self.messages is not None else None
143 class GVoiceBackend(object):
145 This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
146 the functions include login, setting up a callback number, and initalting a callback
150 PHONE_TYPE_MOBILE = 2
154 def __init__(self, cookieFile = None):
155 # Important items in this function are the setup of the browser emulation and cookie file
156 self._browser = browser_emu.MozillaEmulator(1)
157 self._loadedFromCookies = self._browser.load_cookies(cookieFile)
160 self._accountNum = ""
161 self._lastAuthed = 0.0
162 self._callbackNumber = ""
163 self._callbackNumbers = {}
165 # Suprisingly, moving all of these from class to self sped up startup time
167 self._validateRe = re.compile("^\+?[0-9]{10,}$")
169 self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
171 SECURE_URL_BASE = "https://www.google.com/voice/"
172 SECURE_MOBILE_URL_BASE = SECURE_URL_BASE + "mobile/"
173 self._forwardURL = SECURE_MOBILE_URL_BASE + "phones"
174 self._tokenURL = SECURE_URL_BASE + "m"
175 self._callUrl = SECURE_URL_BASE + "call/connect"
176 self._callCancelURL = SECURE_URL_BASE + "call/cancel"
177 self._sendSmsURL = SECURE_URL_BASE + "sms/send"
179 self._isDndURL = "https://www.google.com/voice/m/donotdisturb"
180 self._isDndRe = re.compile(r"""<input.*?id="doNotDisturb".*?checked="(.*?)"\s*/>""")
181 self._setDndURL = "https://www.google.com/voice/m/savednd"
183 self._downloadVoicemailURL = SECURE_URL_BASE + "media/send_voicemail/"
184 self._markAsReadURL = SECURE_URL_BASE + "m/mark"
185 self._archiveMessageURL = SECURE_URL_BASE + "m/archive"
187 self._XML_SEARCH_URL = SECURE_URL_BASE + "inbox/search/"
188 self._XML_ACCOUNT_URL = SECURE_URL_BASE + "contacts/"
189 # HACK really this redirects to the main pge and we are grabbing some javascript
190 self._XML_CONTACTS_URL = "http://www.google.com/voice/inbox/search/contact"
191 self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
194 'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms',
195 'recorded', 'placed', 'received', 'missed'
197 self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox"
198 self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred"
199 self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all"
200 self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam"
201 self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash"
202 self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/"
203 self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/"
204 self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/"
205 self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/"
206 self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
207 self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
209 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
210 self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
211 self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
212 self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
214 self._contactsBodyRe = re.compile(r"""gcData\s*=\s*({.*?});""", re.MULTILINE | re.DOTALL)
215 self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
216 self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
217 self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
218 self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
219 self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
220 self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
221 self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
222 self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
223 self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
224 self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
225 self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
226 self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
228 def is_quick_login_possible(self):
230 @returns True then is_authed might be enough to login, else full login is required
232 return self._loadedFromCookies or 0.0 < self._lastAuthed
234 def is_authed(self, force = False):
236 Attempts to detect a current session
237 @note Once logged in try not to reauth more than once a minute.
238 @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
288 galxToken = self._get_token()
289 loginSuccessOrFailurePage = self._login(username, password, galxToken)
292 self._grab_account_info(loginSuccessOrFailurePage)
294 # Retry in case the redirect failed
295 # luckily is_authed does everything we need for a retry
296 loggedIn = self.is_authed(True)
298 _moduleLogger.exception(str(e))
300 _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
302 self._browser.save_cookies()
303 self._lastAuthed = time.time()
307 self._browser.save_cookies()
310 self._browser.clear_cookies()
311 self._browser.save_cookies()
313 self._lastAuthed = 0.0
316 isDndPage = self._get_page(self._isDndURL)
318 dndGroup = self._isDndRe.search(isDndPage)
321 dndStatus = dndGroup.group(1)
322 isDnd = True if dndStatus.strip().lower() == "true" else False
325 def set_dnd(self, doNotDisturb):
327 "doNotDisturb": 1 if doNotDisturb else 0,
330 dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
332 def call(self, outgoingNumber):
334 This is the main function responsible for initating the callback
336 outgoingNumber = self._send_validation(outgoingNumber)
337 subscriberNumber = None
338 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
341 'outgoingNumber': outgoingNumber,
342 'forwardingNumber': self._callbackNumber,
343 'subscriberNumber': subscriberNumber or 'undefined',
344 'phoneType': str(phoneType),
347 _moduleLogger.info("%r" % callData)
349 page = self._get_page_with_token(
353 self._parse_with_validation(page)
356 def cancel(self, outgoingNumber=None):
358 Cancels a call matching outgoing and forwarding numbers (if given).
359 Will raise an error if no matching call is being placed
361 page = self._get_page_with_token(
364 'outgoingNumber': outgoingNumber or 'undefined',
365 'forwardingNumber': self._callbackNumber or 'undefined',
369 self._parse_with_validation(page)
371 def send_sms(self, phoneNumbers, message):
372 validatedPhoneNumbers = [
373 self._send_validation(phoneNumber)
374 for phoneNumber in phoneNumbers
376 flattenedPhoneNumbers = ",".join(validatedPhoneNumbers)
377 page = self._get_page_with_token(
380 'phoneNumber': flattenedPhoneNumbers,
384 self._parse_with_validation(page)
386 def search(self, query):
388 Search your Google Voice Account history for calls, voicemails, and sms
389 Returns ``Folder`` instance containting matching messages
391 page = self._get_page(
392 self._XML_SEARCH_URL,
395 json, html = extract_payload(page)
398 def get_feed(self, feed):
399 actualFeed = "_XML_%s_URL" % feed.upper()
400 feedUrl = getattr(self, actualFeed)
402 page = self._get_page(feedUrl)
403 json, html = extract_payload(page)
407 def download(self, messageId, adir):
409 Download a voicemail or recorded call MP3 matching the given ``msg``
410 which can either be a ``Message`` instance, or a SHA1 identifier.
411 Saves files to ``adir`` (defaults to current directory).
412 Message hashes can be found in ``self.voicemail().messages`` for example.
413 Returns location of saved file.
415 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
416 fn = os.path.join(adir, '%s.mp3' % messageId)
417 with open(fn, 'wb') as fo:
421 def is_valid_syntax(self, number):
423 @returns If This number be called ( syntax validation only )
425 return self._validateRe.match(number) is not None
427 def get_account_number(self):
429 @returns The GoogleVoice phone number
431 return self._accountNum
433 def get_callback_numbers(self):
435 @returns a dictionary mapping call back numbers to descriptions
436 @note These results are cached for 30 minutes.
438 if not self.is_authed():
440 return self._callbackNumbers
442 def set_callback_number(self, callbacknumber):
444 Set the number that GoogleVoice calls
445 @param callbacknumber should be a proper 10 digit number
447 self._callbackNumber = callbacknumber
448 _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
451 def get_callback_number(self):
453 @returns Current callback number or None
455 return self._callbackNumber
457 def get_recent(self):
459 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
462 ("Received", self._XML_RECEIVED_URL),
463 ("Missed", self._XML_MISSED_URL),
464 ("Placed", self._XML_PLACED_URL),
466 flatXml = self._get_page(url)
468 allRecentHtml = self._grab_html(flatXml)
469 allRecentData = self._parse_history(allRecentHtml)
470 for recentCallData in allRecentData:
471 recentCallData["action"] = action
474 def get_contacts(self):
476 @returns Iterable of (contact id, contact name)
478 page = self._get_page(self._XML_CONTACTS_URL)
479 contactsBody = self._contactsBodyRe.search(page)
480 if contactsBody is None:
481 raise RuntimeError("Could not extract contact information")
482 accountData = _fake_parse_json(contactsBody.group(1))
483 for contactId, contactDetails in accountData["contacts"].iteritems():
484 # A zero contact id is the catch all for unknown contacts
486 if "name" in contactDetails:
487 contactDetails["name"] = unescape(contactDetails["name"])
488 yield contactId, contactDetails
490 def get_voicemails(self):
491 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
492 voicemailHtml = self._grab_html(voicemailPage)
493 voicemailJson = self._grab_json(voicemailPage)
494 parsedVoicemail = self._parse_voicemail(voicemailHtml)
495 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
499 smsPage = self._get_page(self._XML_SMS_URL)
500 smsHtml = self._grab_html(smsPage)
501 smsJson = self._grab_json(smsPage)
502 parsedSms = self._parse_sms(smsHtml)
503 smss = self._merge_conversation_sources(parsedSms, smsJson)
506 def mark_message(self, messageId, asRead):
508 "read": 1 if asRead else 0,
512 markPage = self._get_page(self._markAsReadURL, postData)
514 def archive_message(self, messageId):
519 markPage = self._get_page(self._archiveMessageURL, postData)
521 def _grab_json(self, flatXml):
522 xmlTree = ElementTree.fromstring(flatXml)
523 jsonElement = xmlTree.getchildren()[0]
524 flatJson = jsonElement.text
525 jsonTree = parse_json(flatJson)
528 def _grab_html(self, flatXml):
529 xmlTree = ElementTree.fromstring(flatXml)
530 htmlElement = xmlTree.getchildren()[1]
531 flatHtml = htmlElement.text
534 def _grab_account_info(self, page):
535 tokenGroup = self._tokenRe.search(page)
536 if tokenGroup is None:
537 raise RuntimeError("Could not extract authentication token from GoogleVoice")
538 self._token = tokenGroup.group(1)
540 anGroup = self._accountNumRe.search(page)
541 if anGroup is not None:
542 self._accountNum = anGroup.group(1)
544 _moduleLogger.debug("Could not extract account number from GoogleVoice")
546 self._callbackNumbers = {}
547 for match in self._callbackRe.finditer(page):
548 callbackNumber = match.group(2)
549 callbackName = match.group(1)
550 self._callbackNumbers[callbackNumber] = callbackName
551 if len(self._callbackNumbers) == 0:
552 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
554 def _send_validation(self, number):
555 if not self.is_valid_syntax(number):
556 raise ValueError('Number is not valid: "%s"' % number)
557 elif not self.is_authed():
558 raise RuntimeError("Not Authenticated")
561 def _parse_history(self, historyHtml):
562 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
563 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
564 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
565 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
566 exactTime = google_strptime(exactTime)
567 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
568 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
569 locationGroup = self._voicemailLocationRegex.search(messageHtml)
570 location = locationGroup.group(1).strip() if locationGroup else ""
572 nameGroup = self._voicemailNameRegex.search(messageHtml)
573 name = nameGroup.group(1).strip() if nameGroup else ""
574 numberGroup = self._voicemailNumberRegex.search(messageHtml)
575 number = numberGroup.group(1).strip() if numberGroup else ""
576 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
577 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
578 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
579 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
582 "id": messageId.strip(),
583 "contactId": contactId,
584 "name": unescape(name),
586 "relTime": relativeTime,
587 "prettyNumber": prettyNumber,
589 "location": unescape(location),
593 def _interpret_voicemail_regex(group):
594 quality, content, number = group.group(2), group.group(3), group.group(4)
596 if quality is not None and content is not None:
597 text.accuracy = quality
598 text.text = unescape(content)
600 elif number is not None:
601 text.accuracy = MessageText.ACCURACY_HIGH
605 def _parse_voicemail(self, voicemailHtml):
606 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
607 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
608 conv = Conversation()
609 conv.type = Conversation.TYPE_VOICEMAIL
610 conv.id = messageId.strip()
612 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
613 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
614 conv.time = google_strptime(exactTimeText)
615 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
616 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
617 locationGroup = self._voicemailLocationRegex.search(messageHtml)
618 conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "")
620 nameGroup = self._voicemailNameRegex.search(messageHtml)
621 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
622 numberGroup = self._voicemailNumberRegex.search(messageHtml)
623 conv.number = numberGroup.group(1).strip() if numberGroup else ""
624 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
625 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
626 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
627 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
629 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
631 self._interpret_voicemail_regex(group)
632 for group in messageGroups
633 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
635 message.body = messageParts
636 message.whoFrom = conv.name
637 message.when = conv.time.strftime("%I:%M %p")
638 conv.messages = (message, )
643 def _interpret_sms_message_parts(fromPart, textPart, timePart):
645 text.accuracy = MessageText.ACCURACY_MEDIUM
646 text.text = unescape(textPart)
649 message.body = (text, )
650 message.whoFrom = fromPart
651 message.when = timePart
655 def _parse_sms(self, smsHtml):
656 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
657 for messageId, messageHtml in itergroup(splitSms[1:], 2):
658 conv = Conversation()
659 conv.type = Conversation.TYPE_SMS
660 conv.id = messageId.strip()
662 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
663 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
664 conv.time = google_strptime(exactTimeText)
665 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
666 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
669 nameGroup = self._voicemailNameRegex.search(messageHtml)
670 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
671 numberGroup = self._voicemailNumberRegex.search(messageHtml)
672 conv.number = numberGroup.group(1).strip() if numberGroup else ""
673 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
674 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
675 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
676 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
678 fromGroups = self._smsFromRegex.finditer(messageHtml)
679 fromParts = (group.group(1).strip() for group in fromGroups)
680 textGroups = self._smsTextRegex.finditer(messageHtml)
681 textParts = (group.group(1).strip() for group in textGroups)
682 timeGroups = self._smsTimeRegex.finditer(messageHtml)
683 timeParts = (group.group(1).strip() for group in timeGroups)
685 messageParts = itertools.izip(fromParts, textParts, timeParts)
686 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
687 conv.messages = messages
692 def _merge_conversation_sources(parsedMessages, json):
693 for message in parsedMessages:
694 jsonItem = json["messages"][message.id]
695 message.isRead = jsonItem["isRead"]
696 message.isSpam = jsonItem["isSpam"]
697 message.isTrash = jsonItem["isTrash"]
698 message.isArchived = "inbox" not in jsonItem["labels"]
701 def _get_page(self, url, data = None, refererUrl = None):
703 if refererUrl is not None:
704 headers["Referer"] = refererUrl
706 encodedData = urllib.urlencode(data) if data is not None else None
709 page = self._browser.download(url, encodedData, None, headers)
710 except urllib2.URLError, e:
711 _moduleLogger.error("Translating error: %s" % str(e))
712 raise NetworkError("%s is not accesible" % url)
716 def _get_page_with_token(self, url, data = None, refererUrl = None):
719 data['_rnr_se'] = self._token
721 page = self._get_page(url, data, refererUrl)
725 def _parse_with_validation(self, page):
726 json = parse_json(page)
727 validate_response(json)
731 _UNESCAPE_ENTITIES = {
739 plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
743 def google_strptime(time):
745 Hack: Google always returns the time in the same locale. Sadly if the
746 local system's locale is different, there isn't a way to perfectly handle
747 the time. So instead we handle implement some time formatting
749 abbrevTime = time[:-3]
750 parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
752 parsedTime += datetime.timedelta(hours=12)
756 def itergroup(iterator, count, padValue = None):
758 Iterate in groups of 'count' values. If there
759 aren't enough values, the last result is padded with
762 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
766 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
770 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
775 >>> for val in itergroup("123456", 3):
779 >>> for val in itergroup("123456", 3):
780 ... print repr("".join(val))
784 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
785 nIterators = (paddedIterator, ) * count
786 return itertools.izip(*nIterators)
790 _TRUE_REGEX = re.compile("true")
791 _FALSE_REGEX = re.compile("false")
792 s = _TRUE_REGEX.sub("True", s)
793 s = _FALSE_REGEX.sub("False", s)
794 return eval(s, {}, {})
797 def _fake_parse_json(flattened):
798 return safe_eval(flattened)
801 def _actual_parse_json(flattened):
802 return simplejson.loads(flattened)
805 if simplejson is None:
806 parse_json = _fake_parse_json
808 parse_json = _actual_parse_json
811 def extract_payload(flatXml):
812 xmlTree = ElementTree.fromstring(flatXml)
814 jsonElement = xmlTree.getchildren()[0]
815 flatJson = jsonElement.text
816 jsonTree = parse_json(flatJson)
818 htmlElement = xmlTree.getchildren()[1]
819 flatHtml = htmlElement.text
821 return jsonTree, flatHtml
824 def validate_response(response):
826 Validates that the JSON response is A-OK
829 assert 'ok' in response and response['ok']
830 except AssertionError:
831 raise RuntimeError('There was a problem with GV: %s' % response)
834 def guess_phone_type(number):
835 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
836 return GVoiceBackend.PHONE_TYPE_GIZMO
838 return GVoiceBackend.PHONE_TYPE_MOBILE
841 def get_sane_callback(backend):
843 Try to set a sane default callback number on these preferences
844 1) 1747 numbers ( Gizmo )
845 2) anything with gizmo in the name
846 3) anything with computer in the name
849 numbers = backend.get_callback_numbers()
851 priorityOrderedCriteria = [
861 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
863 descriptionMatcher = None
864 if numberCriteria is not None:
865 numberMatcher = re.compile(numberCriteria)
866 elif descriptionCriteria is not None:
867 descriptionMatcher = re.compile(descriptionCriteria, re.I)
869 for number, description in numbers.iteritems():
870 if numberMatcher is not None and numberMatcher.match(number) is None:
872 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
877 def set_sane_callback(backend):
879 Try to set a sane default callback number on these preferences
880 1) 1747 numbers ( Gizmo )
881 2) anything with gizmo in the name
882 3) anything with computer in the name
885 number = get_sane_callback(backend)
886 backend.set_callback_number(number)
889 def _is_not_special(name):
890 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
894 members = inspect.getmembers(obj)
895 return dict((name, value) for (name, value) in members if _is_not_special(name))
898 def grab_debug_info(username, password):
899 cookieFile = os.path.join(".", "raw_cookies.txt")
901 os.remove(cookieFile)
905 backend = GVoiceBackend(cookieFile)
906 browser = backend._browser
909 ("forward", backend._forwardURL),
910 ("token", backend._tokenURL),
911 ("login", backend._loginURL),
912 ("isdnd", backend._isDndURL),
913 ("account", backend._XML_ACCOUNT_URL),
914 ("contacts", backend._XML_CONTACTS_URL),
916 ("voicemail", backend._XML_VOICEMAIL_URL),
917 ("sms", backend._XML_SMS_URL),
919 ("recent", backend._XML_RECENT_URL),
920 ("placed", backend._XML_PLACED_URL),
921 ("recieved", backend._XML_RECEIVED_URL),
922 ("missed", backend._XML_MISSED_URL),
926 print "Grabbing pre-login pages"
927 for name, url in _TEST_WEBPAGES:
929 page = browser.download(url)
930 except StandardError, e:
933 print "\tWriting to file"
934 with open("not_loggedin_%s.txt" % name, "w") as f:
938 print "Attempting login"
939 galxToken = backend._get_token()
940 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
941 with open("loggingin.txt", "w") as f:
942 print "\tWriting to file"
943 f.write(loginSuccessOrFailurePage)
945 backend._grab_account_info(loginSuccessOrFailurePage)
947 # Retry in case the redirect failed
948 # luckily is_authed does everything we need for a retry
949 loggedIn = backend.is_authed(True)
954 print "Grabbing post-login pages"
955 for name, url in _TEST_WEBPAGES:
957 page = browser.download(url)
958 except StandardError, e:
961 print "\tWriting to file"
962 with open("loggedin_%s.txt" % name, "w") as f:
966 browser.save_cookies()
967 print "\tWriting cookies to file"
968 with open("cookies.txt", "w") as f:
970 "%s: %s\n" % (c.name, c.value)
971 for c in browser._cookies
977 logging.basicConfig(level=logging.DEBUG)
983 grab_debug_info(username, password)
986 if __name__ == "__main__":