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
309 self._callbackNumbers = {}
315 isDndPage = self._get_page(self._isDndURL)
317 dndGroup = self._isDndRe.search(isDndPage)
320 dndStatus = dndGroup.group(1)
321 isDnd = True if dndStatus.strip().lower() == "true" else False
324 def set_dnd(self, doNotDisturb):
329 "doNotDisturb": 1 if doNotDisturb else 0,
332 dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
334 def call(self, outgoingNumber):
336 This is the main function responsible for initating the callback
339 outgoingNumber = self._send_validation(outgoingNumber)
340 subscriberNumber = None
341 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
344 'outgoingNumber': outgoingNumber,
345 'forwardingNumber': self._callbackNumber,
346 'subscriberNumber': subscriberNumber or 'undefined',
347 'phoneType': str(phoneType),
350 _moduleLogger.info("%r" % callData)
352 page = self._get_page_with_token(
356 self._parse_with_validation(page)
359 def cancel(self, outgoingNumber=None):
361 Cancels a call matching outgoing and forwarding numbers (if given).
362 Will raise an error if no matching call is being placed
365 page = self._get_page_with_token(
368 'outgoingNumber': outgoingNumber or 'undefined',
369 'forwardingNumber': self._callbackNumber or 'undefined',
373 self._parse_with_validation(page)
375 def send_sms(self, phoneNumbers, message):
379 validatedPhoneNumbers = [
380 self._send_validation(phoneNumber)
381 for phoneNumber in phoneNumbers
383 flattenedPhoneNumbers = ",".join(validatedPhoneNumbers)
384 page = self._get_page_with_token(
387 'phoneNumber': flattenedPhoneNumbers,
388 'text': unicode(message).encode("utf-8"),
391 self._parse_with_validation(page)
393 def search(self, query):
395 Search your Google Voice Account history for calls, voicemails, and sms
396 Returns ``Folder`` instance containting matching messages
399 page = self._get_page(
400 self._XML_SEARCH_URL,
403 json, html = extract_payload(page)
406 def get_feed(self, feed):
410 actualFeed = "_XML_%s_URL" % feed.upper()
411 feedUrl = getattr(self, actualFeed)
413 page = self._get_page(feedUrl)
414 json, html = extract_payload(page)
418 def recording_url(self, messageId):
419 url = self._downloadVoicemailURL+messageId
422 def download(self, messageId, targetPath):
424 Download a voicemail or recorded call MP3 matching the given ``msg``
425 which can either be a ``Message`` instance, or a SHA1 identifier.
426 Message hashes can be found in ``self.voicemail().messages`` for example.
427 @returns location of saved file.
430 page = self._get_page(self.recording_url(messageId))
431 with open(targetPath, 'wb') as fo:
434 def is_valid_syntax(self, number):
436 @returns If This number be called ( syntax validation only )
438 return self._validateRe.match(number) is not None
440 def get_account_number(self):
442 @returns The GoogleVoice phone number
444 return self._accountNum
446 def get_callback_numbers(self):
448 @returns a dictionary mapping call back numbers to descriptions
449 @note These results are cached for 30 minutes.
451 return self._callbackNumbers
453 def set_callback_number(self, callbacknumber):
455 Set the number that GoogleVoice calls
456 @param callbacknumber should be a proper 10 digit number
458 self._callbackNumber = callbacknumber
459 _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
462 def get_callback_number(self):
464 @returns Current callback number or None
466 return self._callbackNumber
468 def get_received_calls(self):
470 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
473 return self._parse_recent(self._get_page(self._XML_RECEIVED_URL))
475 def get_missed_calls(self):
477 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
480 return self._parse_recent(self._get_page(self._XML_MISSED_URL))
482 def get_placed_calls(self):
484 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
487 return self._parse_recent(self._get_page(self._XML_PLACED_URL))
489 def get_csv_contacts(self):
491 "groupToExport": "mine",
493 "out": "OUTLOOK_CSV",
495 encodedData = urllib.urlencode(data)
496 contacts = self._get_page(self._CSV_CONTACTS_URL+"?"+encodedData)
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 if voicemailJson is None:
508 parsedVoicemail = self._parse_voicemail(voicemailHtml)
509 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
516 smsPage = self._get_page(self._XML_SMS_URL)
517 smsHtml = self._grab_html(smsPage)
518 smsJson = self._grab_json(smsPage)
521 parsedSms = self._parse_sms(smsHtml)
522 smss = self._merge_conversation_sources(parsedSms, smsJson)
525 def get_unread_counts(self):
526 countPage = self._get_page(self._JSON_SMS_COUNT_URL)
527 counts = parse_json(countPage)
528 counts = counts["unreadCounts"]
531 def mark_message(self, messageId, asRead):
536 "read": 1 if asRead else 0,
540 markPage = self._get_page(self._markAsReadURL, postData)
542 def archive_message(self, messageId):
550 markPage = self._get_page(self._archiveMessageURL, postData)
552 def _grab_json(self, flatXml):
553 xmlTree = ElementTree.fromstring(flatXml)
554 jsonElement = xmlTree.getchildren()[0]
555 flatJson = jsonElement.text
556 jsonTree = parse_json(flatJson)
559 def _grab_html(self, flatXml):
560 xmlTree = ElementTree.fromstring(flatXml)
561 htmlElement = xmlTree.getchildren()[1]
562 flatHtml = htmlElement.text
565 def _grab_account_info(self, page):
566 accountData = parse_json(page)
567 self._token = accountData["r"]
568 self._accountNum = accountData["number"]["raw"]
569 for callback in accountData["phones"].itervalues():
570 self._callbackNumbers[callback["phoneNumber"]] = callback["name"]
571 if len(self._callbackNumbers) == 0:
572 _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)
580 def _parse_recent(self, recentPage):
581 allRecentHtml = self._grab_html(recentPage)
582 allRecentData = self._parse_history(allRecentHtml)
583 for recentCallData in allRecentData:
586 def _parse_history(self, historyHtml):
587 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
588 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
589 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
590 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
591 exactTime = google_strptime(exactTime)
592 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
593 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
594 locationGroup = self._voicemailLocationRegex.search(messageHtml)
595 location = locationGroup.group(1).strip() if locationGroup else ""
597 nameGroup = self._voicemailNameRegex.search(messageHtml)
598 name = nameGroup.group(1).strip() if nameGroup else ""
599 numberGroup = self._voicemailNumberRegex.search(messageHtml)
600 number = numberGroup.group(1).strip() if numberGroup else ""
601 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
602 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
603 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
604 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
607 "id": messageId.strip(),
608 "contactId": contactId,
609 "name": unescape(name),
611 "relTime": relativeTime,
612 "prettyNumber": prettyNumber,
614 "location": unescape(location),
618 def _interpret_voicemail_regex(group):
619 quality, content, number = group.group(2), group.group(3), group.group(4)
621 if quality is not None and content is not None:
622 text.accuracy = quality
623 text.text = unescape(content)
625 elif number is not None:
626 text.accuracy = MessageText.ACCURACY_HIGH
630 def _parse_voicemail(self, voicemailHtml):
631 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
632 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
633 conv = Conversation()
634 conv.type = Conversation.TYPE_VOICEMAIL
635 conv.id = messageId.strip()
637 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
638 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
639 conv.time = google_strptime(exactTimeText)
640 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
641 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
642 locationGroup = self._voicemailLocationRegex.search(messageHtml)
643 conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "")
645 nameGroup = self._voicemailNameRegex.search(messageHtml)
646 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
647 numberGroup = self._voicemailNumberRegex.search(messageHtml)
648 conv.number = numberGroup.group(1).strip() if numberGroup else ""
649 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
650 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
651 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
652 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
654 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
656 self._interpret_voicemail_regex(group)
657 for group in messageGroups
658 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
660 message.body = messageParts
661 message.whoFrom = conv.name
663 message.when = conv.time.strftime("%I:%M %p")
665 _moduleLogger.exception("Confusing time provided: %r" % conv.time)
666 message.when = "Unknown"
667 conv.messages = (message, )
672 def _interpret_sms_message_parts(fromPart, textPart, timePart):
674 text.accuracy = MessageText.ACCURACY_MEDIUM
675 text.text = unescape(textPart)
678 message.body = (text, )
679 message.whoFrom = fromPart
680 message.when = timePart
684 def _parse_sms(self, smsHtml):
685 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
686 for messageId, messageHtml in itergroup(splitSms[1:], 2):
687 conv = Conversation()
688 conv.type = Conversation.TYPE_SMS
689 conv.id = messageId.strip()
691 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
692 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
693 conv.time = google_strptime(exactTimeText)
694 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
695 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
698 nameGroup = self._voicemailNameRegex.search(messageHtml)
699 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
700 numberGroup = self._voicemailNumberRegex.search(messageHtml)
701 conv.number = numberGroup.group(1).strip() if numberGroup else ""
702 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
703 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
704 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
705 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
707 fromGroups = self._smsFromRegex.finditer(messageHtml)
708 fromParts = (group.group(1).strip() for group in fromGroups)
709 textGroups = self._smsTextRegex.finditer(messageHtml)
710 textParts = (group.group(1).strip() for group in textGroups)
711 timeGroups = self._smsTimeRegex.finditer(messageHtml)
712 timeParts = (group.group(1).strip() for group in timeGroups)
714 messageParts = itertools.izip(fromParts, textParts, timeParts)
715 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
716 conv.messages = messages
721 def _merge_conversation_sources(parsedMessages, json):
722 for message in parsedMessages:
723 jsonItem = json["messages"][message.id]
724 message.isRead = jsonItem["isRead"]
725 message.isSpam = jsonItem["isSpam"]
726 message.isTrash = jsonItem["isTrash"]
727 message.isArchived = "inbox" not in jsonItem["labels"]
730 def _get_page(self, url, data = None, refererUrl = None):
732 if refererUrl is not None:
733 headers["Referer"] = refererUrl
735 encodedData = urllib.urlencode(data) if data is not None else None
738 page = self._browser.download(url, encodedData, None, headers)
739 except urllib2.URLError, e:
740 _moduleLogger.error("Translating error: %s" % str(e))
741 raise NetworkError("%s is not accesible" % url)
745 def _get_page_with_token(self, url, data = None, refererUrl = None):
748 data['_rnr_se'] = self._token
750 page = self._get_page(url, data, refererUrl)
754 def _parse_with_validation(self, page):
755 json = parse_json(page)
756 self._validate_response(json)
759 def _validate_response(self, response):
761 Validates that the JSON response is A-OK
764 assert response is not None, "Response not provided"
765 assert 'ok' in response, "Response lacks status"
766 assert response['ok'], "Response not good"
767 except AssertionError:
769 if response["data"]["code"] == 20:
771 """Ambiguous error 20 returned by Google Voice.
772 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)
775 raise RuntimeError('There was a problem with GV: %s' % response)
778 _UNESCAPE_ENTITIES = {
786 plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
790 def google_strptime(time):
792 Hack: Google always returns the time in the same locale. Sadly if the
793 local system's locale is different, there isn't a way to perfectly handle
794 the time. So instead we handle implement some time formatting
796 abbrevTime = time[:-3]
797 parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
798 if time.endswith("PM"):
799 parsedTime += datetime.timedelta(hours=12)
803 def itergroup(iterator, count, padValue = None):
805 Iterate in groups of 'count' values. If there
806 aren't enough values, the last result is padded with
809 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
813 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
817 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
822 >>> for val in itergroup("123456", 3):
826 >>> for val in itergroup("123456", 3):
827 ... print repr("".join(val))
831 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
832 nIterators = (paddedIterator, ) * count
833 return itertools.izip(*nIterators)
837 _TRUE_REGEX = re.compile("true")
838 _FALSE_REGEX = re.compile("false")
839 _COMMENT_REGEX = re.compile("^\s+//.*$", re.M)
840 s = _TRUE_REGEX.sub("True", s)
841 s = _FALSE_REGEX.sub("False", s)
842 s = _COMMENT_REGEX.sub("#", s)
844 results = eval(s, {}, {})
846 _moduleLogger.exception("Oops")
851 def _fake_parse_json(flattened):
852 return safe_eval(flattened)
855 def _actual_parse_json(flattened):
856 return simplejson.loads(flattened)
859 if simplejson is None:
860 parse_json = _fake_parse_json
862 parse_json = _actual_parse_json
865 def extract_payload(flatXml):
866 xmlTree = ElementTree.fromstring(flatXml)
868 jsonElement = xmlTree.getchildren()[0]
869 flatJson = jsonElement.text
870 jsonTree = parse_json(flatJson)
872 htmlElement = xmlTree.getchildren()[1]
873 flatHtml = htmlElement.text
875 return jsonTree, flatHtml
878 def guess_phone_type(number):
879 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
880 return GVoiceBackend.PHONE_TYPE_GIZMO
882 return GVoiceBackend.PHONE_TYPE_MOBILE
885 def get_sane_callback(backend):
887 Try to set a sane default callback number on these preferences
888 1) 1747 numbers ( Gizmo )
889 2) anything with gizmo in the name
890 3) anything with computer in the name
893 numbers = backend.get_callback_numbers()
895 priorityOrderedCriteria = [
905 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
907 descriptionMatcher = None
908 if numberCriteria is not None:
909 numberMatcher = re.compile(numberCriteria)
910 elif descriptionCriteria is not None:
911 descriptionMatcher = re.compile(descriptionCriteria, re.I)
913 for number, description in numbers.iteritems():
914 if numberMatcher is not None and numberMatcher.match(number) is None:
916 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
921 def set_sane_callback(backend):
923 Try to set a sane default callback number on these preferences
924 1) 1747 numbers ( Gizmo )
925 2) anything with gizmo in the name
926 3) anything with computer in the name
929 number = get_sane_callback(backend)
930 backend.set_callback_number(number)
933 def _is_not_special(name):
934 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
938 members = inspect.getmembers(obj)
939 return dict((name, value) for (name, value) in members if _is_not_special(name))
942 def grab_debug_info(username, password):
943 cookieFile = os.path.join(".", "raw_cookies.txt")
945 os.remove(cookieFile)
949 backend = GVoiceBackend(cookieFile)
950 browser = backend._browser
953 ("token", backend._tokenURL),
954 ("login", backend._loginURL),
955 ("isdnd", backend._isDndURL),
956 ("account", backend._XML_ACCOUNT_URL),
957 ("html_contacts", backend._XML_CONTACTS_URL),
958 ("contacts", backend._JSON_CONTACTS_URL),
959 ("csv", backend._CSV_CONTACTS_URL),
961 ("voicemail", backend._XML_VOICEMAIL_URL),
962 ("html_sms", backend._XML_SMS_URL),
963 ("sms", backend._JSON_SMS_URL),
964 ("count", backend._JSON_SMS_COUNT_URL),
966 ("recent", backend._XML_RECENT_URL),
967 ("placed", backend._XML_PLACED_URL),
968 ("recieved", backend._XML_RECEIVED_URL),
969 ("missed", backend._XML_MISSED_URL),
973 print "Grabbing pre-login pages"
974 for name, url in _TEST_WEBPAGES:
976 page = browser.download(url)
977 except StandardError, e:
980 print "\tWriting to file"
981 with open("not_loggedin_%s.txt" % name, "w") as f:
985 print "Attempting login"
986 galxToken = backend._get_token()
987 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
988 with open("loggingin.txt", "w") as f:
989 print "\tWriting to file"
990 f.write(loginSuccessOrFailurePage)
992 backend._grab_account_info(loginSuccessOrFailurePage)
994 # Retry in case the redirect failed
995 # luckily refresh_account_info does everything we need for a retry
996 loggedIn = backend.refresh_account_info() is not None
1001 print "Grabbing post-login pages"
1002 for name, url in _TEST_WEBPAGES:
1004 page = browser.download(url)
1005 except StandardError, e:
1008 print "\tWriting to file"
1009 with open("loggedin_%s.txt" % name, "w") as f:
1013 browser.save_cookies()
1014 print "\tWriting cookies to file"
1015 with open("cookies.txt", "w") as f:
1017 "%s: %s\n" % (c.name, c.value)
1018 for c in browser._cookies
1022 def grab_voicemails(username, password):
1023 cookieFile = os.path.join(".", "raw_cookies.txt")
1025 os.remove(cookieFile)
1029 backend = GVoiceBackend(cookieFile)
1030 backend.login(username, password)
1031 voicemails = list(backend.get_voicemails())
1032 for voicemail in voicemails:
1034 backend.download(voicemail.id, ".")
1039 logging.basicConfig(level=logging.DEBUG)
1045 grab_debug_info(username, password)
1046 grab_voicemails(username, password)
1049 if __name__ == "__main__":