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 download(self, messageId, adir):
419 Download a voicemail or recorded call MP3 matching the given ``msg``
420 which can either be a ``Message`` instance, or a SHA1 identifier.
421 Saves files to ``adir`` (defaults to current directory).
422 Message hashes can be found in ``self.voicemail().messages`` for example.
423 @returns location of saved file.
426 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
427 fn = os.path.join(adir, '%s.mp3' % messageId)
428 with open(fn, 'wb') as fo:
432 def is_valid_syntax(self, number):
434 @returns If This number be called ( syntax validation only )
436 return self._validateRe.match(number) is not None
438 def get_account_number(self):
440 @returns The GoogleVoice phone number
442 return self._accountNum
444 def get_callback_numbers(self):
446 @returns a dictionary mapping call back numbers to descriptions
447 @note These results are cached for 30 minutes.
449 return self._callbackNumbers
451 def set_callback_number(self, callbacknumber):
453 Set the number that GoogleVoice calls
454 @param callbacknumber should be a proper 10 digit number
456 self._callbackNumber = callbacknumber
457 _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
460 def get_callback_number(self):
462 @returns Current callback number or None
464 return self._callbackNumber
466 def get_recent(self):
468 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
472 (action, self._get_page(url))
474 ("Received", self._XML_RECEIVED_URL),
475 ("Missed", self._XML_MISSED_URL),
476 ("Placed", self._XML_PLACED_URL),
479 return self._parse_recent(recentPages)
481 def get_csv_contacts(self):
483 "groupToExport": "mine",
485 "out": "OUTLOOK_CSV",
487 encodedData = urllib.urlencode(data)
488 contacts = self._get_page(self._CSV_CONTACTS_URL+"?"+encodedData)
491 def get_voicemails(self):
495 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
496 voicemailHtml = self._grab_html(voicemailPage)
497 voicemailJson = self._grab_json(voicemailPage)
498 if voicemailJson is None:
500 parsedVoicemail = self._parse_voicemail(voicemailHtml)
501 voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
508 smsPage = self._get_page(self._XML_SMS_URL)
509 smsHtml = self._grab_html(smsPage)
510 smsJson = self._grab_json(smsPage)
513 parsedSms = self._parse_sms(smsHtml)
514 smss = self._merge_conversation_sources(parsedSms, smsJson)
517 def get_unread_counts(self):
518 countPage = self._get_page(self._JSON_SMS_COUNT_URL)
519 counts = parse_json(countPage)
520 counts = counts["unreadCounts"]
523 def mark_message(self, messageId, asRead):
528 "read": 1 if asRead else 0,
532 markPage = self._get_page(self._markAsReadURL, postData)
534 def archive_message(self, messageId):
542 markPage = self._get_page(self._archiveMessageURL, postData)
544 def _grab_json(self, flatXml):
545 xmlTree = ElementTree.fromstring(flatXml)
546 jsonElement = xmlTree.getchildren()[0]
547 flatJson = jsonElement.text
548 jsonTree = parse_json(flatJson)
551 def _grab_html(self, flatXml):
552 xmlTree = ElementTree.fromstring(flatXml)
553 htmlElement = xmlTree.getchildren()[1]
554 flatHtml = htmlElement.text
557 def _grab_account_info(self, page):
558 accountData = parse_json(page)
559 self._token = accountData["r"]
560 self._accountNum = accountData["number"]["raw"]
561 for callback in accountData["phones"].itervalues():
562 self._callbackNumbers[callback["phoneNumber"]] = callback["name"]
563 if len(self._callbackNumbers) == 0:
564 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
567 def _send_validation(self, number):
568 if not self.is_valid_syntax(number):
569 raise ValueError('Number is not valid: "%s"' % number)
572 def _parse_recent(self, recentPages):
573 for action, flatXml in recentPages:
574 allRecentHtml = self._grab_html(flatXml)
575 allRecentData = self._parse_history(allRecentHtml)
576 for recentCallData in allRecentData:
577 recentCallData["action"] = action
580 def _parse_history(self, historyHtml):
581 splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
582 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
583 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
584 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
585 exactTime = google_strptime(exactTime)
586 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
587 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
588 locationGroup = self._voicemailLocationRegex.search(messageHtml)
589 location = locationGroup.group(1).strip() if locationGroup else ""
591 nameGroup = self._voicemailNameRegex.search(messageHtml)
592 name = nameGroup.group(1).strip() if nameGroup else ""
593 numberGroup = self._voicemailNumberRegex.search(messageHtml)
594 number = numberGroup.group(1).strip() if numberGroup else ""
595 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
596 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
597 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
598 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
601 "id": messageId.strip(),
602 "contactId": contactId,
603 "name": unescape(name),
605 "relTime": relativeTime,
606 "prettyNumber": prettyNumber,
608 "location": unescape(location),
612 def _interpret_voicemail_regex(group):
613 quality, content, number = group.group(2), group.group(3), group.group(4)
615 if quality is not None and content is not None:
616 text.accuracy = quality
617 text.text = unescape(content)
619 elif number is not None:
620 text.accuracy = MessageText.ACCURACY_HIGH
624 def _parse_voicemail(self, voicemailHtml):
625 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
626 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
627 conv = Conversation()
628 conv.type = Conversation.TYPE_VOICEMAIL
629 conv.id = messageId.strip()
631 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
632 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
633 conv.time = google_strptime(exactTimeText)
634 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
635 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
636 locationGroup = self._voicemailLocationRegex.search(messageHtml)
637 conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "")
639 nameGroup = self._voicemailNameRegex.search(messageHtml)
640 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
641 numberGroup = self._voicemailNumberRegex.search(messageHtml)
642 conv.number = numberGroup.group(1).strip() if numberGroup else ""
643 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
644 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
645 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
646 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
648 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
650 self._interpret_voicemail_regex(group)
651 for group in messageGroups
652 ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
654 message.body = messageParts
655 message.whoFrom = conv.name
657 message.when = conv.time.strftime("%I:%M %p")
659 _moduleLogger.exception("Confusing time provided: %r" % conv.time)
660 message.when = "Unknown"
661 conv.messages = (message, )
666 def _interpret_sms_message_parts(fromPart, textPart, timePart):
668 text.accuracy = MessageText.ACCURACY_MEDIUM
669 text.text = unescape(textPart)
672 message.body = (text, )
673 message.whoFrom = fromPart
674 message.when = timePart
678 def _parse_sms(self, smsHtml):
679 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
680 for messageId, messageHtml in itergroup(splitSms[1:], 2):
681 conv = Conversation()
682 conv.type = Conversation.TYPE_SMS
683 conv.id = messageId.strip()
685 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
686 exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
687 conv.time = google_strptime(exactTimeText)
688 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
689 conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
692 nameGroup = self._voicemailNameRegex.search(messageHtml)
693 conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
694 numberGroup = self._voicemailNumberRegex.search(messageHtml)
695 conv.number = numberGroup.group(1).strip() if numberGroup else ""
696 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
697 conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
698 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
699 conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
701 fromGroups = self._smsFromRegex.finditer(messageHtml)
702 fromParts = (group.group(1).strip() for group in fromGroups)
703 textGroups = self._smsTextRegex.finditer(messageHtml)
704 textParts = (group.group(1).strip() for group in textGroups)
705 timeGroups = self._smsTimeRegex.finditer(messageHtml)
706 timeParts = (group.group(1).strip() for group in timeGroups)
708 messageParts = itertools.izip(fromParts, textParts, timeParts)
709 messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
710 conv.messages = messages
715 def _merge_conversation_sources(parsedMessages, json):
716 for message in parsedMessages:
717 jsonItem = json["messages"][message.id]
718 message.isRead = jsonItem["isRead"]
719 message.isSpam = jsonItem["isSpam"]
720 message.isTrash = jsonItem["isTrash"]
721 message.isArchived = "inbox" not in jsonItem["labels"]
724 def _get_page(self, url, data = None, refererUrl = None):
726 if refererUrl is not None:
727 headers["Referer"] = refererUrl
729 encodedData = urllib.urlencode(data) if data is not None else None
732 page = self._browser.download(url, encodedData, None, headers)
733 except urllib2.URLError, e:
734 _moduleLogger.error("Translating error: %s" % str(e))
735 raise NetworkError("%s is not accesible" % url)
739 def _get_page_with_token(self, url, data = None, refererUrl = None):
742 data['_rnr_se'] = self._token
744 page = self._get_page(url, data, refererUrl)
748 def _parse_with_validation(self, page):
749 json = parse_json(page)
750 self._validate_response(json)
753 def _validate_response(self, response):
755 Validates that the JSON response is A-OK
758 assert response is not None, "Response not provided"
759 assert 'ok' in response, "Response lacks status"
760 assert response['ok'], "Response not good"
761 except AssertionError:
763 if response["data"]["code"] == 20:
765 """Ambiguous error 20 returned by Google Voice.
766 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)
769 raise RuntimeError('There was a problem with GV: %s' % response)
772 _UNESCAPE_ENTITIES = {
780 plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
784 def google_strptime(time):
786 Hack: Google always returns the time in the same locale. Sadly if the
787 local system's locale is different, there isn't a way to perfectly handle
788 the time. So instead we handle implement some time formatting
790 abbrevTime = time[:-3]
791 parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
792 if time.endswith("PM"):
793 parsedTime += datetime.timedelta(hours=12)
797 def itergroup(iterator, count, padValue = None):
799 Iterate in groups of 'count' values. If there
800 aren't enough values, the last result is padded with
803 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
807 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
811 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
816 >>> for val in itergroup("123456", 3):
820 >>> for val in itergroup("123456", 3):
821 ... print repr("".join(val))
825 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
826 nIterators = (paddedIterator, ) * count
827 return itertools.izip(*nIterators)
831 _TRUE_REGEX = re.compile("true")
832 _FALSE_REGEX = re.compile("false")
833 _COMMENT_REGEX = re.compile("^\s+//.*$", re.M)
834 s = _TRUE_REGEX.sub("True", s)
835 s = _FALSE_REGEX.sub("False", s)
836 s = _COMMENT_REGEX.sub("#", s)
838 results = eval(s, {}, {})
840 _moduleLogger.exception("Oops")
845 def _fake_parse_json(flattened):
846 return safe_eval(flattened)
849 def _actual_parse_json(flattened):
850 return simplejson.loads(flattened)
853 if simplejson is None:
854 parse_json = _fake_parse_json
856 parse_json = _actual_parse_json
859 def extract_payload(flatXml):
860 xmlTree = ElementTree.fromstring(flatXml)
862 jsonElement = xmlTree.getchildren()[0]
863 flatJson = jsonElement.text
864 jsonTree = parse_json(flatJson)
866 htmlElement = xmlTree.getchildren()[1]
867 flatHtml = htmlElement.text
869 return jsonTree, flatHtml
872 def guess_phone_type(number):
873 if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
874 return GVoiceBackend.PHONE_TYPE_GIZMO
876 return GVoiceBackend.PHONE_TYPE_MOBILE
879 def get_sane_callback(backend):
881 Try to set a sane default callback number on these preferences
882 1) 1747 numbers ( Gizmo )
883 2) anything with gizmo in the name
884 3) anything with computer in the name
887 numbers = backend.get_callback_numbers()
889 priorityOrderedCriteria = [
899 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
901 descriptionMatcher = None
902 if numberCriteria is not None:
903 numberMatcher = re.compile(numberCriteria)
904 elif descriptionCriteria is not None:
905 descriptionMatcher = re.compile(descriptionCriteria, re.I)
907 for number, description in numbers.iteritems():
908 if numberMatcher is not None and numberMatcher.match(number) is None:
910 if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
915 def set_sane_callback(backend):
917 Try to set a sane default callback number on these preferences
918 1) 1747 numbers ( Gizmo )
919 2) anything with gizmo in the name
920 3) anything with computer in the name
923 number = get_sane_callback(backend)
924 backend.set_callback_number(number)
927 def _is_not_special(name):
928 return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
932 members = inspect.getmembers(obj)
933 return dict((name, value) for (name, value) in members if _is_not_special(name))
936 def grab_debug_info(username, password):
937 cookieFile = os.path.join(".", "raw_cookies.txt")
939 os.remove(cookieFile)
943 backend = GVoiceBackend(cookieFile)
944 browser = backend._browser
947 ("token", backend._tokenURL),
948 ("login", backend._loginURL),
949 ("isdnd", backend._isDndURL),
950 ("account", backend._XML_ACCOUNT_URL),
951 ("html_contacts", backend._XML_CONTACTS_URL),
952 ("contacts", backend._JSON_CONTACTS_URL),
953 ("csv", backend._CSV_CONTACTS_URL),
955 ("voicemail", backend._XML_VOICEMAIL_URL),
956 ("html_sms", backend._XML_SMS_URL),
957 ("sms", backend._JSON_SMS_URL),
958 ("count", backend._JSON_SMS_COUNT_URL),
960 ("recent", backend._XML_RECENT_URL),
961 ("placed", backend._XML_PLACED_URL),
962 ("recieved", backend._XML_RECEIVED_URL),
963 ("missed", backend._XML_MISSED_URL),
967 print "Grabbing pre-login pages"
968 for name, url in _TEST_WEBPAGES:
970 page = browser.download(url)
971 except StandardError, e:
974 print "\tWriting to file"
975 with open("not_loggedin_%s.txt" % name, "w") as f:
979 print "Attempting login"
980 galxToken = backend._get_token()
981 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
982 with open("loggingin.txt", "w") as f:
983 print "\tWriting to file"
984 f.write(loginSuccessOrFailurePage)
986 backend._grab_account_info(loginSuccessOrFailurePage)
988 # Retry in case the redirect failed
989 # luckily refresh_account_info does everything we need for a retry
990 loggedIn = backend.refresh_account_info() is not None
995 print "Grabbing post-login pages"
996 for name, url in _TEST_WEBPAGES:
998 page = browser.download(url)
999 except StandardError, e:
1002 print "\tWriting to file"
1003 with open("loggedin_%s.txt" % name, "w") as f:
1007 browser.save_cookies()
1008 print "\tWriting cookies to file"
1009 with open("cookies.txt", "w") as f:
1011 "%s: %s\n" % (c.name, c.value)
1012 for c in browser._cookies
1018 logging.basicConfig(level=logging.DEBUG)
1024 grab_debug_info(username, password)
1027 if __name__ == "__main__":