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
38 from xml.sax import saxutils
40 from xml.etree import ElementTree
43 import simplejson as _simplejson
44 simplejson = _simplejson
51 _moduleLogger = logging.getLogger("gvoice.dialer")
54 class NetworkError(RuntimeError):
58 class GVDialer(object):
60 This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
61 the functions include login, setting up a callback number, and initalting a callback
69 def __init__(self, cookieFile = None):
70 # Important items in this function are the setup of the browser emulation and cookie file
71 self._browser = browser_emu.MozillaEmulator(1)
72 self._loadedFromCookies = self._browser.load_cookies(cookieFile)
76 self._lastAuthed = 0.0
77 self._callbackNumber = ""
78 self._callbackNumbers = {}
80 # Suprisingly, moving all of these from class to self sped up startup time
82 self._validateRe = re.compile("^[0-9]{10,}$")
84 self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
86 SECURE_URL_BASE = "https://www.google.com/voice/"
87 SECURE_MOBILE_URL_BASE = SECURE_URL_BASE + "mobile/"
88 self._forwardURL = SECURE_MOBILE_URL_BASE + "phones"
89 self._tokenURL = SECURE_URL_BASE + "m"
90 self._callUrl = SECURE_URL_BASE + "call/connect"
91 self._callCancelURL = SECURE_URL_BASE + "call/cancel"
92 self._sendSmsURL = SECURE_URL_BASE + "sms/send"
94 self._isDndURL = "https://www.google.com/voice/m/donotdisturb"
95 self._isDndRe = re.compile(r"""<input.*?id="doNotDisturb".*?checked="(.*?)"\s*/>""")
96 self._setDndURL = "https://www.google.com/voice/m/savednd"
98 self._downloadVoicemailURL = SECURE_URL_BASE + "media/send_voicemail/"
100 self._XML_SEARCH_URL = SECURE_URL_BASE + "inbox/search/"
101 self._XML_ACCOUNT_URL = SECURE_URL_BASE + "inbox/contacts/"
102 self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
105 'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms',
106 'recorded', 'placed', 'received', 'missed'
108 self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox"
109 self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred"
110 self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all"
111 self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam"
112 self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash"
113 self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/"
114 self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/"
115 self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/"
116 self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/"
117 self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
118 self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
120 self._contactsURL = SECURE_MOBILE_URL_BASE + "contacts"
121 self._contactDetailURL = SECURE_MOBILE_URL_BASE + "contact"
123 self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
124 self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
125 self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
126 self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
128 self._contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
129 self._contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
130 self._contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
132 self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
133 self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
134 self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
135 self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
136 self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
137 self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
138 self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
139 self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
140 self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
141 self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
142 self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
143 self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
145 def is_quick_login_possible(self):
147 @returns True then is_authed might be enough to login, else full login is required
149 return self._loadedFromCookies or 0.0 < self._lastAuthed
151 def is_authed(self, force = False):
153 Attempts to detect a current session
154 @note Once logged in try not to reauth more than once a minute.
155 @returns If authenticated
157 isRecentledAuthed = (time.time() - self._lastAuthed) < 120
158 isPreviouslyAuthed = self._token is not None
159 if isRecentledAuthed and isPreviouslyAuthed and not force:
163 page = self._get_page(self._forwardURL)
164 self._grab_account_info(page)
166 _moduleLogger.exception(str(e))
169 self._browser.save_cookies()
170 self._lastAuthed = time.time()
173 def _get_token(self):
174 tokenPage = self._get_page(self._tokenURL)
176 galxTokens = self._galxRe.search(tokenPage)
177 if galxTokens is not None:
178 galxToken = galxTokens.group(1)
181 _moduleLogger.debug("Could not grab GALX token")
184 def _login(self, username, password, token):
188 'service': "grandcentral",
191 "PersistentCookie": "yes",
193 "continue": self._forwardURL,
196 loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
197 return loginSuccessOrFailurePage
199 def login(self, username, password):
201 Attempt to login to GoogleVoice
202 @returns Whether login was successful or not
205 galxToken = self._get_token()
206 loginSuccessOrFailurePage = self._login(username, password, galxToken)
209 self._grab_account_info(loginSuccessOrFailurePage)
211 # Retry in case the redirect failed
212 # luckily is_authed does everything we need for a retry
213 loggedIn = self.is_authed(True)
215 _moduleLogger.exception(str(e))
217 _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
219 self._browser.save_cookies()
220 self._lastAuthed = time.time()
224 self._browser.clear_cookies()
225 self._browser.save_cookies()
227 self._lastAuthed = 0.0
230 isDndPage = self._get_page(self._isDndURL)
232 dndGroup = self._isDndRe.search(isDndPage)
235 dndStatus = dndGroup.group(1)
236 isDnd = True if dndStatus.strip().lower() == "true" else False
239 def set_dnd(self, doNotDisturb):
241 "doNotDisturb": 1 if doNotDisturb else 0,
242 "_rnr_se": self._token,
245 dndPage = self._get_page(self._setDndURL, dndPostData)
247 def call(self, outgoingNumber):
249 This is the main function responsible for initating the callback
251 outgoingNumber = self._send_validation(outgoingNumber)
252 subscriberNumber = None
253 phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
256 'outgoingNumber': outgoingNumber,
257 'forwardingNumber': self._callbackNumber,
258 'subscriberNumber': subscriberNumber or 'undefined',
259 'phoneType': str(phoneType),
262 _moduleLogger.info("%r" % callData)
264 page = self._get_page_with_token(
268 self._parse_with_validation(page)
271 def cancel(self, outgoingNumber=None):
273 Cancels a call matching outgoing and forwarding numbers (if given).
274 Will raise an error if no matching call is being placed
276 page = self._get_page_with_token(
279 'outgoingNumber': outgoingNumber or 'undefined',
280 'forwardingNumber': self._callbackNumber or 'undefined',
284 self._parse_with_validation(page)
286 def send_sms(self, phoneNumber, message):
287 phoneNumber = self._send_validation(phoneNumber)
288 page = self._get_page_with_token(
291 'phoneNumber': phoneNumber,
295 self._parse_with_validation(page)
297 def search(self, query):
299 Search your Google Voice Account history for calls, voicemails, and sms
300 Returns ``Folder`` instance containting matching messages
302 page = self._get_page(
303 self._XML_SEARCH_URL,
306 json, html = extract_payload(page)
309 def get_feed(self, feed):
310 actualFeed = "_XML_%s_URL" % feed.upper()
311 feedUrl = getattr(self, actualFeed)
313 page = self._get_page(feedUrl)
314 json, html = extract_payload(page)
318 def download(self, messageId, adir):
320 Download a voicemail or recorded call MP3 matching the given ``msg``
321 which can either be a ``Message`` instance, or a SHA1 identifier.
322 Saves files to ``adir`` (defaults to current directory).
323 Message hashes can be found in ``self.voicemail().messages`` for example.
324 Returns location of saved file.
326 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
327 fn = os.path.join(adir, '%s.mp3' % messageId)
328 with open(fn, 'wb') as fo:
332 def is_valid_syntax(self, number):
334 @returns If This number be called ( syntax validation only )
336 return self._validateRe.match(number) is not None
338 def get_account_number(self):
340 @returns The GoogleVoice phone number
342 return self._accountNum
344 def get_callback_numbers(self):
346 @returns a dictionary mapping call back numbers to descriptions
347 @note These results are cached for 30 minutes.
349 if not self.is_authed():
351 return self._callbackNumbers
353 def set_callback_number(self, callbacknumber):
355 Set the number that GoogleVoice calls
356 @param callbacknumber should be a proper 10 digit number
358 self._callbackNumber = callbacknumber
361 def get_callback_number(self):
363 @returns Current callback number or None
365 return self._callbackNumber
367 def get_recent(self):
369 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
372 ("Received", self._XML_RECEIVED_URL),
373 ("Missed", self._XML_MISSED_URL),
374 ("Placed", self._XML_PLACED_URL),
376 flatXml = self._get_page(url)
378 allRecentHtml = self._grab_html(flatXml)
379 allRecentData = self._parse_voicemail(allRecentHtml)
380 for recentCallData in allRecentData:
381 recentCallData["action"] = action
384 def get_contacts(self):
386 @returns Iterable of (contact id, contact name)
388 contactsPagesUrls = [self._contactsURL]
389 for contactsPageUrl in contactsPagesUrls:
390 contactsPage = self._get_page(contactsPageUrl)
391 for contact_match in self._contactsRe.finditer(contactsPage):
392 contactId = contact_match.group(1)
393 contactName = saxutils.unescape(contact_match.group(2))
394 contact = contactId, contactName
397 next_match = self._contactsNextRe.match(contactsPage)
398 if next_match is not None:
399 newContactsPageUrl = self._contactsURL + next_match.group(1)
400 contactsPagesUrls.append(newContactsPageUrl)
402 def get_contact_details(self, contactId):
404 @returns Iterable of (Phone Type, Phone Number)
406 detailPage = self._get_page(self._contactDetailURL + '/' + contactId)
408 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
409 phoneNumber = detail_match.group(1)
410 phoneType = saxutils.unescape(detail_match.group(2))
411 yield (phoneType, phoneNumber)
413 def get_messages(self):
414 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
415 voicemailHtml = self._grab_html(voicemailPage)
416 voicemailJson = self._grab_json(voicemailPage)
417 parsedVoicemail = self._parse_voicemail(voicemailHtml)
418 voicemails = self._merge_messages(parsedVoicemail, voicemailJson)
419 decoratedVoicemails = self._decorate_voicemail(voicemails)
421 smsPage = self._get_page(self._XML_SMS_URL)
422 smsHtml = self._grab_html(smsPage)
423 smsJson = self._grab_json(smsPage)
424 parsedSms = self._parse_sms(smsHtml)
425 smss = self._merge_messages(parsedSms, smsJson)
426 decoratedSms = self._decorate_sms(smss)
428 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
431 def clear_caches(self):
434 def get_addressbooks(self):
436 @returns Iterable of (Address Book Factory, Book Id, Book Name)
440 def open_addressbook(self, bookId):
444 def contact_source_short_name(contactId):
449 return "Google Voice"
451 def _grab_json(self, flatXml):
452 xmlTree = ElementTree.fromstring(flatXml)
453 jsonElement = xmlTree.getchildren()[0]
454 flatJson = jsonElement.text
455 jsonTree = parse_json(flatJson)
458 def _grab_html(self, flatXml):
459 xmlTree = ElementTree.fromstring(flatXml)
460 htmlElement = xmlTree.getchildren()[1]
461 flatHtml = htmlElement.text
464 def _grab_account_info(self, page):
465 tokenGroup = self._tokenRe.search(page)
466 if tokenGroup is None:
467 raise RuntimeError("Could not extract authentication token from GoogleVoice")
468 self._token = tokenGroup.group(1)
470 anGroup = self._accountNumRe.search(page)
471 if anGroup is not None:
472 self._accountNum = anGroup.group(1)
474 _moduleLogger.debug("Could not extract account number from GoogleVoice")
476 self._callbackNumbers = {}
477 for match in self._callbackRe.finditer(page):
478 callbackNumber = match.group(2)
479 callbackName = match.group(1)
480 self._callbackNumbers[callbackNumber] = callbackName
481 if len(self._callbackNumbers) == 0:
482 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
484 def _send_validation(self, number):
485 if not self.is_valid_syntax(number):
486 raise ValueError('Number is not valid: "%s"' % number)
487 elif not self.is_authed():
488 raise RuntimeError("Not Authenticated")
490 if len(number) == 11 and number[0] == 1:
491 # Strip leading 1 from 11 digit dialing
496 def _interpret_voicemail_regex(group):
497 quality, content, number = group.group(2), group.group(3), group.group(4)
498 if quality is not None and content is not None:
499 return quality, content
500 elif number is not None:
501 return "high", number
503 def _parse_voicemail(self, voicemailHtml):
504 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
505 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
506 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
507 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
508 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
509 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
510 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
511 locationGroup = self._voicemailLocationRegex.search(messageHtml)
512 location = locationGroup.group(1).strip() if locationGroup else ""
514 nameGroup = self._voicemailNameRegex.search(messageHtml)
515 name = nameGroup.group(1).strip() if nameGroup else ""
516 numberGroup = self._voicemailNumberRegex.search(messageHtml)
517 number = numberGroup.group(1).strip() if numberGroup else ""
518 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
519 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
520 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
521 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
523 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
525 self._interpret_voicemail_regex(group)
526 for group in messageGroups
527 ) if messageGroups else ()
530 "id": messageId.strip(),
531 "contactId": contactId,
534 "relTime": relativeTime,
535 "prettyNumber": prettyNumber,
537 "location": location,
538 "messageParts": messageParts,
542 def _decorate_voicemail(self, parsedVoicemails):
543 messagePartFormat = {
548 for voicemailData in parsedVoicemails:
550 messagePartFormat[quality] % part
551 for (quality, part) in voicemailData["messageParts"]
554 message = "No Transcription"
555 whoFrom = voicemailData["name"]
556 when = voicemailData["time"]
557 voicemailData["messageParts"] = ((whoFrom, message, when), )
560 def _parse_sms(self, smsHtml):
561 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
562 for messageId, messageHtml in itergroup(splitSms[1:], 2):
563 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
564 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
565 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
566 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
567 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
569 nameGroup = self._voicemailNameRegex.search(messageHtml)
570 name = nameGroup.group(1).strip() if nameGroup else ""
571 numberGroup = self._voicemailNumberRegex.search(messageHtml)
572 number = numberGroup.group(1).strip() if numberGroup else ""
573 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
574 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
575 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
576 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
578 fromGroups = self._smsFromRegex.finditer(messageHtml)
579 fromParts = (group.group(1).strip() for group in fromGroups)
580 textGroups = self._smsTextRegex.finditer(messageHtml)
581 textParts = (group.group(1).strip() for group in textGroups)
582 timeGroups = self._smsTimeRegex.finditer(messageHtml)
583 timeParts = (group.group(1).strip() for group in timeGroups)
585 messageParts = itertools.izip(fromParts, textParts, timeParts)
588 "id": messageId.strip(),
589 "contactId": contactId,
592 "relTime": relativeTime,
593 "prettyNumber": prettyNumber,
596 "messageParts": messageParts,
600 def _decorate_sms(self, parsedTexts):
604 def _merge_messages(parsedMessages, json):
605 for message in parsedMessages:
607 jsonItem = json["messages"][id]
608 message["isRead"] = jsonItem["isRead"]
609 message["isSpam"] = jsonItem["isSpam"]
610 message["isTrash"] = jsonItem["isTrash"]
611 message["isArchived"] = "inbox" not in jsonItem["labels"]
614 def _get_page(self, url, data = None, refererUrl = None):
616 if refererUrl is not None:
617 headers["Referer"] = refererUrl
619 encodedData = urllib.urlencode(data) if data is not None else None
622 page = self._browser.download(url, encodedData, None, headers)
623 except urllib2.URLError, e:
624 _moduleLogger.error("Translating error: %s" % str(e))
625 raise NetworkError("%s is not accesible" % url)
629 def _get_page_with_token(self, url, data = None, refererUrl = None):
632 data['_rnr_se'] = self._token
634 page = self._get_page(url, data, refererUrl)
638 def _parse_with_validation(self, page):
639 json = parse_json(page)
640 validate_response(json)
644 def itergroup(iterator, count, padValue = None):
646 Iterate in groups of 'count' values. If there
647 aren't enough values, the last result is padded with
650 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
654 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
658 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
663 >>> for val in itergroup("123456", 3):
667 >>> for val in itergroup("123456", 3):
668 ... print repr("".join(val))
672 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
673 nIterators = (paddedIterator, ) * count
674 return itertools.izip(*nIterators)
678 _TRUE_REGEX = re.compile("true")
679 _FALSE_REGEX = re.compile("false")
680 s = _TRUE_REGEX.sub("True", s)
681 s = _FALSE_REGEX.sub("False", s)
682 return eval(s, {}, {})
685 def _fake_parse_json(flattened):
686 return safe_eval(flattened)
689 def _actual_parse_json(flattened):
690 return simplejson.loads(flattened)
693 if simplejson is None:
694 parse_json = _fake_parse_json
696 parse_json = _actual_parse_json
699 def extract_payload(flatXml):
700 xmlTree = ElementTree.fromstring(flatXml)
702 jsonElement = xmlTree.getchildren()[0]
703 flatJson = jsonElement.text
704 jsonTree = parse_json(flatJson)
706 htmlElement = xmlTree.getchildren()[1]
707 flatHtml = htmlElement.text
709 return jsonTree, flatHtml
712 def validate_response(response):
714 Validates that the JSON response is A-OK
717 assert 'ok' in response and response['ok']
718 except AssertionError:
719 raise RuntimeError('There was a problem with GV: %s' % response)
722 def guess_phone_type(number):
723 if number.startswith("747") or number.startswith("1747"):
724 return GVDialer.PHONE_TYPE_GIZMO
726 return GVDialer.PHONE_TYPE_MOBILE
729 def set_sane_callback(backend):
731 Try to set a sane default callback number on these preferences
732 1) 1747 numbers ( Gizmo )
733 2) anything with gizmo in the name
734 3) anything with computer in the name
737 numbers = backend.get_callback_numbers()
739 priorityOrderedCriteria = [
747 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
748 for number, description in numbers.iteritems():
749 if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
751 if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
753 backend.set_callback_number(number)
757 def sort_messages(allMessages):
758 sortableAllMessages = [
759 (message["time"], message)
760 for message in allMessages
762 sortableAllMessages.sort(reverse=True)
765 for (exactTime, message) in sortableAllMessages
769 def decorate_recent(recentCallData):
771 @returns (personsName, phoneNumber, date, action)
773 contactId = recentCallData["contactId"]
774 if recentCallData["name"]:
775 header = recentCallData["name"]
776 elif recentCallData["prettyNumber"]:
777 header = recentCallData["prettyNumber"]
778 elif recentCallData["location"]:
779 header = recentCallData["location"]
783 number = recentCallData["number"]
784 relTime = recentCallData["relTime"]
785 action = recentCallData["action"]
786 return contactId, header, number, relTime, action
789 def decorate_message(messageData):
790 contactId = messageData["contactId"]
791 exactTime = messageData["time"]
792 if messageData["name"]:
793 header = messageData["name"]
794 elif messageData["prettyNumber"]:
795 header = messageData["prettyNumber"]
798 number = messageData["number"]
799 relativeTime = messageData["relTime"]
801 messageParts = list(messageData["messageParts"])
802 if len(messageParts) == 0:
803 messages = ("No Transcription", )
804 elif len(messageParts) == 1:
805 messages = (messageParts[0][1], )
808 "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
809 for messagePart in messageParts
812 decoratedResults = contactId, header, number, relativeTime, messages
813 return decoratedResults
816 def test_backend(username, password):
818 print "Authenticated: ", backend.is_authed()
819 if not backend.is_authed():
820 print "Login?: ", backend.login(username, password)
821 print "Authenticated: ", backend.is_authed()
822 print "Is Dnd: ", backend.is_dnd()
823 #print "Setting Dnd", backend.set_dnd(True)
824 #print "Is Dnd: ", backend.is_dnd()
825 #print "Setting Dnd", backend.set_dnd(False)
826 #print "Is Dnd: ", backend.is_dnd()
828 #print "Token: ", backend._token
829 #print "Account: ", backend.get_account_number()
830 #print "Callback: ", backend.get_callback_number()
831 #print "All Callback: ",
833 #pprint.pprint(backend.get_callback_numbers())
836 #for data in backend.get_recent():
837 # pprint.pprint(data)
838 #for data in sort_messages(backend.get_recent()):
839 # pprint.pprint(decorate_recent(data))
840 #pprint.pprint(list(backend.get_recent()))
843 #for contact in backend.get_contacts():
845 # pprint.pprint(list(backend.get_contact_details(contact[0])))
848 #for message in backend.get_messages():
849 # message["messageParts"] = list(message["messageParts"])
850 # pprint.pprint(message)
851 #for message in sort_messages(backend.get_messages()):
852 # pprint.pprint(decorate_message(message))
857 def grab_debug_info(username, password):
858 cookieFile = os.path.join(".", "raw_cookies.txt")
860 os.remove(cookieFile)
864 backend = GVDialer(cookieFile)
865 browser = backend._browser
868 ("forward", backend._forwardURL),
869 ("token", backend._tokenURL),
870 ("login", backend._loginURL),
871 ("isdnd", backend._isDndURL),
872 ("contacts", backend._contactsURL),
874 ("account", backend._XML_ACCOUNT_URL),
875 ("voicemail", backend._XML_VOICEMAIL_URL),
876 ("sms", backend._XML_SMS_URL),
878 ("recent", backend._XML_RECENT_URL),
879 ("placed", backend._XML_PLACED_URL),
880 ("recieved", backend._XML_RECEIVED_URL),
881 ("missed", backend._XML_MISSED_URL),
885 print "Grabbing pre-login pages"
886 for name, url in _TEST_WEBPAGES:
888 page = browser.download(url)
889 except StandardError, e:
892 print "\tWriting to file"
893 with open("not_loggedin_%s.txt" % name, "w") as f:
897 print "Attempting login"
898 galxToken = backend._get_token()
899 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
900 with open("loggingin.txt", "w") as f:
901 print "\tWriting to file"
902 f.write(loginSuccessOrFailurePage)
904 backend._grab_account_info(loginSuccessOrFailurePage)
906 # Retry in case the redirect failed
907 # luckily is_authed does everything we need for a retry
908 loggedIn = backend.is_authed(True)
913 print "Grabbing post-login pages"
914 for name, url in _TEST_WEBPAGES:
916 page = browser.download(url)
917 except StandardError, e:
920 print "\tWriting to file"
921 with open("loggedin_%s.txt" % name, "w") as f:
925 browser.cookies.save()
926 print "\tWriting cookies to file"
927 with open("cookies.txt", "w") as f:
929 "%s: %s\n" % (c.name, c.value)
930 for c in browser.cookies
934 if __name__ == "__main__":
936 logging.basicConfig(level=logging.DEBUG)
938 grab_debug_info(sys.argv[1], sys.argv[2])
940 test_backend(sys.argv[1], sys.argv[2])