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.save_cookies()
310 self._lastAuthed = 0.0
313 self._browser.clear_cookies()
314 self._browser.save_cookies()
316 self._lastAuthed = 0.0
322 isDndPage = self._get_page(self._isDndURL)
324 dndGroup = self._isDndRe.search(isDndPage)
327 dndStatus = dndGroup.group(1)
328 isDnd = True if dndStatus.strip().lower() == "true" else False
331 def set_dnd(self, doNotDisturb):
336 "doNotDisturb": 1 if doNotDisturb else 0,
339 dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
341 def call(self, outgoingNumber):
343 This is the main function responsible for initating the callback
346 outgoingNumber = self._send_validation(outgoingNumber)
347 subscriberNumber = None
348 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
351 'outgoingNumber': outgoingNumber,
352 'forwardingNumber': self._callbackNumber,
353 'subscriberNumber': subscriberNumber or 'undefined',
354 'phoneType': str(phoneType),
357 _moduleLogger.info("%r" % callData)
359 page = self._get_page_with_token(
363 self._parse_with_validation(page)
366 def cancel(self, outgoingNumber=None):
368 Cancels a call matching outgoing and forwarding numbers (if given).
369 Will raise an error if no matching call is being placed
372 page = self._get_page_with_token(
375 'outgoingNumber': outgoingNumber or 'undefined',
376 'forwardingNumber': self._callbackNumber or 'undefined',
380 self._parse_with_validation(page)
382 def send_sms(self, phoneNumbers, message):
386 validatedPhoneNumbers = [
387 self._send_validation(phoneNumber)
388 for phoneNumber in phoneNumbers
390 flattenedPhoneNumbers = ",".join(validatedPhoneNumbers)
391 page = self._get_page_with_token(
394 'phoneNumber': flattenedPhoneNumbers,
398 self._parse_with_validation(page)
400 def search(self, query):
402 Search your Google Voice Account history for calls, voicemails, and sms
403 Returns ``Folder`` instance containting matching messages
406 page = self._get_page(
407 self._XML_SEARCH_URL,
410 json, html = extract_payload(page)
413 def get_feed(self, feed):
417 actualFeed = "_XML_%s_URL" % feed.upper()
418 feedUrl = getattr(self, actualFeed)
420 page = self._get_page(feedUrl)
421 json, html = extract_payload(page)
425 def download(self, messageId, adir):
427 Download a voicemail or recorded call MP3 matching the given ``msg``
428 which can either be a ``Message`` instance, or a SHA1 identifier.
429 Saves files to ``adir`` (defaults to current directory).
430 Message hashes can be found in ``self.voicemail().messages`` for example.
431 @returns location of saved file.
434 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
435 fn = os.path.join(adir, '%s.mp3' % messageId)
436 with open(fn, 'wb') as fo:
440 def is_valid_syntax(self, number):
442 @returns If This number be called ( syntax validation only )
444 return self._validateRe.match(number) is not None
446 def get_account_number(self):
448 @returns The GoogleVoice phone number
450 return self._accountNum
452 def get_callback_numbers(self):
454 @returns a dictionary mapping call back numbers to descriptions
455 @note These results are cached for 30 minutes.
457 if not self.is_authed():
459 return self._callbackNumbers
461 def set_callback_number(self, callbacknumber):
463 Set the number that GoogleVoice calls
464 @param callbacknumber should be a proper 10 digit number
466 self._callbackNumber = callbacknumber
467 _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
470 def get_callback_number(self):
472 @returns Current callback number or None
474 return self._callbackNumber
476 def get_recent(self):
478 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
482 (action, self._get_page(url))
484 ("Received", self._XML_RECEIVED_URL),
485 ("Missed", self._XML_MISSED_URL),
486 ("Placed", self._XML_PLACED_URL),
489 return self._parse_recent(recentPages)
491 def get_contacts(self):
493 @returns Iterable of (contact id, contact name)
496 page = self._get_page(self._XML_CONTACTS_URL)
497 return self._process_contacts(page)
499 def get_voicemails(self):
503 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
504 voicemailHtml = self._grab_html(voicemailPage)
505 voicemailJson = self._grab_json(voicemailPage)
506 parsedVoicemail = self._parse_voicemail(voicemailHtml)
507 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
514 smsPage = self._get_page(self._XML_SMS_URL)
515 smsHtml = self._grab_html(smsPage)
516 smsJson = self._grab_json(smsPage)
517 parsedSms = self._parse_sms(smsHtml)
518 smss = self._merge_conversation_sources(parsedSms, smsJson)
521 def mark_message(self, messageId, asRead):
526 "read": 1 if asRead else 0,
530 markPage = self._get_page(self._markAsReadURL, postData)
532 def archive_message(self, messageId):
540 markPage = self._get_page(self._archiveMessageURL, postData)
542 def _grab_json(self, flatXml):
543 xmlTree = ElementTree.fromstring(flatXml)
544 jsonElement = xmlTree.getchildren()[0]
545 flatJson = jsonElement.text
546 jsonTree = parse_json(flatJson)
549 def _grab_html(self, flatXml):
550 xmlTree = ElementTree.fromstring(flatXml)
551 htmlElement = xmlTree.getchildren()[1]
552 flatHtml = htmlElement.text
555 def _grab_account_info(self, page):
556 tokenGroup = self._tokenRe.search(page)
557 if tokenGroup is None:
558 raise RuntimeError("Could not extract authentication token from GoogleVoice")
559 self._token = tokenGroup.group(1)
561 anGroup = self._accountNumRe.search(page)
562 if anGroup is not None:
563 self._accountNum = anGroup.group(1)
565 _moduleLogger.debug("Could not extract account number from GoogleVoice")
567 self._callbackNumbers = {}
568 for match in self._callbackRe.finditer(page):
569 callbackNumber = match.group(2)
570 callbackName = match.group(1)
571 self._callbackNumbers[callbackNumber] = callbackName
572 if len(self._callbackNumbers) == 0:
573 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
575 def _send_validation(self, number):
576 if not self.is_valid_syntax(number):
577 raise ValueError('Number is not valid: "%s"' % number)
578 elif not self.is_authed():
579 raise RuntimeError("Not Authenticated")
582 def _parse_recent(self, recentPages):
583 for action, flatXml in recentPages:
584 allRecentHtml = self._grab_html(flatXml)
585 allRecentData = self._parse_history(allRecentHtml)
586 for recentCallData in allRecentData:
587 recentCallData["action"] = action
590 def _process_contacts(self, page):
591 contactsBody = self._contactsBodyRe.search(page)
592 if contactsBody is None:
593 raise RuntimeError("Could not extract contact information")
594 accountData = _fake_parse_json(contactsBody.group(1))
595 for contactId, contactDetails in accountData["contacts"].iteritems():
596 # A zero contact id is the catch all for unknown contacts
598 yield contactId, contactDetails
600 def _parse_history(self, historyHtml):
601 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
602 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
603 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
604 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
605 exactTime = google_strptime(exactTime)
606 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
607 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
608 locationGroup = self._voicemailLocationRegex.search(messageHtml)
609 location = locationGroup.group(1).strip() if locationGroup else ""
611 nameGroup = self._voicemailNameRegex.search(messageHtml)
612 name = nameGroup.group(1).strip() if nameGroup else ""
613 numberGroup = self._voicemailNumberRegex.search(messageHtml)
614 number = numberGroup.group(1).strip() if numberGroup else ""
615 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
616 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
617 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
618 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
621 "id": messageId.strip(),
622 "contactId": contactId,
625 "relTime": relativeTime,
626 "prettyNumber": prettyNumber,
628 "location": location,
632 def _interpret_voicemail_regex(group):
633 quality, content, number = group.group(2), group.group(3), group.group(4)
635 if quality is not None and content is not None:
636 text.accuracy = quality
639 elif number is not None:
640 text.accuracy = MessageText.ACCURACY_HIGH
644 def _parse_voicemail(self, voicemailHtml):
645 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
646 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
647 conv = Conversation()
648 conv.type = Conversation.TYPE_VOICEMAIL
649 conv.id = messageId.strip()
651 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
652 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
653 conv.time = google_strptime(exactTimeText)
654 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
655 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
656 locationGroup = self._voicemailLocationRegex.search(messageHtml)
657 conv.location = locationGroup.group(1).strip() if locationGroup else ""
659 nameGroup = self._voicemailNameRegex.search(messageHtml)
660 conv.name = nameGroup.group(1).strip() if nameGroup else ""
661 numberGroup = self._voicemailNumberRegex.search(messageHtml)
662 conv.number = numberGroup.group(1).strip() if numberGroup else ""
663 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
664 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
665 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
666 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
668 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
670 self._interpret_voicemail_regex(group)
671 for group in messageGroups
672 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
674 message.body = messageParts
675 message.whoFrom = conv.name
676 message.when = conv.time.strftime("%I:%M %p")
677 conv.messages = (message, )
682 def _interpret_sms_message_parts(fromPart, textPart, timePart):
684 text.accuracy = MessageText.ACCURACY_MEDIUM
688 message.body = (text, )
689 message.whoFrom = fromPart
690 message.when = timePart
694 def _parse_sms(self, smsHtml):
695 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
696 for messageId, messageHtml in itergroup(splitSms[1:], 2):
697 conv = Conversation()
698 conv.type = Conversation.TYPE_SMS
699 conv.id = messageId.strip()
701 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
702 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
703 conv.time = google_strptime(exactTimeText)
704 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
705 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
708 nameGroup = self._voicemailNameRegex.search(messageHtml)
709 conv.name = nameGroup.group(1).strip() if nameGroup else ""
710 numberGroup = self._voicemailNumberRegex.search(messageHtml)
711 conv.number = numberGroup.group(1).strip() if numberGroup else ""
712 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
713 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
714 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
715 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
717 fromGroups = self._smsFromRegex.finditer(messageHtml)
718 fromParts = (group.group(1).strip() for group in fromGroups)
719 textGroups = self._smsTextRegex.finditer(messageHtml)
720 textParts = (group.group(1).strip() for group in textGroups)
721 timeGroups = self._smsTimeRegex.finditer(messageHtml)
722 timeParts = (group.group(1).strip() for group in timeGroups)
724 messageParts = itertools.izip(fromParts, textParts, timeParts)
725 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
726 conv.messages = messages
731 def _merge_conversation_sources(parsedMessages, json):
732 for message in parsedMessages:
733 jsonItem = json["messages"][message.id]
734 message.isRead = jsonItem["isRead"]
735 message.isSpam = jsonItem["isSpam"]
736 message.isTrash = jsonItem["isTrash"]
737 message.isArchived = "inbox" not in jsonItem["labels"]
740 def _get_page(self, url, data = None, refererUrl = None):
742 if refererUrl is not None:
743 headers["Referer"] = refererUrl
745 encodedData = urllib.urlencode(data) if data is not None else None
748 page = self._browser.download(url, encodedData, None, headers)
749 except urllib2.URLError, e:
750 _moduleLogger.error("Translating error: %s" % str(e))
751 raise NetworkError("%s is not accesible" % url)
755 def _get_page_with_token(self, url, data = None, refererUrl = None):
758 data['_rnr_se'] = self._token
760 page = self._get_page(url, data, refererUrl)
764 def _parse_with_validation(self, page):
765 json = parse_json(page)
766 validate_response(json)
770 def google_strptime(time):
772 Hack: Google always returns the time in the same locale. Sadly if the
773 local system's locale is different, there isn't a way to perfectly handle
774 the time. So instead we handle implement some time formatting
776 abbrevTime = time[:-3]
777 parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
778 if time.endswith("PM"):
779 parsedTime += datetime.timedelta(hours=12)
783 def itergroup(iterator, count, padValue = None):
785 Iterate in groups of 'count' values. If there
786 aren't enough values, the last result is padded with
789 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
793 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
797 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
802 >>> for val in itergroup("123456", 3):
806 >>> for val in itergroup("123456", 3):
807 ... print repr("".join(val))
811 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
812 nIterators = (paddedIterator, ) * count
813 return itertools.izip(*nIterators)
817 _TRUE_REGEX = re.compile("true")
818 _FALSE_REGEX = re.compile("false")
819 s = _TRUE_REGEX.sub("True", s)
820 s = _FALSE_REGEX.sub("False", s)
821 return eval(s, {}, {})
824 def _fake_parse_json(flattened):
825 return safe_eval(flattened)
828 def _actual_parse_json(flattened):
829 return simplejson.loads(flattened)
832 if simplejson is None:
833 parse_json = _fake_parse_json
835 parse_json = _actual_parse_json
838 def extract_payload(flatXml):
839 xmlTree = ElementTree.fromstring(flatXml)
841 jsonElement = xmlTree.getchildren()[0]
842 flatJson = jsonElement.text
843 jsonTree = parse_json(flatJson)
845 htmlElement = xmlTree.getchildren()[1]
846 flatHtml = htmlElement.text
848 return jsonTree, flatHtml
851 def validate_response(response):
853 Validates that the JSON response is A-OK
856 assert 'ok' in response and response['ok']
857 except AssertionError:
858 raise RuntimeError('There was a problem with GV: %s' % response)
861 def guess_phone_type(number):
862 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
863 return GVoiceBackend.PHONE_TYPE_GIZMO
865 return GVoiceBackend.PHONE_TYPE_MOBILE
868 def get_sane_callback(backend):
870 Try to set a sane default callback number on these preferences
871 1) 1747 numbers ( Gizmo )
872 2) anything with gizmo in the name
873 3) anything with computer in the name
876 numbers = backend.get_callback_numbers()
878 priorityOrderedCriteria = [
888 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
890 descriptionMatcher = None
891 if numberCriteria is not None:
892 numberMatcher = re.compile(numberCriteria)
893 elif descriptionCriteria is not None:
894 descriptionMatcher = re.compile(descriptionCriteria, re.I)
896 for number, description in numbers.iteritems():
897 if numberMatcher is not None and numberMatcher.match(number) is None:
899 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
904 def set_sane_callback(backend):
906 Try to set a sane default callback number on these preferences
907 1) 1747 numbers ( Gizmo )
908 2) anything with gizmo in the name
909 3) anything with computer in the name
912 number = get_sane_callback(backend)
913 backend.set_callback_number(number)
916 def _is_not_special(name):
917 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
921 members = inspect.getmembers(obj)
922 return dict((name, value) for (name, value) in members if _is_not_special(name))
925 def grab_debug_info(username, password):
926 cookieFile = os.path.join(".", "raw_cookies.txt")
928 os.remove(cookieFile)
932 backend = GVoiceBackend(cookieFile)
933 browser = backend._browser
936 ("forward", backend._forwardURL),
937 ("token", backend._tokenURL),
938 ("login", backend._loginURL),
939 ("isdnd", backend._isDndURL),
940 ("account", backend._XML_ACCOUNT_URL),
941 ("contacts", backend._XML_CONTACTS_URL),
943 ("voicemail", backend._XML_VOICEMAIL_URL),
944 ("sms", backend._XML_SMS_URL),
946 ("recent", backend._XML_RECENT_URL),
947 ("placed", backend._XML_PLACED_URL),
948 ("recieved", backend._XML_RECEIVED_URL),
949 ("missed", backend._XML_MISSED_URL),
953 print "Grabbing pre-login pages"
954 for name, url in _TEST_WEBPAGES:
956 page = browser.download(url)
957 except StandardError, e:
960 print "\tWriting to file"
961 with open("not_loggedin_%s.txt" % name, "w") as f:
965 print "Attempting login"
966 galxToken = backend._get_token()
967 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
968 with open("loggingin.txt", "w") as f:
969 print "\tWriting to file"
970 f.write(loginSuccessOrFailurePage)
972 backend._grab_account_info(loginSuccessOrFailurePage)
974 # Retry in case the redirect failed
975 # luckily is_authed does everything we need for a retry
976 loggedIn = backend.is_authed(True)
981 print "Grabbing post-login pages"
982 for name, url in _TEST_WEBPAGES:
984 page = browser.download(url)
985 except StandardError, e:
988 print "\tWriting to file"
989 with open("loggedin_%s.txt" % name, "w") as f:
993 browser.save_cookies()
994 print "\tWriting cookies to file"
995 with open("cookies.txt", "w") as f:
997 "%s: %s\n" % (c.name, c.value)
998 for c in browser._cookies
1004 logging.basicConfig(level=logging.DEBUG)
1010 grab_debug_info(username, password)
1013 if __name__ == "__main__":