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 for contactId, contactDetails in accountData["contacts"].iteritems():
481 # A zero contact id is the catch all for unknown contacts
483 if "name" in contactDetails:
484 contactDetails["name"] = unescape(contactDetails["name"])
485 yield contactId, contactDetails
487 def get_voicemails(self):
488 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
489 voicemailHtml = self._grab_html(voicemailPage)
490 voicemailJson = self._grab_json(voicemailPage)
491 parsedVoicemail = self._parse_voicemail(voicemailHtml)
492 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
496 smsPage = self._get_page(self._XML_SMS_URL)
497 smsHtml = self._grab_html(smsPage)
498 smsJson = self._grab_json(smsPage)
499 parsedSms = self._parse_sms(smsHtml)
500 smss = self._merge_conversation_sources(parsedSms, smsJson)
503 def mark_message(self, messageId, asRead):
505 "read": 1 if asRead else 0,
509 markPage = self._get_page(self._markAsReadURL, postData)
511 def archive_message(self, messageId):
516 markPage = self._get_page(self._archiveMessageURL, postData)
518 def _grab_json(self, flatXml):
519 xmlTree = ElementTree.fromstring(flatXml)
520 jsonElement = xmlTree.getchildren()[0]
521 flatJson = jsonElement.text
522 jsonTree = parse_json(flatJson)
525 def _grab_html(self, flatXml):
526 xmlTree = ElementTree.fromstring(flatXml)
527 htmlElement = xmlTree.getchildren()[1]
528 flatHtml = htmlElement.text
531 def _grab_account_info(self, page):
532 tokenGroup = self._tokenRe.search(page)
533 if tokenGroup is None:
534 raise RuntimeError("Could not extract authentication token from GoogleVoice")
535 self._token = tokenGroup.group(1)
537 anGroup = self._accountNumRe.search(page)
538 if anGroup is not None:
539 self._accountNum = anGroup.group(1)
541 _moduleLogger.debug("Could not extract account number from GoogleVoice")
543 self._callbackNumbers = {}
544 for match in self._callbackRe.finditer(page):
545 callbackNumber = match.group(2)
546 callbackName = match.group(1)
547 self._callbackNumbers[callbackNumber] = callbackName
548 if len(self._callbackNumbers) == 0:
549 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
551 def _send_validation(self, number):
552 if not self.is_valid_syntax(number):
553 raise ValueError('Number is not valid: "%s"' % number)
554 elif not self.is_authed():
555 raise RuntimeError("Not Authenticated")
558 def _parse_history(self, historyHtml):
559 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
560 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
561 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
562 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
563 exactTime = google_strptime(exactTime)
564 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
565 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
566 locationGroup = self._voicemailLocationRegex.search(messageHtml)
567 location = locationGroup.group(1).strip() if locationGroup else ""
569 nameGroup = self._voicemailNameRegex.search(messageHtml)
570 name = nameGroup.group(1).strip() if nameGroup else ""
571 numberGroup = self._voicemailNumberRegex.search(messageHtml)
572 number = numberGroup.group(1).strip() if numberGroup else ""
573 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
574 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
575 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
576 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
579 "id": messageId.strip(),
580 "contactId": contactId,
581 "name": unescape(name),
583 "relTime": relativeTime,
584 "prettyNumber": prettyNumber,
586 "location": unescape(location),
590 def _interpret_voicemail_regex(group):
591 quality, content, number = group.group(2), group.group(3), group.group(4)
593 if quality is not None and content is not None:
594 text.accuracy = quality
595 text.text = unescape(content)
597 elif number is not None:
598 text.accuracy = MessageText.ACCURACY_HIGH
602 def _parse_voicemail(self, voicemailHtml):
603 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
604 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
605 conv = Conversation()
606 conv.type = Conversation.TYPE_VOICEMAIL
607 conv.id = messageId.strip()
609 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
610 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
611 conv.time = google_strptime(exactTimeText)
612 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
613 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
614 locationGroup = self._voicemailLocationRegex.search(messageHtml)
615 conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "")
617 nameGroup = self._voicemailNameRegex.search(messageHtml)
618 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
619 numberGroup = self._voicemailNumberRegex.search(messageHtml)
620 conv.number = numberGroup.group(1).strip() if numberGroup else ""
621 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
622 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
623 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
624 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
626 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
628 self._interpret_voicemail_regex(group)
629 for group in messageGroups
630 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
632 message.body = messageParts
633 message.whoFrom = conv.name
634 message.when = conv.time.strftime("%I:%M %p")
635 conv.messages = (message, )
640 def _interpret_sms_message_parts(fromPart, textPart, timePart):
642 text.accuracy = MessageText.ACCURACY_MEDIUM
643 text.text = unescape(textPart)
646 message.body = (text, )
647 message.whoFrom = fromPart
648 message.when = timePart
652 def _parse_sms(self, smsHtml):
653 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
654 for messageId, messageHtml in itergroup(splitSms[1:], 2):
655 conv = Conversation()
656 conv.type = Conversation.TYPE_SMS
657 conv.id = messageId.strip()
659 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
660 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
661 conv.time = google_strptime(exactTimeText)
662 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
663 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
666 nameGroup = self._voicemailNameRegex.search(messageHtml)
667 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
668 numberGroup = self._voicemailNumberRegex.search(messageHtml)
669 conv.number = numberGroup.group(1).strip() if numberGroup else ""
670 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
671 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
672 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
673 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
675 fromGroups = self._smsFromRegex.finditer(messageHtml)
676 fromParts = (group.group(1).strip() for group in fromGroups)
677 textGroups = self._smsTextRegex.finditer(messageHtml)
678 textParts = (group.group(1).strip() for group in textGroups)
679 timeGroups = self._smsTimeRegex.finditer(messageHtml)
680 timeParts = (group.group(1).strip() for group in timeGroups)
682 messageParts = itertools.izip(fromParts, textParts, timeParts)
683 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
684 conv.messages = messages
689 def _merge_conversation_sources(parsedMessages, json):
690 for message in parsedMessages:
691 jsonItem = json["messages"][message.id]
692 message.isRead = jsonItem["isRead"]
693 message.isSpam = jsonItem["isSpam"]
694 message.isTrash = jsonItem["isTrash"]
695 message.isArchived = "inbox" not in jsonItem["labels"]
698 def _get_page(self, url, data = None, refererUrl = None):
700 if refererUrl is not None:
701 headers["Referer"] = refererUrl
703 encodedData = urllib.urlencode(data) if data is not None else None
706 page = self._browser.download(url, encodedData, None, headers)
707 except urllib2.URLError, e:
708 _moduleLogger.error("Translating error: %s" % str(e))
709 raise NetworkError("%s is not accesible" % url)
713 def _get_page_with_token(self, url, data = None, refererUrl = None):
716 data['_rnr_se'] = self._token
718 page = self._get_page(url, data, refererUrl)
722 def _parse_with_validation(self, page):
723 json = parse_json(page)
724 validate_response(json)
728 _UNESCAPE_ENTITIES = {
736 plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
740 def google_strptime(time):
742 Hack: Google always returns the time in the same locale. Sadly if the
743 local system's locale is different, there isn't a way to perfectly handle
744 the time. So instead we handle implement some time formatting
746 abbrevTime = time[:-3]
747 parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
749 parsedTime += datetime.timedelta(hours=12)
753 def itergroup(iterator, count, padValue = None):
755 Iterate in groups of 'count' values. If there
756 aren't enough values, the last result is padded with
759 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
763 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
767 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
772 >>> for val in itergroup("123456", 3):
776 >>> for val in itergroup("123456", 3):
777 ... print repr("".join(val))
781 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
782 nIterators = (paddedIterator, ) * count
783 return itertools.izip(*nIterators)
787 _TRUE_REGEX = re.compile("true")
788 _FALSE_REGEX = re.compile("false")
789 s = _TRUE_REGEX.sub("True", s)
790 s = _FALSE_REGEX.sub("False", s)
791 return eval(s, {}, {})
794 def _fake_parse_json(flattened):
795 return safe_eval(flattened)
798 def _actual_parse_json(flattened):
799 return simplejson.loads(flattened)
802 if simplejson is None:
803 parse_json = _fake_parse_json
805 parse_json = _actual_parse_json
808 def extract_payload(flatXml):
809 xmlTree = ElementTree.fromstring(flatXml)
811 jsonElement = xmlTree.getchildren()[0]
812 flatJson = jsonElement.text
813 jsonTree = parse_json(flatJson)
815 htmlElement = xmlTree.getchildren()[1]
816 flatHtml = htmlElement.text
818 return jsonTree, flatHtml
821 def validate_response(response):
823 Validates that the JSON response is A-OK
826 assert 'ok' in response and response['ok']
827 except AssertionError:
828 raise RuntimeError('There was a problem with GV: %s' % response)
831 def guess_phone_type(number):
832 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
833 return GVoiceBackend.PHONE_TYPE_GIZMO
835 return GVoiceBackend.PHONE_TYPE_MOBILE
838 def get_sane_callback(backend):
840 Try to set a sane default callback number on these preferences
841 1) 1747 numbers ( Gizmo )
842 2) anything with gizmo in the name
843 3) anything with computer in the name
846 numbers = backend.get_callback_numbers()
848 priorityOrderedCriteria = [
858 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
860 descriptionMatcher = None
861 if numberCriteria is not None:
862 numberMatcher = re.compile(numberCriteria)
863 elif descriptionCriteria is not None:
864 descriptionMatcher = re.compile(descriptionCriteria, re.I)
866 for number, description in numbers.iteritems():
867 if numberMatcher is not None and numberMatcher.match(number) is None:
869 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
874 def set_sane_callback(backend):
876 Try to set a sane default callback number on these preferences
877 1) 1747 numbers ( Gizmo )
878 2) anything with gizmo in the name
879 3) anything with computer in the name
882 number = get_sane_callback(backend)
883 backend.set_callback_number(number)
886 def _is_not_special(name):
887 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
891 members = inspect.getmembers(obj)
892 return dict((name, value) for (name, value) in members if _is_not_special(name))
895 def grab_debug_info(username, password):
896 cookieFile = os.path.join(".", "raw_cookies.txt")
898 os.remove(cookieFile)
902 backend = GVoiceBackend(cookieFile)
903 browser = backend._browser
906 ("forward", backend._forwardURL),
907 ("token", backend._tokenURL),
908 ("login", backend._loginURL),
909 ("isdnd", backend._isDndURL),
910 ("account", backend._XML_ACCOUNT_URL),
911 ("contacts", backend._XML_CONTACTS_URL),
913 ("voicemail", backend._XML_VOICEMAIL_URL),
914 ("sms", backend._XML_SMS_URL),
916 ("recent", backend._XML_RECENT_URL),
917 ("placed", backend._XML_PLACED_URL),
918 ("recieved", backend._XML_RECEIVED_URL),
919 ("missed", backend._XML_MISSED_URL),
923 print "Grabbing pre-login pages"
924 for name, url in _TEST_WEBPAGES:
926 page = browser.download(url)
927 except StandardError, e:
930 print "\tWriting to file"
931 with open("not_loggedin_%s.txt" % name, "w") as f:
935 print "Attempting login"
936 galxToken = backend._get_token()
937 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
938 with open("loggingin.txt", "w") as f:
939 print "\tWriting to file"
940 f.write(loginSuccessOrFailurePage)
942 backend._grab_account_info(loginSuccessOrFailurePage)
944 # Retry in case the redirect failed
945 # luckily is_authed does everything we need for a retry
946 loggedIn = backend.is_authed(True)
951 print "Grabbing post-login pages"
952 for name, url in _TEST_WEBPAGES:
954 page = browser.download(url)
955 except StandardError, e:
958 print "\tWriting to file"
959 with open("loggedin_%s.txt" % name, "w") as f:
963 browser.save_cookies()
964 print "\tWriting cookies to file"
965 with open("cookies.txt", "w") as f:
967 "%s: %s\n" % (c.name, c.value)
968 for c in browser._cookies
974 logging.basicConfig(level=logging.DEBUG)
980 grab_debug_info(username, password)
983 if __name__ == "__main__":