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
48 import simplejson as _simplejson
49 simplejson = _simplejson
56 _moduleLogger = logging.getLogger(__name__)
59 class NetworkError(RuntimeError):
63 class MessageText(object):
66 ACCURACY_MEDIUM = "med2"
67 ACCURACY_HIGH = "high"
79 def __eq__(self, other):
80 return self.accuracy == other.accuracy and self.text == other.text
83 class Message(object):
91 return "%s (%s): %s" % (
94 "".join(unicode(part) for part in self.body)
98 selfDict = to_dict(self)
99 selfDict["body"] = [text.to_dict() for text in self.body] if self.body is not None else None
102 def __eq__(self, other):
103 return self.whoFrom == other.whoFrom and self.when == other.when and self.body == other.body
106 class Conversation(object):
108 TYPE_VOICEMAIL = "Voicemail"
114 self.contactId = None
117 self.prettyNumber = None
126 self.isArchived = None
128 def __cmp__(self, other):
129 cmpValue = cmp(self.contactId, other.contactId)
133 cmpValue = cmp(self.time, other.time)
137 cmpValue = cmp(self.id, other.id)
142 selfDict = to_dict(self)
143 selfDict["messages"] = [message.to_dict() for message in self.messages] if self.messages is not None else None
147 class GVoiceBackend(object):
149 This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
150 the functions include login, setting up a callback number, and initalting a callback
154 PHONE_TYPE_MOBILE = 2
158 def __init__(self, cookieFile = None):
159 # Important items in this function are the setup of the browser emulation and cookie file
160 self._browser = browser_emu.MozillaEmulator(1)
161 self._loadedFromCookies = self._browser.load_cookies(cookieFile)
164 self._accountNum = ""
165 self._lastAuthed = 0.0
166 self._callbackNumber = ""
167 self._callbackNumbers = {}
169 # Suprisingly, moving all of these from class to self sped up startup time
171 self._validateRe = re.compile("^\+?[0-9]{10,}$")
173 self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
175 SECURE_URL_BASE = "https://www.google.com/voice/"
176 SECURE_MOBILE_URL_BASE = SECURE_URL_BASE + "mobile/"
177 self._tokenURL = SECURE_URL_BASE + "m"
178 self._callUrl = SECURE_URL_BASE + "call/connect"
179 self._callCancelURL = SECURE_URL_BASE + "call/cancel"
180 self._sendSmsURL = SECURE_URL_BASE + "sms/send"
182 self._isDndURL = "https://www.google.com/voice/m/donotdisturb"
183 self._isDndRe = re.compile(r"""<input.*?id="doNotDisturb".*?checked="(.*?)"\s*/>""")
184 self._setDndURL = "https://www.google.com/voice/m/savednd"
186 self._downloadVoicemailURL = SECURE_URL_BASE + "media/send_voicemail/"
187 self._markAsReadURL = SECURE_URL_BASE + "m/mark"
188 self._archiveMessageURL = SECURE_URL_BASE + "m/archive"
190 self._XML_SEARCH_URL = SECURE_URL_BASE + "inbox/search/"
191 self._XML_ACCOUNT_URL = SECURE_URL_BASE + "contacts/"
192 # HACK really this redirects to the main pge and we are grabbing some javascript
193 self._XML_CONTACTS_URL = "http://www.google.com/voice/inbox/search/contact"
194 self._CSV_CONTACTS_URL = "http://mail.google.com/mail/contacts/data/export"
195 self._JSON_CONTACTS_URL = SECURE_URL_BASE + "b/0/request/user"
196 self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
199 'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms',
200 'recorded', 'placed', 'received', 'missed'
202 self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox"
203 self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred"
204 self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all"
205 self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam"
206 self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash"
207 self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/"
208 self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/"
209 self._JSON_SMS_URL = SECURE_URL_BASE + "b/0/request/messages/"
210 self._JSON_SMS_COUNT_URL = SECURE_URL_BASE + "b/0/request/unread"
211 self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/"
212 self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/"
213 self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
214 self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
216 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
218 self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
219 self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
220 self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
221 self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
222 self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
223 self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
224 self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
225 self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
226 self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
227 self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
228 self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
229 self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
231 def is_quick_login_possible(self):
233 @returns True then refresh_account_info might be enough to login, else full login is required
235 return self._loadedFromCookies or 0.0 < self._lastAuthed
237 def refresh_account_info(self):
239 page = self._get_page(self._JSON_CONTACTS_URL)
240 accountData = self._grab_account_info(page)
242 _moduleLogger.exception(str(e))
245 self._browser.save_cookies()
246 self._lastAuthed = time.time()
249 def _get_token(self):
250 tokenPage = self._get_page(self._tokenURL)
252 galxTokens = self._galxRe.search(tokenPage)
253 if galxTokens is not None:
254 galxToken = galxTokens.group(1)
257 _moduleLogger.debug("Could not grab GALX token")
260 def _login(self, username, password, token):
264 'service': "grandcentral",
267 "PersistentCookie": "yes",
269 "continue": self._JSON_CONTACTS_URL,
272 loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
273 return loginSuccessOrFailurePage
275 def login(self, username, password):
277 Attempt to login to GoogleVoice
278 @returns Whether login was successful or not
282 galxToken = self._get_token()
283 loginSuccessOrFailurePage = self._login(username, password, galxToken)
286 accountData = self._grab_account_info(loginSuccessOrFailurePage)
288 # Retry in case the redirect failed
289 # luckily refresh_account_info does everything we need for a retry
290 accountData = self.refresh_account_info()
291 if accountData is None:
292 _moduleLogger.exception(str(e))
294 _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
296 self._browser.save_cookies()
297 self._lastAuthed = time.time()
301 self._browser.save_cookies()
304 self._browser.save_cookies()
306 self._lastAuthed = 0.0
309 self._browser.clear_cookies()
310 self._browser.save_cookies()
312 self._lastAuthed = 0.0
313 self._callbackNumbers = {}
319 isDndPage = self._get_page(self._isDndURL)
321 dndGroup = self._isDndRe.search(isDndPage)
324 dndStatus = dndGroup.group(1)
325 isDnd = True if dndStatus.strip().lower() == "true" else False
328 def set_dnd(self, doNotDisturb):
333 "doNotDisturb": 1 if doNotDisturb else 0,
336 dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
338 def call(self, outgoingNumber):
340 This is the main function responsible for initating the callback
343 outgoingNumber = self._send_validation(outgoingNumber)
344 subscriberNumber = None
345 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
348 'outgoingNumber': outgoingNumber,
349 'forwardingNumber': self._callbackNumber,
350 'subscriberNumber': subscriberNumber or 'undefined',
351 'phoneType': str(phoneType),
354 _moduleLogger.info("%r" % callData)
356 page = self._get_page_with_token(
360 self._parse_with_validation(page)
363 def cancel(self, outgoingNumber=None):
365 Cancels a call matching outgoing and forwarding numbers (if given).
366 Will raise an error if no matching call is being placed
369 page = self._get_page_with_token(
372 'outgoingNumber': outgoingNumber or 'undefined',
373 'forwardingNumber': self._callbackNumber or 'undefined',
377 self._parse_with_validation(page)
379 def send_sms(self, phoneNumbers, message):
383 validatedPhoneNumbers = [
384 self._send_validation(phoneNumber)
385 for phoneNumber in phoneNumbers
387 flattenedPhoneNumbers = ",".join(validatedPhoneNumbers)
388 page = self._get_page_with_token(
391 'phoneNumber': flattenedPhoneNumbers,
392 'text': unicode(message).encode("utf-8"),
395 self._parse_with_validation(page)
397 def search(self, query):
399 Search your Google Voice Account history for calls, voicemails, and sms
400 Returns ``Folder`` instance containting matching messages
403 page = self._get_page(
404 self._XML_SEARCH_URL,
407 json, html = extract_payload(page)
410 def get_feed(self, feed):
414 actualFeed = "_XML_%s_URL" % feed.upper()
415 feedUrl = getattr(self, actualFeed)
417 page = self._get_page(feedUrl)
418 json, html = extract_payload(page)
422 def recording_url(self, messageId):
423 url = self._downloadVoicemailURL+messageId
426 def download(self, messageId, targetPath):
428 Download a voicemail or recorded call MP3 matching the given ``msg``
429 which can either be a ``Message`` instance, or a SHA1 identifier.
430 Message hashes can be found in ``self.voicemail().messages`` for example.
431 @returns location of saved file.
434 page = self._get_page(self.recording_url(messageId))
435 with open(targetPath, 'wb') as fo:
438 def is_valid_syntax(self, number):
440 @returns If This number be called ( syntax validation only )
442 return self._validateRe.match(number) is not None
444 def get_account_number(self):
446 @returns The GoogleVoice phone number
448 return self._accountNum
450 def get_callback_numbers(self):
452 @returns a dictionary mapping call back numbers to descriptions
453 @note These results are cached for 30 minutes.
455 return self._callbackNumbers
457 def set_callback_number(self, callbacknumber):
459 Set the number that GoogleVoice calls
460 @param callbacknumber should be a proper 10 digit number
462 self._callbackNumber = callbacknumber
463 _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
466 def get_callback_number(self):
468 @returns Current callback number or None
470 return self._callbackNumber
472 def get_received_calls(self):
474 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
477 return self._parse_recent(self._get_page(self._XML_RECEIVED_URL))
479 def get_missed_calls(self):
481 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
484 return self._parse_recent(self._get_page(self._XML_MISSED_URL))
486 def get_placed_calls(self):
488 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
491 return self._parse_recent(self._get_page(self._XML_PLACED_URL))
493 def get_csv_contacts(self):
495 "groupToExport": "mine",
497 "out": "OUTLOOK_CSV",
499 encodedData = urllib.urlencode(data)
500 contacts = self._get_page(self._CSV_CONTACTS_URL+"?"+encodedData)
503 def get_voicemails(self):
507 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
508 voicemailHtml = self._grab_html(voicemailPage)
509 voicemailJson = self._grab_json(voicemailPage)
510 if voicemailJson is None:
512 parsedVoicemail = self._parse_voicemail(voicemailHtml)
513 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
520 smsPage = self._get_page(self._XML_SMS_URL)
521 smsHtml = self._grab_html(smsPage)
522 smsJson = self._grab_json(smsPage)
525 parsedSms = self._parse_sms(smsHtml)
526 smss = self._merge_conversation_sources(parsedSms, smsJson)
529 def get_unread_counts(self):
530 countPage = self._get_page(self._JSON_SMS_COUNT_URL)
531 counts = parse_json(countPage)
532 counts = counts["unreadCounts"]
535 def mark_message(self, messageId, asRead):
540 "read": 1 if asRead else 0,
544 markPage = self._get_page(self._markAsReadURL, postData)
546 def archive_message(self, messageId):
554 markPage = self._get_page(self._archiveMessageURL, postData)
556 def _grab_json(self, flatXml):
557 xmlTree = ElementTree.fromstring(flatXml)
558 jsonElement = xmlTree.getchildren()[0]
559 flatJson = jsonElement.text
560 jsonTree = parse_json(flatJson)
563 def _grab_html(self, flatXml):
564 xmlTree = ElementTree.fromstring(flatXml)
565 htmlElement = xmlTree.getchildren()[1]
566 flatHtml = htmlElement.text
569 def _grab_account_info(self, page):
570 accountData = parse_json(page)
571 self._token = accountData["r"]
572 self._accountNum = accountData["number"]["raw"]
573 for callback in accountData["phones"].itervalues():
574 self._callbackNumbers[callback["phoneNumber"]] = callback["name"]
575 if len(self._callbackNumbers) == 0:
576 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
579 def _send_validation(self, number):
580 if not self.is_valid_syntax(number):
581 raise ValueError('Number is not valid: "%s"' % number)
584 def _parse_recent(self, recentPage):
585 allRecentHtml = self._grab_html(recentPage)
586 allRecentData = self._parse_history(allRecentHtml)
587 for recentCallData in allRecentData:
590 def _parse_history(self, historyHtml):
591 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
592 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
593 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
594 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
595 exactTime = google_strptime(exactTime)
596 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
597 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
598 locationGroup = self._voicemailLocationRegex.search(messageHtml)
599 location = locationGroup.group(1).strip() if locationGroup else ""
601 nameGroup = self._voicemailNameRegex.search(messageHtml)
602 name = nameGroup.group(1).strip() if nameGroup else ""
603 numberGroup = self._voicemailNumberRegex.search(messageHtml)
604 number = numberGroup.group(1).strip() if numberGroup else ""
605 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
606 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
607 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
608 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
611 "id": messageId.strip(),
612 "contactId": contactId,
613 "name": unescape(name),
615 "relTime": relativeTime,
616 "prettyNumber": prettyNumber,
618 "location": unescape(location),
622 def _interpret_voicemail_regex(group):
623 quality, content, number = group.group(2), group.group(3), group.group(4)
625 if quality is not None and content is not None:
626 text.accuracy = quality
627 text.text = unescape(content)
629 elif number is not None:
630 text.accuracy = MessageText.ACCURACY_HIGH
634 def _parse_voicemail(self, voicemailHtml):
635 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
636 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
637 conv = Conversation()
638 conv.type = Conversation.TYPE_VOICEMAIL
639 conv.id = messageId.strip()
641 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
642 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
643 conv.time = google_strptime(exactTimeText)
644 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
645 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
646 locationGroup = self._voicemailLocationRegex.search(messageHtml)
647 conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "")
649 nameGroup = self._voicemailNameRegex.search(messageHtml)
650 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
651 numberGroup = self._voicemailNumberRegex.search(messageHtml)
652 conv.number = numberGroup.group(1).strip() if numberGroup else ""
653 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
654 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
655 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
656 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
658 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
660 self._interpret_voicemail_regex(group)
661 for group in messageGroups
662 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
664 message.body = messageParts
665 message.whoFrom = conv.name
667 message.when = conv.time.strftime("%I:%M %p")
669 _moduleLogger.exception("Confusing time provided: %r" % conv.time)
670 message.when = "Unknown"
671 conv.messages = (message, )
676 def _interpret_sms_message_parts(fromPart, textPart, timePart):
678 text.accuracy = MessageText.ACCURACY_MEDIUM
679 text.text = unescape(textPart)
682 message.body = (text, )
683 message.whoFrom = fromPart
684 message.when = timePart
688 def _parse_sms(self, smsHtml):
689 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
690 for messageId, messageHtml in itergroup(splitSms[1:], 2):
691 conv = Conversation()
692 conv.type = Conversation.TYPE_SMS
693 conv.id = messageId.strip()
695 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
696 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
697 conv.time = google_strptime(exactTimeText)
698 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
699 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
702 nameGroup = self._voicemailNameRegex.search(messageHtml)
703 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
704 numberGroup = self._voicemailNumberRegex.search(messageHtml)
705 conv.number = numberGroup.group(1).strip() if numberGroup else ""
706 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
707 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
708 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
709 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
711 fromGroups = self._smsFromRegex.finditer(messageHtml)
712 fromParts = (group.group(1).strip() for group in fromGroups)
713 textGroups = self._smsTextRegex.finditer(messageHtml)
714 textParts = (group.group(1).strip() for group in textGroups)
715 timeGroups = self._smsTimeRegex.finditer(messageHtml)
716 timeParts = (group.group(1).strip() for group in timeGroups)
718 messageParts = itertools.izip(fromParts, textParts, timeParts)
719 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
720 conv.messages = messages
725 def _merge_conversation_sources(parsedMessages, json):
726 for message in parsedMessages:
727 jsonItem = json["messages"][message.id]
728 message.isRead = jsonItem["isRead"]
729 message.isSpam = jsonItem["isSpam"]
730 message.isTrash = jsonItem["isTrash"]
731 message.isArchived = "inbox" not in jsonItem["labels"]
734 def _get_page(self, url, data = None, refererUrl = None):
736 if refererUrl is not None:
737 headers["Referer"] = refererUrl
739 encodedData = urllib.urlencode(data) if data is not None else None
742 page = self._browser.download(url, encodedData, None, headers)
743 except urllib2.URLError, e:
744 _moduleLogger.error("Translating error: %s" % str(e))
745 raise NetworkError("%s is not accesible" % url)
749 def _get_page_with_token(self, url, data = None, refererUrl = None):
752 data['_rnr_se'] = self._token
754 page = self._get_page(url, data, refererUrl)
758 def _parse_with_validation(self, page):
759 json = parse_json(page)
760 self._validate_response(json)
763 def _validate_response(self, response):
765 Validates that the JSON response is A-OK
768 assert response is not None, "Response not provided"
769 assert 'ok' in response, "Response lacks status"
770 assert response['ok'], "Response not good"
771 except AssertionError:
773 if response["data"]["code"] == 20:
775 """Ambiguous error 20 returned by Google Voice.
776 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)
779 raise RuntimeError('There was a problem with GV: %s' % response)
782 _UNESCAPE_ENTITIES = {
790 plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
794 def google_strptime(time):
796 Hack: Google always returns the time in the same locale. Sadly if the
797 local system's locale is different, there isn't a way to perfectly handle
798 the time. So instead we handle implement some time formatting
800 abbrevTime = time[:-3]
801 parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
802 if time.endswith("PM"):
803 parsedTime += datetime.timedelta(hours=12)
807 def itergroup(iterator, count, padValue = None):
809 Iterate in groups of 'count' values. If there
810 aren't enough values, the last result is padded with
813 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
817 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
821 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
826 >>> for val in itergroup("123456", 3):
830 >>> for val in itergroup("123456", 3):
831 ... print repr("".join(val))
835 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
836 nIterators = (paddedIterator, ) * count
837 return itertools.izip(*nIterators)
841 _TRUE_REGEX = re.compile("true")
842 _FALSE_REGEX = re.compile("false")
843 _COMMENT_REGEX = re.compile("^\s+//.*$", re.M)
844 s = _TRUE_REGEX.sub("True", s)
845 s = _FALSE_REGEX.sub("False", s)
846 s = _COMMENT_REGEX.sub("#", s)
848 results = eval(s, {}, {})
850 _moduleLogger.exception("Oops")
855 def _fake_parse_json(flattened):
856 return safe_eval(flattened)
859 def _actual_parse_json(flattened):
860 return simplejson.loads(flattened)
863 if simplejson is None:
864 parse_json = _fake_parse_json
866 parse_json = _actual_parse_json
869 def extract_payload(flatXml):
870 xmlTree = ElementTree.fromstring(flatXml)
872 jsonElement = xmlTree.getchildren()[0]
873 flatJson = jsonElement.text
874 jsonTree = parse_json(flatJson)
876 htmlElement = xmlTree.getchildren()[1]
877 flatHtml = htmlElement.text
879 return jsonTree, flatHtml
882 def guess_phone_type(number):
883 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
884 return GVoiceBackend.PHONE_TYPE_GIZMO
886 return GVoiceBackend.PHONE_TYPE_MOBILE
889 def get_sane_callback(backend):
891 Try to set a sane default callback number on these preferences
892 1) 1747 numbers ( Gizmo )
893 2) anything with gizmo in the name
894 3) anything with computer in the name
897 numbers = backend.get_callback_numbers()
899 priorityOrderedCriteria = [
909 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
911 descriptionMatcher = None
912 if numberCriteria is not None:
913 numberMatcher = re.compile(numberCriteria)
914 elif descriptionCriteria is not None:
915 descriptionMatcher = re.compile(descriptionCriteria, re.I)
917 for number, description in numbers.iteritems():
918 if numberMatcher is not None and numberMatcher.match(number) is None:
920 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
925 def set_sane_callback(backend):
927 Try to set a sane default callback number on these preferences
928 1) 1747 numbers ( Gizmo )
929 2) anything with gizmo in the name
930 3) anything with computer in the name
933 number = get_sane_callback(backend)
934 backend.set_callback_number(number)
937 def _is_not_special(name):
938 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
942 members = inspect.getmembers(obj)
943 return dict((name, value) for (name, value) in members if _is_not_special(name))
946 def grab_debug_info(username, password):
947 cookieFile = os.path.join(".", "raw_cookies.txt")
949 os.remove(cookieFile)
953 backend = GVoiceBackend(cookieFile)
954 browser = backend._browser
957 ("token", backend._tokenURL),
958 ("login", backend._loginURL),
959 ("isdnd", backend._isDndURL),
960 ("account", backend._XML_ACCOUNT_URL),
961 ("html_contacts", backend._XML_CONTACTS_URL),
962 ("contacts", backend._JSON_CONTACTS_URL),
963 ("csv", backend._CSV_CONTACTS_URL),
965 ("voicemail", backend._XML_VOICEMAIL_URL),
966 ("html_sms", backend._XML_SMS_URL),
967 ("sms", backend._JSON_SMS_URL),
968 ("count", backend._JSON_SMS_COUNT_URL),
970 ("recent", backend._XML_RECENT_URL),
971 ("placed", backend._XML_PLACED_URL),
972 ("recieved", backend._XML_RECEIVED_URL),
973 ("missed", backend._XML_MISSED_URL),
977 print "Grabbing pre-login pages"
978 for name, url in _TEST_WEBPAGES:
980 page = browser.download(url)
981 except StandardError, e:
984 print "\tWriting to file"
985 with open("not_loggedin_%s.txt" % name, "w") as f:
989 print "Attempting login"
990 galxToken = backend._get_token()
991 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
992 with open("loggingin.txt", "w") as f:
993 print "\tWriting to file"
994 f.write(loginSuccessOrFailurePage)
996 backend._grab_account_info(loginSuccessOrFailurePage)
998 # Retry in case the redirect failed
999 # luckily refresh_account_info does everything we need for a retry
1000 loggedIn = backend.refresh_account_info() is not None
1005 print "Grabbing post-login pages"
1006 for name, url in _TEST_WEBPAGES:
1008 page = browser.download(url)
1009 except StandardError, e:
1012 print "\tWriting to file"
1013 with open("loggedin_%s.txt" % name, "w") as f:
1017 browser.save_cookies()
1018 print "\tWriting cookies to file"
1019 with open("cookies.txt", "w") as f:
1021 "%s: %s\n" % (c.name, c.value)
1022 for c in browser._cookies
1026 def grab_voicemails(username, password):
1027 cookieFile = os.path.join(".", "raw_cookies.txt")
1029 os.remove(cookieFile)
1033 backend = GVoiceBackend(cookieFile)
1034 backend.login(username, password)
1035 voicemails = list(backend.get_voicemails())
1036 for voicemail in voicemails:
1038 backend.download(voicemail.id, ".")
1043 logging.basicConfig(level=logging.DEBUG)
1049 grab_debug_info(username, password)
1050 grab_voicemails(username, password)
1053 if __name__ == "__main__":