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(unicode(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._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._CSV_CONTACTS_URL = "http://mail.google.com/mail/contacts/data/export"
191 self._JSON_CONTACTS_URL = SECURE_URL_BASE + "b/0/request/user"
192 self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
195 'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms',
196 'recorded', 'placed', 'received', 'missed'
198 self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox"
199 self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred"
200 self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all"
201 self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam"
202 self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash"
203 self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/"
204 self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/"
205 self._JSON_SMS_URL = SECURE_URL_BASE + "b/0/request/messages/"
206 self._JSON_SMS_COUNT_URL = SECURE_URL_BASE + "b/0/request/unread"
207 self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/"
208 self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/"
209 self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
210 self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
212 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", 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 refresh_account_info might be enough to login, else full login is required
231 return self._loadedFromCookies or 0.0 < self._lastAuthed
233 def refresh_account_info(self):
235 page = self._get_page(self._JSON_CONTACTS_URL)
236 accountData = self._grab_account_info(page)
238 _moduleLogger.exception(str(e))
241 self._browser.save_cookies()
242 self._lastAuthed = time.time()
245 def _get_token(self):
246 tokenPage = self._get_page(self._tokenURL)
248 galxTokens = self._galxRe.search(tokenPage)
249 if galxTokens is not None:
250 galxToken = galxTokens.group(1)
253 _moduleLogger.debug("Could not grab GALX token")
256 def _login(self, username, password, token):
260 'service': "grandcentral",
263 "PersistentCookie": "yes",
265 "continue": self._JSON_CONTACTS_URL,
268 loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
269 return loginSuccessOrFailurePage
271 def login(self, username, password):
273 Attempt to login to GoogleVoice
274 @returns Whether login was successful or not
278 galxToken = self._get_token()
279 loginSuccessOrFailurePage = self._login(username, password, galxToken)
282 accountData = self._grab_account_info(loginSuccessOrFailurePage)
284 # Retry in case the redirect failed
285 # luckily refresh_account_info does everything we need for a retry
286 accountData = self.refresh_account_info()
287 if accountData is None:
288 _moduleLogger.exception(str(e))
290 _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
292 self._browser.save_cookies()
293 self._lastAuthed = time.time()
297 self._browser.save_cookies()
300 self._browser.save_cookies()
302 self._lastAuthed = 0.0
305 self._browser.clear_cookies()
306 self._browser.save_cookies()
308 self._lastAuthed = 0.0
314 isDndPage = self._get_page(self._isDndURL)
316 dndGroup = self._isDndRe.search(isDndPage)
319 dndStatus = dndGroup.group(1)
320 isDnd = True if dndStatus.strip().lower() == "true" else False
323 def set_dnd(self, doNotDisturb):
328 "doNotDisturb": 1 if doNotDisturb else 0,
331 dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
333 def call(self, outgoingNumber):
335 This is the main function responsible for initating the callback
338 outgoingNumber = self._send_validation(outgoingNumber)
339 subscriberNumber = None
340 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
343 'outgoingNumber': outgoingNumber,
344 'forwardingNumber': self._callbackNumber,
345 'subscriberNumber': subscriberNumber or 'undefined',
346 'phoneType': str(phoneType),
349 _moduleLogger.info("%r" % callData)
351 page = self._get_page_with_token(
355 self._parse_with_validation(page)
358 def cancel(self, outgoingNumber=None):
360 Cancels a call matching outgoing and forwarding numbers (if given).
361 Will raise an error if no matching call is being placed
364 page = self._get_page_with_token(
367 'outgoingNumber': outgoingNumber or 'undefined',
368 'forwardingNumber': self._callbackNumber or 'undefined',
372 self._parse_with_validation(page)
374 def send_sms(self, phoneNumbers, message):
378 validatedPhoneNumbers = [
379 self._send_validation(phoneNumber)
380 for phoneNumber in phoneNumbers
382 flattenedPhoneNumbers = ",".join(validatedPhoneNumbers)
383 page = self._get_page_with_token(
386 'phoneNumber': flattenedPhoneNumbers,
387 'text': unicode(message).encode("utf-8"),
390 self._parse_with_validation(page)
392 def search(self, query):
394 Search your Google Voice Account history for calls, voicemails, and sms
395 Returns ``Folder`` instance containting matching messages
398 page = self._get_page(
399 self._XML_SEARCH_URL,
402 json, html = extract_payload(page)
405 def get_feed(self, feed):
409 actualFeed = "_XML_%s_URL" % feed.upper()
410 feedUrl = getattr(self, actualFeed)
412 page = self._get_page(feedUrl)
413 json, html = extract_payload(page)
417 def recording_url(self, messageId):
418 url = self._downloadVoicemailURL+messageId
421 def download(self, messageId, targetPath):
423 Download a voicemail or recorded call MP3 matching the given ``msg``
424 which can either be a ``Message`` instance, or a SHA1 identifier.
425 Message hashes can be found in ``self.voicemail().messages`` for example.
426 @returns location of saved file.
429 page = self._get_page(self.recording_url(messageId))
430 with open(targetPath, 'wb') as fo:
433 def is_valid_syntax(self, number):
435 @returns If This number be called ( syntax validation only )
437 return self._validateRe.match(number) is not None
439 def get_account_number(self):
441 @returns The GoogleVoice phone number
443 return self._accountNum
445 def get_callback_numbers(self):
447 @returns a dictionary mapping call back numbers to descriptions
448 @note These results are cached for 30 minutes.
450 return self._callbackNumbers
452 def set_callback_number(self, callbacknumber):
454 Set the number that GoogleVoice calls
455 @param callbacknumber should be a proper 10 digit number
457 self._callbackNumber = callbacknumber
458 _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
461 def get_callback_number(self):
463 @returns Current callback number or None
465 return self._callbackNumber
467 def get_recent(self):
469 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
473 (action, self._get_page(url))
475 ("Received", self._XML_RECEIVED_URL),
476 ("Missed", self._XML_MISSED_URL),
477 ("Placed", self._XML_PLACED_URL),
480 return self._parse_recent(recentPages)
482 def get_csv_contacts(self):
484 "groupToExport": "mine",
486 "out": "OUTLOOK_CSV",
488 encodedData = urllib.urlencode(data)
489 contacts = self._get_page(self._CSV_CONTACTS_URL+"?"+encodedData)
492 def get_voicemails(self):
496 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
497 voicemailHtml = self._grab_html(voicemailPage)
498 voicemailJson = self._grab_json(voicemailPage)
499 if voicemailJson is None:
501 parsedVoicemail = self._parse_voicemail(voicemailHtml)
502 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
509 smsPage = self._get_page(self._XML_SMS_URL)
510 smsHtml = self._grab_html(smsPage)
511 smsJson = self._grab_json(smsPage)
514 parsedSms = self._parse_sms(smsHtml)
515 smss = self._merge_conversation_sources(parsedSms, smsJson)
518 def get_unread_counts(self):
519 countPage = self._get_page(self._JSON_SMS_COUNT_URL)
520 counts = parse_json(countPage)
521 counts = counts["unreadCounts"]
524 def mark_message(self, messageId, asRead):
529 "read": 1 if asRead else 0,
533 markPage = self._get_page(self._markAsReadURL, postData)
535 def archive_message(self, messageId):
543 markPage = self._get_page(self._archiveMessageURL, postData)
545 def _grab_json(self, flatXml):
546 xmlTree = ElementTree.fromstring(flatXml)
547 jsonElement = xmlTree.getchildren()[0]
548 flatJson = jsonElement.text
549 jsonTree = parse_json(flatJson)
552 def _grab_html(self, flatXml):
553 xmlTree = ElementTree.fromstring(flatXml)
554 htmlElement = xmlTree.getchildren()[1]
555 flatHtml = htmlElement.text
558 def _grab_account_info(self, page):
559 accountData = parse_json(page)
560 self._token = accountData["r"]
561 self._accountNum = accountData["number"]["raw"]
562 for callback in accountData["phones"].itervalues():
563 self._callbackNumbers[callback["phoneNumber"]] = callback["name"]
564 if len(self._callbackNumbers) == 0:
565 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
568 def _send_validation(self, number):
569 if not self.is_valid_syntax(number):
570 raise ValueError('Number is not valid: "%s"' % number)
573 def _parse_recent(self, recentPages):
574 for action, flatXml in recentPages:
575 allRecentHtml = self._grab_html(flatXml)
576 allRecentData = self._parse_history(allRecentHtml)
577 for recentCallData in allRecentData:
578 recentCallData["action"] = action
581 def _parse_history(self, historyHtml):
582 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
583 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
584 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
585 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
586 exactTime = google_strptime(exactTime)
587 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
588 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
589 locationGroup = self._voicemailLocationRegex.search(messageHtml)
590 location = locationGroup.group(1).strip() if locationGroup else ""
592 nameGroup = self._voicemailNameRegex.search(messageHtml)
593 name = nameGroup.group(1).strip() if nameGroup else ""
594 numberGroup = self._voicemailNumberRegex.search(messageHtml)
595 number = numberGroup.group(1).strip() if numberGroup else ""
596 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
597 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
598 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
599 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
602 "id": messageId.strip(),
603 "contactId": contactId,
604 "name": unescape(name),
606 "relTime": relativeTime,
607 "prettyNumber": prettyNumber,
609 "location": unescape(location),
613 def _interpret_voicemail_regex(group):
614 quality, content, number = group.group(2), group.group(3), group.group(4)
616 if quality is not None and content is not None:
617 text.accuracy = quality
618 text.text = unescape(content)
620 elif number is not None:
621 text.accuracy = MessageText.ACCURACY_HIGH
625 def _parse_voicemail(self, voicemailHtml):
626 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
627 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
628 conv = Conversation()
629 conv.type = Conversation.TYPE_VOICEMAIL
630 conv.id = messageId.strip()
632 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
633 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
634 conv.time = google_strptime(exactTimeText)
635 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
636 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
637 locationGroup = self._voicemailLocationRegex.search(messageHtml)
638 conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "")
640 nameGroup = self._voicemailNameRegex.search(messageHtml)
641 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
642 numberGroup = self._voicemailNumberRegex.search(messageHtml)
643 conv.number = numberGroup.group(1).strip() if numberGroup else ""
644 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
645 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
646 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
647 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
649 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
651 self._interpret_voicemail_regex(group)
652 for group in messageGroups
653 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
655 message.body = messageParts
656 message.whoFrom = conv.name
658 message.when = conv.time.strftime("%I:%M %p")
660 _moduleLogger.exception("Confusing time provided: %r" % conv.time)
661 message.when = "Unknown"
662 conv.messages = (message, )
667 def _interpret_sms_message_parts(fromPart, textPart, timePart):
669 text.accuracy = MessageText.ACCURACY_MEDIUM
670 text.text = unescape(textPart)
673 message.body = (text, )
674 message.whoFrom = fromPart
675 message.when = timePart
679 def _parse_sms(self, smsHtml):
680 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
681 for messageId, messageHtml in itergroup(splitSms[1:], 2):
682 conv = Conversation()
683 conv.type = Conversation.TYPE_SMS
684 conv.id = messageId.strip()
686 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
687 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
688 conv.time = google_strptime(exactTimeText)
689 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
690 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
693 nameGroup = self._voicemailNameRegex.search(messageHtml)
694 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
695 numberGroup = self._voicemailNumberRegex.search(messageHtml)
696 conv.number = numberGroup.group(1).strip() if numberGroup else ""
697 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
698 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
699 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
700 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
702 fromGroups = self._smsFromRegex.finditer(messageHtml)
703 fromParts = (group.group(1).strip() for group in fromGroups)
704 textGroups = self._smsTextRegex.finditer(messageHtml)
705 textParts = (group.group(1).strip() for group in textGroups)
706 timeGroups = self._smsTimeRegex.finditer(messageHtml)
707 timeParts = (group.group(1).strip() for group in timeGroups)
709 messageParts = itertools.izip(fromParts, textParts, timeParts)
710 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
711 conv.messages = messages
716 def _merge_conversation_sources(parsedMessages, json):
717 for message in parsedMessages:
718 jsonItem = json["messages"][message.id]
719 message.isRead = jsonItem["isRead"]
720 message.isSpam = jsonItem["isSpam"]
721 message.isTrash = jsonItem["isTrash"]
722 message.isArchived = "inbox" not in jsonItem["labels"]
725 def _get_page(self, url, data = None, refererUrl = None):
727 if refererUrl is not None:
728 headers["Referer"] = refererUrl
730 encodedData = urllib.urlencode(data) if data is not None else None
733 page = self._browser.download(url, encodedData, None, headers)
734 except urllib2.URLError, e:
735 _moduleLogger.error("Translating error: %s" % str(e))
736 raise NetworkError("%s is not accesible" % url)
740 def _get_page_with_token(self, url, data = None, refererUrl = None):
743 data['_rnr_se'] = self._token
745 page = self._get_page(url, data, refererUrl)
749 def _parse_with_validation(self, page):
750 json = parse_json(page)
751 self._validate_response(json)
754 def _validate_response(self, response):
756 Validates that the JSON response is A-OK
759 assert response is not None, "Response not provided"
760 assert 'ok' in response, "Response lacks status"
761 assert response['ok'], "Response not good"
762 except AssertionError:
764 if response["data"]["code"] == 20:
766 """Ambiguous error 20 returned by Google Voice.
767 Please verify you have configured your callback number (currently "%s"). If it is configured some other suspected causes are: non-verified callback numbers, and Gizmo5 callback numbers.""" % self._callbackNumber)
770 raise RuntimeError('There was a problem with GV: %s' % response)
773 _UNESCAPE_ENTITIES = {
781 plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
785 def google_strptime(time):
787 Hack: Google always returns the time in the same locale. Sadly if the
788 local system's locale is different, there isn't a way to perfectly handle
789 the time. So instead we handle implement some time formatting
791 abbrevTime = time[:-3]
792 parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
793 if time.endswith("PM"):
794 parsedTime += datetime.timedelta(hours=12)
798 def itergroup(iterator, count, padValue = None):
800 Iterate in groups of 'count' values. If there
801 aren't enough values, the last result is padded with
804 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
808 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
812 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
817 >>> for val in itergroup("123456", 3):
821 >>> for val in itergroup("123456", 3):
822 ... print repr("".join(val))
826 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
827 nIterators = (paddedIterator, ) * count
828 return itertools.izip(*nIterators)
832 _TRUE_REGEX = re.compile("true")
833 _FALSE_REGEX = re.compile("false")
834 _COMMENT_REGEX = re.compile("^\s+//.*$", re.M)
835 s = _TRUE_REGEX.sub("True", s)
836 s = _FALSE_REGEX.sub("False", s)
837 s = _COMMENT_REGEX.sub("#", s)
839 results = eval(s, {}, {})
841 _moduleLogger.exception("Oops")
846 def _fake_parse_json(flattened):
847 return safe_eval(flattened)
850 def _actual_parse_json(flattened):
851 return simplejson.loads(flattened)
854 if simplejson is None:
855 parse_json = _fake_parse_json
857 parse_json = _actual_parse_json
860 def extract_payload(flatXml):
861 xmlTree = ElementTree.fromstring(flatXml)
863 jsonElement = xmlTree.getchildren()[0]
864 flatJson = jsonElement.text
865 jsonTree = parse_json(flatJson)
867 htmlElement = xmlTree.getchildren()[1]
868 flatHtml = htmlElement.text
870 return jsonTree, flatHtml
873 def guess_phone_type(number):
874 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
875 return GVoiceBackend.PHONE_TYPE_GIZMO
877 return GVoiceBackend.PHONE_TYPE_MOBILE
880 def get_sane_callback(backend):
882 Try to set a sane default callback number on these preferences
883 1) 1747 numbers ( Gizmo )
884 2) anything with gizmo in the name
885 3) anything with computer in the name
888 numbers = backend.get_callback_numbers()
890 priorityOrderedCriteria = [
900 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
902 descriptionMatcher = None
903 if numberCriteria is not None:
904 numberMatcher = re.compile(numberCriteria)
905 elif descriptionCriteria is not None:
906 descriptionMatcher = re.compile(descriptionCriteria, re.I)
908 for number, description in numbers.iteritems():
909 if numberMatcher is not None and numberMatcher.match(number) is None:
911 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
916 def set_sane_callback(backend):
918 Try to set a sane default callback number on these preferences
919 1) 1747 numbers ( Gizmo )
920 2) anything with gizmo in the name
921 3) anything with computer in the name
924 number = get_sane_callback(backend)
925 backend.set_callback_number(number)
928 def _is_not_special(name):
929 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
933 members = inspect.getmembers(obj)
934 return dict((name, value) for (name, value) in members if _is_not_special(name))
937 def grab_debug_info(username, password):
938 cookieFile = os.path.join(".", "raw_cookies.txt")
940 os.remove(cookieFile)
944 backend = GVoiceBackend(cookieFile)
945 browser = backend._browser
948 ("token", backend._tokenURL),
949 ("login", backend._loginURL),
950 ("isdnd", backend._isDndURL),
951 ("account", backend._XML_ACCOUNT_URL),
952 ("html_contacts", backend._XML_CONTACTS_URL),
953 ("contacts", backend._JSON_CONTACTS_URL),
954 ("csv", backend._CSV_CONTACTS_URL),
956 ("voicemail", backend._XML_VOICEMAIL_URL),
957 ("html_sms", backend._XML_SMS_URL),
958 ("sms", backend._JSON_SMS_URL),
959 ("count", backend._JSON_SMS_COUNT_URL),
961 ("recent", backend._XML_RECENT_URL),
962 ("placed", backend._XML_PLACED_URL),
963 ("recieved", backend._XML_RECEIVED_URL),
964 ("missed", backend._XML_MISSED_URL),
968 print "Grabbing pre-login pages"
969 for name, url in _TEST_WEBPAGES:
971 page = browser.download(url)
972 except StandardError, e:
975 print "\tWriting to file"
976 with open("not_loggedin_%s.txt" % name, "w") as f:
980 print "Attempting login"
981 galxToken = backend._get_token()
982 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
983 with open("loggingin.txt", "w") as f:
984 print "\tWriting to file"
985 f.write(loginSuccessOrFailurePage)
987 backend._grab_account_info(loginSuccessOrFailurePage)
989 # Retry in case the redirect failed
990 # luckily refresh_account_info does everything we need for a retry
991 loggedIn = backend.refresh_account_info() is not None
996 print "Grabbing post-login pages"
997 for name, url in _TEST_WEBPAGES:
999 page = browser.download(url)
1000 except StandardError, e:
1003 print "\tWriting to file"
1004 with open("loggedin_%s.txt" % name, "w") as f:
1008 browser.save_cookies()
1009 print "\tWriting cookies to file"
1010 with open("cookies.txt", "w") as f:
1012 "%s: %s\n" % (c.name, c.value)
1013 for c in browser._cookies
1017 def grab_voicemails(username, password):
1018 cookieFile = os.path.join(".", "raw_cookies.txt")
1020 os.remove(cookieFile)
1024 backend = GVoiceBackend(cookieFile)
1025 backend.login(username, password)
1026 voicemails = list(backend.get_voicemails())
1027 for voicemail in voicemails:
1029 backend.download(voicemail.id, ".")
1034 logging.basicConfig(level=logging.DEBUG)
1040 grab_debug_info(username, password)
1041 grab_voicemails(username, password)
1044 if __name__ == "__main__":