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.clear_cookies()
308 self._browser.save_cookies()
310 self._lastAuthed = 0.0
313 isDndPage = self._get_page(self._isDndURL)
315 dndGroup = self._isDndRe.search(isDndPage)
318 dndStatus = dndGroup.group(1)
319 isDnd = True if dndStatus.strip().lower() == "true" else False
322 def set_dnd(self, doNotDisturb):
324 "doNotDisturb": 1 if doNotDisturb else 0,
327 dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
329 def call(self, outgoingNumber):
331 This is the main function responsible for initating the callback
333 outgoingNumber = self._send_validation(outgoingNumber)
334 subscriberNumber = None
335 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
338 'outgoingNumber': outgoingNumber,
339 'forwardingNumber': self._callbackNumber,
340 'subscriberNumber': subscriberNumber or 'undefined',
341 'phoneType': str(phoneType),
344 _moduleLogger.info("%r" % callData)
346 page = self._get_page_with_token(
350 self._parse_with_validation(page)
353 def cancel(self, outgoingNumber=None):
355 Cancels a call matching outgoing and forwarding numbers (if given).
356 Will raise an error if no matching call is being placed
358 page = self._get_page_with_token(
361 'outgoingNumber': outgoingNumber or 'undefined',
362 'forwardingNumber': self._callbackNumber or 'undefined',
366 self._parse_with_validation(page)
368 def send_sms(self, phoneNumbers, message):
369 validatedPhoneNumbers = [
370 self._send_validation(phoneNumber)
371 for phoneNumber in phoneNumbers
373 flattenedPhoneNumbers = ",".join(validatedPhoneNumbers)
374 page = self._get_page_with_token(
377 'phoneNumber': flattenedPhoneNumbers,
381 self._parse_with_validation(page)
383 def search(self, query):
385 Search your Google Voice Account history for calls, voicemails, and sms
386 Returns ``Folder`` instance containting matching messages
388 page = self._get_page(
389 self._XML_SEARCH_URL,
392 json, html = extract_payload(page)
395 def get_feed(self, feed):
396 actualFeed = "_XML_%s_URL" % feed.upper()
397 feedUrl = getattr(self, actualFeed)
399 page = self._get_page(feedUrl)
400 json, html = extract_payload(page)
404 def download(self, messageId, adir):
406 Download a voicemail or recorded call MP3 matching the given ``msg``
407 which can either be a ``Message`` instance, or a SHA1 identifier.
408 Saves files to ``adir`` (defaults to current directory).
409 Message hashes can be found in ``self.voicemail().messages`` for example.
410 Returns location of saved file.
412 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
413 fn = os.path.join(adir, '%s.mp3' % messageId)
414 with open(fn, 'wb') as fo:
418 def is_valid_syntax(self, number):
420 @returns If This number be called ( syntax validation only )
422 return self._validateRe.match(number) is not None
424 def get_account_number(self):
426 @returns The GoogleVoice phone number
428 return self._accountNum
430 def get_callback_numbers(self):
432 @returns a dictionary mapping call back numbers to descriptions
433 @note These results are cached for 30 minutes.
435 if not self.is_authed():
437 return self._callbackNumbers
439 def set_callback_number(self, callbacknumber):
441 Set the number that GoogleVoice calls
442 @param callbacknumber should be a proper 10 digit number
444 self._callbackNumber = callbacknumber
445 _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
448 def get_callback_number(self):
450 @returns Current callback number or None
452 return self._callbackNumber
454 def get_recent(self):
456 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
459 ("Received", self._XML_RECEIVED_URL),
460 ("Missed", self._XML_MISSED_URL),
461 ("Placed", self._XML_PLACED_URL),
463 flatXml = self._get_page(url)
465 allRecentHtml = self._grab_html(flatXml)
466 allRecentData = self._parse_history(allRecentHtml)
467 for recentCallData in allRecentData:
468 recentCallData["action"] = action
471 def get_contacts(self):
473 @returns Iterable of (contact id, contact name)
475 page = self._get_page(self._XML_CONTACTS_URL)
476 contactsBody = self._contactsBodyRe.search(page)
477 if contactsBody is None:
478 raise RuntimeError("Could not extract contact information")
479 accountData = _fake_parse_json(contactsBody.group(1))
480 if accountData is None:
482 for contactId, contactDetails in accountData["contacts"].iteritems():
483 # A zero contact id is the catch all for unknown contacts
485 if "name" in contactDetails:
486 contactDetails["name"] = unescape(contactDetails["name"])
487 yield contactId, contactDetails
489 def get_voicemails(self):
490 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
491 voicemailHtml = self._grab_html(voicemailPage)
492 voicemailJson = self._grab_json(voicemailPage)
493 if voicemailJson is None:
495 parsedVoicemail = self._parse_voicemail(voicemailHtml)
496 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
500 smsPage = self._get_page(self._XML_SMS_URL)
501 smsHtml = self._grab_html(smsPage)
502 smsJson = self._grab_json(smsPage)
505 parsedSms = self._parse_sms(smsHtml)
506 smss = self._merge_conversation_sources(parsedSms, smsJson)
509 def mark_message(self, messageId, asRead):
511 "read": 1 if asRead else 0,
515 markPage = self._get_page(self._markAsReadURL, postData)
517 def archive_message(self, messageId):
522 markPage = self._get_page(self._archiveMessageURL, postData)
524 def _grab_json(self, flatXml):
525 xmlTree = ElementTree.fromstring(flatXml)
526 jsonElement = xmlTree.getchildren()[0]
527 flatJson = jsonElement.text
528 jsonTree = parse_json(flatJson)
531 def _grab_html(self, flatXml):
532 xmlTree = ElementTree.fromstring(flatXml)
533 htmlElement = xmlTree.getchildren()[1]
534 flatHtml = htmlElement.text
537 def _grab_account_info(self, page):
538 tokenGroup = self._tokenRe.search(page)
539 if tokenGroup is None:
540 raise RuntimeError("Could not extract authentication token from GoogleVoice")
541 self._token = tokenGroup.group(1)
543 anGroup = self._accountNumRe.search(page)
544 if anGroup is not None:
545 self._accountNum = anGroup.group(1)
547 _moduleLogger.debug("Could not extract account number from GoogleVoice")
549 self._callbackNumbers = {}
550 for match in self._callbackRe.finditer(page):
551 callbackNumber = match.group(2)
552 callbackName = match.group(1)
553 self._callbackNumbers[callbackNumber] = callbackName
554 if len(self._callbackNumbers) == 0:
555 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
557 def _send_validation(self, number):
558 if not self.is_valid_syntax(number):
559 raise ValueError('Number is not valid: "%s"' % number)
560 elif not self.is_authed():
561 raise RuntimeError("Not Authenticated")
564 def _parse_history(self, historyHtml):
565 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
566 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
567 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
568 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
569 exactTime = google_strptime(exactTime)
570 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
571 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
572 locationGroup = self._voicemailLocationRegex.search(messageHtml)
573 location = locationGroup.group(1).strip() if locationGroup else ""
575 nameGroup = self._voicemailNameRegex.search(messageHtml)
576 name = nameGroup.group(1).strip() if nameGroup else ""
577 numberGroup = self._voicemailNumberRegex.search(messageHtml)
578 number = numberGroup.group(1).strip() if numberGroup else ""
579 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
580 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
581 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
582 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
585 "id": messageId.strip(),
586 "contactId": contactId,
587 "name": unescape(name),
589 "relTime": relativeTime,
590 "prettyNumber": prettyNumber,
592 "location": unescape(location),
596 def _interpret_voicemail_regex(group):
597 quality, content, number = group.group(2), group.group(3), group.group(4)
599 if quality is not None and content is not None:
600 text.accuracy = quality
601 text.text = unescape(content)
603 elif number is not None:
604 text.accuracy = MessageText.ACCURACY_HIGH
608 def _parse_voicemail(self, voicemailHtml):
609 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
610 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
611 conv = Conversation()
612 conv.type = Conversation.TYPE_VOICEMAIL
613 conv.id = messageId.strip()
615 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
616 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
617 conv.time = google_strptime(exactTimeText)
618 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
619 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
620 locationGroup = self._voicemailLocationRegex.search(messageHtml)
621 conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "")
623 nameGroup = self._voicemailNameRegex.search(messageHtml)
624 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
625 numberGroup = self._voicemailNumberRegex.search(messageHtml)
626 conv.number = numberGroup.group(1).strip() if numberGroup else ""
627 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
628 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
629 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
630 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
632 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
634 self._interpret_voicemail_regex(group)
635 for group in messageGroups
636 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
638 message.body = messageParts
639 message.whoFrom = conv.name
640 message.when = conv.time.strftime("%I:%M %p")
641 conv.messages = (message, )
646 def _interpret_sms_message_parts(fromPart, textPart, timePart):
648 text.accuracy = MessageText.ACCURACY_MEDIUM
649 text.text = unescape(textPart)
652 message.body = (text, )
653 message.whoFrom = fromPart
654 message.when = timePart
658 def _parse_sms(self, smsHtml):
659 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
660 for messageId, messageHtml in itergroup(splitSms[1:], 2):
661 conv = Conversation()
662 conv.type = Conversation.TYPE_SMS
663 conv.id = messageId.strip()
665 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
666 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
667 conv.time = google_strptime(exactTimeText)
668 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
669 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
672 nameGroup = self._voicemailNameRegex.search(messageHtml)
673 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
674 numberGroup = self._voicemailNumberRegex.search(messageHtml)
675 conv.number = numberGroup.group(1).strip() if numberGroup else ""
676 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
677 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
678 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
679 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
681 fromGroups = self._smsFromRegex.finditer(messageHtml)
682 fromParts = (group.group(1).strip() for group in fromGroups)
683 textGroups = self._smsTextRegex.finditer(messageHtml)
684 textParts = (group.group(1).strip() for group in textGroups)
685 timeGroups = self._smsTimeRegex.finditer(messageHtml)
686 timeParts = (group.group(1).strip() for group in timeGroups)
688 messageParts = itertools.izip(fromParts, textParts, timeParts)
689 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
690 conv.messages = messages
695 def _merge_conversation_sources(parsedMessages, json):
696 for message in parsedMessages:
697 jsonItem = json["messages"][message.id]
698 message.isRead = jsonItem["isRead"]
699 message.isSpam = jsonItem["isSpam"]
700 message.isTrash = jsonItem["isTrash"]
701 message.isArchived = "inbox" not in jsonItem["labels"]
704 def _get_page(self, url, data = None, refererUrl = None):
706 if refererUrl is not None:
707 headers["Referer"] = refererUrl
709 encodedData = urllib.urlencode(data) if data is not None else None
712 page = self._browser.download(url, encodedData, None, headers)
713 except urllib2.URLError, e:
714 _moduleLogger.error("Translating error: %s" % str(e))
715 raise NetworkError("%s is not accesible" % url)
719 def _get_page_with_token(self, url, data = None, refererUrl = None):
722 data['_rnr_se'] = self._token
724 page = self._get_page(url, data, refererUrl)
728 def _parse_with_validation(self, page):
729 json = parse_json(page)
730 validate_response(json)
734 _UNESCAPE_ENTITIES = {
742 plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
746 def google_strptime(time):
748 Hack: Google always returns the time in the same locale. Sadly if the
749 local system's locale is different, there isn't a way to perfectly handle
750 the time. So instead we handle implement some time formatting
752 abbrevTime = time[:-3]
753 parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
755 parsedTime += datetime.timedelta(hours=12)
759 def itergroup(iterator, count, padValue = None):
761 Iterate in groups of 'count' values. If there
762 aren't enough values, the last result is padded with
765 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
769 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
773 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
778 >>> for val in itergroup("123456", 3):
782 >>> for val in itergroup("123456", 3):
783 ... print repr("".join(val))
787 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
788 nIterators = (paddedIterator, ) * count
789 return itertools.izip(*nIterators)
793 _TRUE_REGEX = re.compile("true")
794 _FALSE_REGEX = re.compile("false")
795 s = _TRUE_REGEX.sub("True", s)
796 s = _FALSE_REGEX.sub("False", s)
798 results = eval(s, {}, {})
800 _moduleLogger.exception("Oops")
804 def _fake_parse_json(flattened):
805 return safe_eval(flattened)
808 def _actual_parse_json(flattened):
809 return simplejson.loads(flattened)
812 if simplejson is None:
813 parse_json = _fake_parse_json
815 parse_json = _actual_parse_json
818 def extract_payload(flatXml):
819 xmlTree = ElementTree.fromstring(flatXml)
821 jsonElement = xmlTree.getchildren()[0]
822 flatJson = jsonElement.text
823 jsonTree = parse_json(flatJson)
825 htmlElement = xmlTree.getchildren()[1]
826 flatHtml = htmlElement.text
828 return jsonTree, flatHtml
831 def validate_response(response):
833 Validates that the JSON response is A-OK
836 assert response is not None
837 assert 'ok' in response
838 assert response['ok']
839 except AssertionError:
840 raise RuntimeError('There was a problem with GV: %s' % response)
843 def guess_phone_type(number):
844 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
845 return GVoiceBackend.PHONE_TYPE_GIZMO
847 return GVoiceBackend.PHONE_TYPE_MOBILE
850 def get_sane_callback(backend):
852 Try to set a sane default callback number on these preferences
853 1) 1747 numbers ( Gizmo )
854 2) anything with gizmo in the name
855 3) anything with computer in the name
858 numbers = backend.get_callback_numbers()
860 priorityOrderedCriteria = [
870 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
872 descriptionMatcher = None
873 if numberCriteria is not None:
874 numberMatcher = re.compile(numberCriteria)
875 elif descriptionCriteria is not None:
876 descriptionMatcher = re.compile(descriptionCriteria, re.I)
878 for number, description in numbers.iteritems():
879 if numberMatcher is not None and numberMatcher.match(number) is None:
881 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
886 def set_sane_callback(backend):
888 Try to set a sane default callback number on these preferences
889 1) 1747 numbers ( Gizmo )
890 2) anything with gizmo in the name
891 3) anything with computer in the name
894 number = get_sane_callback(backend)
895 backend.set_callback_number(number)
898 def _is_not_special(name):
899 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
903 members = inspect.getmembers(obj)
904 return dict((name, value) for (name, value) in members if _is_not_special(name))
907 def grab_debug_info(username, password):
908 cookieFile = os.path.join(".", "raw_cookies.txt")
910 os.remove(cookieFile)
914 backend = GVoiceBackend(cookieFile)
915 browser = backend._browser
918 ("forward", backend._forwardURL),
919 ("token", backend._tokenURL),
920 ("login", backend._loginURL),
921 ("isdnd", backend._isDndURL),
922 ("account", backend._XML_ACCOUNT_URL),
923 ("contacts", backend._XML_CONTACTS_URL),
925 ("voicemail", backend._XML_VOICEMAIL_URL),
926 ("sms", backend._XML_SMS_URL),
928 ("recent", backend._XML_RECENT_URL),
929 ("placed", backend._XML_PLACED_URL),
930 ("recieved", backend._XML_RECEIVED_URL),
931 ("missed", backend._XML_MISSED_URL),
935 print "Grabbing pre-login pages"
936 for name, url in _TEST_WEBPAGES:
938 page = browser.download(url)
939 except StandardError, e:
942 print "\tWriting to file"
943 with open("not_loggedin_%s.txt" % name, "w") as f:
947 print "Attempting login"
948 galxToken = backend._get_token()
949 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
950 with open("loggingin.txt", "w") as f:
951 print "\tWriting to file"
952 f.write(loginSuccessOrFailurePage)
954 backend._grab_account_info(loginSuccessOrFailurePage)
956 # Retry in case the redirect failed
957 # luckily is_authed does everything we need for a retry
958 loggedIn = backend.is_authed(True)
963 print "Grabbing post-login pages"
964 for name, url in _TEST_WEBPAGES:
966 page = browser.download(url)
967 except StandardError, e:
970 print "\tWriting to file"
971 with open("loggedin_%s.txt" % name, "w") as f:
975 browser.save_cookies()
976 print "\tWriting cookies to file"
977 with open("cookies.txt", "w") as f:
979 "%s: %s\n" % (c.name, c.value)
980 for c in browser._cookies
986 logging.basicConfig(level=logging.DEBUG)
992 grab_debug_info(username, password)
995 if __name__ == "__main__":