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
255 page = self._get_page_with_token(
258 'outgoingNumber': outgoingNumber,
259 'forwardingNumber': self._callbackNumber,
260 'subscriberNumber': subscriberNumber or 'undefined',
261 'phoneType': phoneType,
265 self._parse_with_validation(page)
268 def cancel(self, outgoingNumber=None):
270 Cancels a call matching outgoing and forwarding numbers (if given).
271 Will raise an error if no matching call is being placed
273 page = self._get_page_with_token(
276 'outgoingNumber': outgoingNumber or 'undefined',
277 'forwardingNumber': self._callbackNumber or 'undefined',
281 self._parse_with_validation(page)
283 def send_sms(self, phoneNumber, message):
284 phoneNumber = self._send_validation(phoneNumber)
285 page = self._get_page_with_token(
288 'phoneNumber': phoneNumber,
292 self._parse_with_validation(page)
294 def search(self, query):
296 Search your Google Voice Account history for calls, voicemails, and sms
297 Returns ``Folder`` instance containting matching messages
299 page = self._get_page(
300 self._XML_SEARCH_URL,
303 json, html = extract_payload(page)
306 def get_feed(self, feed):
307 actualFeed = "_XML_%s_URL" % feed.upper()
308 feedUrl = getattr(self, actualFeed)
310 page = self._get_page(feedUrl)
311 json, html = extract_payload(page)
315 def download(self, messageId, adir):
317 Download a voicemail or recorded call MP3 matching the given ``msg``
318 which can either be a ``Message`` instance, or a SHA1 identifier.
319 Saves files to ``adir`` (defaults to current directory).
320 Message hashes can be found in ``self.voicemail().messages`` for example.
321 Returns location of saved file.
323 page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
324 fn = os.path.join(adir, '%s.mp3' % messageId)
325 with open(fn, 'wb') as fo:
329 def is_valid_syntax(self, number):
331 @returns If This number be called ( syntax validation only )
333 return self._validateRe.match(number) is not None
335 def get_account_number(self):
337 @returns The GoogleVoice phone number
339 return self._accountNum
341 def get_callback_numbers(self):
343 @returns a dictionary mapping call back numbers to descriptions
344 @note These results are cached for 30 minutes.
346 if not self.is_authed():
348 return self._callbackNumbers
350 def set_callback_number(self, callbacknumber):
352 Set the number that GoogleVoice calls
353 @param callbacknumber should be a proper 10 digit number
355 self._callbackNumber = callbacknumber
358 def get_callback_number(self):
360 @returns Current callback number or None
362 return self._callbackNumber
364 def get_recent(self):
366 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
369 ("Received", self._XML_RECEIVED_URL),
370 ("Missed", self._XML_MISSED_URL),
371 ("Placed", self._XML_PLACED_URL),
373 flatXml = self._get_page(url)
375 allRecentHtml = self._grab_html(flatXml)
376 allRecentData = self._parse_voicemail(allRecentHtml)
377 for recentCallData in allRecentData:
378 recentCallData["action"] = action
381 def get_contacts(self):
383 @returns Iterable of (contact id, contact name)
385 contactsPagesUrls = [self._contactsURL]
386 for contactsPageUrl in contactsPagesUrls:
387 contactsPage = self._get_page(contactsPageUrl)
388 for contact_match in self._contactsRe.finditer(contactsPage):
389 contactId = contact_match.group(1)
390 contactName = saxutils.unescape(contact_match.group(2))
391 contact = contactId, contactName
394 next_match = self._contactsNextRe.match(contactsPage)
395 if next_match is not None:
396 newContactsPageUrl = self._contactsURL + next_match.group(1)
397 contactsPagesUrls.append(newContactsPageUrl)
399 def get_contact_details(self, contactId):
401 @returns Iterable of (Phone Type, Phone Number)
403 detailPage = self._get_page(self._contactDetailURL + '/' + contactId)
405 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
406 phoneNumber = detail_match.group(1)
407 phoneType = saxutils.unescape(detail_match.group(2))
408 yield (phoneType, phoneNumber)
410 def get_messages(self):
411 voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
412 voicemailHtml = self._grab_html(voicemailPage)
413 voicemailJson = self._grab_json(voicemailPage)
414 parsedVoicemail = self._parse_voicemail(voicemailHtml)
415 voicemails = self._merge_messages(parsedVoicemail, voicemailJson)
416 decoratedVoicemails = self._decorate_voicemail(voicemails)
418 smsPage = self._get_page(self._XML_SMS_URL)
419 smsHtml = self._grab_html(smsPage)
420 smsJson = self._grab_json(smsPage)
421 parsedSms = self._parse_sms(smsHtml)
422 smss = self._merge_messages(parsedSms, smsJson)
423 decoratedSms = self._decorate_sms(smss)
425 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
428 def clear_caches(self):
431 def get_addressbooks(self):
433 @returns Iterable of (Address Book Factory, Book Id, Book Name)
437 def open_addressbook(self, bookId):
441 def contact_source_short_name(contactId):
446 return "Google Voice"
448 def _grab_json(self, flatXml):
449 xmlTree = ElementTree.fromstring(flatXml)
450 jsonElement = xmlTree.getchildren()[0]
451 flatJson = jsonElement.text
452 jsonTree = parse_json(flatJson)
455 def _grab_html(self, flatXml):
456 xmlTree = ElementTree.fromstring(flatXml)
457 htmlElement = xmlTree.getchildren()[1]
458 flatHtml = htmlElement.text
461 def _grab_account_info(self, page):
462 tokenGroup = self._tokenRe.search(page)
463 if tokenGroup is None:
464 raise RuntimeError("Could not extract authentication token from GoogleVoice")
465 self._token = tokenGroup.group(1)
467 anGroup = self._accountNumRe.search(page)
468 if anGroup is not None:
469 self._accountNum = anGroup.group(1)
471 _moduleLogger.debug("Could not extract account number from GoogleVoice")
473 self._callbackNumbers = {}
474 for match in self._callbackRe.finditer(page):
475 callbackNumber = match.group(2)
476 callbackName = match.group(1)
477 self._callbackNumbers[callbackNumber] = callbackName
478 if len(self._callbackNumbers) == 0:
479 _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
481 def _send_validation(self, number):
482 if not self.is_valid_syntax(number):
483 raise ValueError('Number is not valid: "%s"' % number)
484 elif not self.is_authed():
485 raise RuntimeError("Not Authenticated")
487 if len(number) == 11 and number[0] == 1:
488 # Strip leading 1 from 11 digit dialing
493 def _interpret_voicemail_regex(group):
494 quality, content, number = group.group(2), group.group(3), group.group(4)
495 if quality is not None and content is not None:
496 return quality, content
497 elif number is not None:
498 return "high", number
500 def _parse_voicemail(self, voicemailHtml):
501 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
502 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
503 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
504 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
505 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
506 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
507 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
508 locationGroup = self._voicemailLocationRegex.search(messageHtml)
509 location = locationGroup.group(1).strip() if locationGroup else ""
511 nameGroup = self._voicemailNameRegex.search(messageHtml)
512 name = nameGroup.group(1).strip() if nameGroup else ""
513 numberGroup = self._voicemailNumberRegex.search(messageHtml)
514 number = numberGroup.group(1).strip() if numberGroup else ""
515 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
516 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
517 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
518 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
520 messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
522 self._interpret_voicemail_regex(group)
523 for group in messageGroups
524 ) if messageGroups else ()
527 "id": messageId.strip(),
528 "contactId": contactId,
531 "relTime": relativeTime,
532 "prettyNumber": prettyNumber,
534 "location": location,
535 "messageParts": messageParts,
539 def _decorate_voicemail(self, parsedVoicemails):
540 messagePartFormat = {
545 for voicemailData in parsedVoicemails:
547 messagePartFormat[quality] % part
548 for (quality, part) in voicemailData["messageParts"]
551 message = "No Transcription"
552 whoFrom = voicemailData["name"]
553 when = voicemailData["time"]
554 voicemailData["messageParts"] = ((whoFrom, message, when), )
557 def _parse_sms(self, smsHtml):
558 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
559 for messageId, messageHtml in itergroup(splitSms[1:], 2):
560 exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
561 exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
562 exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
563 relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
564 relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
566 nameGroup = self._voicemailNameRegex.search(messageHtml)
567 name = nameGroup.group(1).strip() if nameGroup else ""
568 numberGroup = self._voicemailNumberRegex.search(messageHtml)
569 number = numberGroup.group(1).strip() if numberGroup else ""
570 prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
571 prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
572 contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
573 contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
575 fromGroups = self._smsFromRegex.finditer(messageHtml)
576 fromParts = (group.group(1).strip() for group in fromGroups)
577 textGroups = self._smsTextRegex.finditer(messageHtml)
578 textParts = (group.group(1).strip() for group in textGroups)
579 timeGroups = self._smsTimeRegex.finditer(messageHtml)
580 timeParts = (group.group(1).strip() for group in timeGroups)
582 messageParts = itertools.izip(fromParts, textParts, timeParts)
585 "id": messageId.strip(),
586 "contactId": contactId,
589 "relTime": relativeTime,
590 "prettyNumber": prettyNumber,
593 "messageParts": messageParts,
597 def _decorate_sms(self, parsedTexts):
601 def _merge_messages(parsedMessages, json):
602 for message in parsedMessages:
604 jsonItem = json["messages"][id]
605 message["isRead"] = jsonItem["isRead"]
606 message["isSpam"] = jsonItem["isSpam"]
607 message["isTrash"] = jsonItem["isTrash"]
608 message["isArchived"] = "inbox" not in jsonItem["labels"]
611 def _get_page(self, url, data = None, refererUrl = None):
613 if refererUrl is not None:
614 headers["Referer"] = refererUrl
616 encodedData = urllib.urlencode(data) if data is not None else None
619 page = self._browser.download(url, encodedData, None, headers)
620 except urllib2.URLError, e:
621 _moduleLogger.error("Translating error: %s" % str(e))
622 raise NetworkError("%s is not accesible" % url)
626 def _get_page_with_token(self, url, data = None, refererUrl = None):
629 data['_rnr_se'] = self._token
631 page = self._get_page(url, data, refererUrl)
635 def _parse_with_validation(self, page):
636 json, html = extract_payload(page)
637 validate_response(json)
641 def itergroup(iterator, count, padValue = None):
643 Iterate in groups of 'count' values. If there
644 aren't enough values, the last result is padded with
647 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
651 >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
655 >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
660 >>> for val in itergroup("123456", 3):
664 >>> for val in itergroup("123456", 3):
665 ... print repr("".join(val))
669 paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
670 nIterators = (paddedIterator, ) * count
671 return itertools.izip(*nIterators)
675 _TRUE_REGEX = re.compile("true")
676 _FALSE_REGEX = re.compile("false")
677 s = _TRUE_REGEX.sub("True", s)
678 s = _FALSE_REGEX.sub("False", s)
679 return eval(s, {}, {})
682 def _fake_parse_json(flattened):
683 return safe_eval(flattened)
686 def _actual_parse_json(flattened):
687 return simplejson.loads(flattened)
690 if simplejson is None:
691 parse_json = _fake_parse_json
693 parse_json = _actual_parse_json
696 def extract_payload(flatXml):
697 xmlTree = ElementTree.fromstring(flatXml)
699 jsonElement = xmlTree.getchildren()[0]
700 flatJson = jsonElement.text
701 jsonTree = parse_json(flatJson)
703 htmlElement = xmlTree.getchildren()[1]
704 flatHtml = htmlElement.text
706 return jsonTree, flatHtml
709 def validate_response(response):
711 Validates that the JSON response is A-OK
714 assert 'ok' in response and response['ok']
715 except AssertionError:
716 raise RuntimeError('There was a problem with GV: %s' % response)
719 def guess_phone_type(number):
720 if number.startswith("747") or number.startswith("1747"):
721 return GVDialer.PHONE_TYPE_GIZMO
723 return GVDialer.PHONE_TYPE_MOBILE
726 def set_sane_callback(backend):
728 Try to set a sane default callback number on these preferences
729 1) 1747 numbers ( Gizmo )
730 2) anything with gizmo in the name
731 3) anything with computer in the name
734 numbers = backend.get_callback_numbers()
736 priorityOrderedCriteria = [
744 for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
745 for number, description in numbers.iteritems():
746 if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
748 if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
750 backend.set_callback_number(number)
754 def sort_messages(allMessages):
755 sortableAllMessages = [
756 (message["time"], message)
757 for message in allMessages
759 sortableAllMessages.sort(reverse=True)
762 for (exactTime, message) in sortableAllMessages
766 def decorate_recent(recentCallData):
768 @returns (personsName, phoneNumber, date, action)
770 contactId = recentCallData["contactId"]
771 if recentCallData["name"]:
772 header = recentCallData["name"]
773 elif recentCallData["prettyNumber"]:
774 header = recentCallData["prettyNumber"]
775 elif recentCallData["location"]:
776 header = recentCallData["location"]
780 number = recentCallData["number"]
781 relTime = recentCallData["relTime"]
782 action = recentCallData["action"]
783 return contactId, header, number, relTime, action
786 def decorate_message(messageData):
787 contactId = messageData["contactId"]
788 exactTime = messageData["time"]
789 if messageData["name"]:
790 header = messageData["name"]
791 elif messageData["prettyNumber"]:
792 header = messageData["prettyNumber"]
795 number = messageData["number"]
796 relativeTime = messageData["relTime"]
798 messageParts = list(messageData["messageParts"])
799 if len(messageParts) == 0:
800 messages = ("No Transcription", )
801 elif len(messageParts) == 1:
802 messages = (messageParts[0][1], )
805 "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
806 for messagePart in messageParts
809 decoratedResults = contactId, header, number, relativeTime, messages
810 return decoratedResults
813 def test_backend(username, password):
815 print "Authenticated: ", backend.is_authed()
816 if not backend.is_authed():
817 print "Login?: ", backend.login(username, password)
818 print "Authenticated: ", backend.is_authed()
819 print "Is Dnd: ", backend.is_dnd()
820 #print "Setting Dnd", backend.set_dnd(True)
821 #print "Is Dnd: ", backend.is_dnd()
822 #print "Setting Dnd", backend.set_dnd(False)
823 #print "Is Dnd: ", backend.is_dnd()
825 #print "Token: ", backend._token
826 #print "Account: ", backend.get_account_number()
827 #print "Callback: ", backend.get_callback_number()
828 #print "All Callback: ",
830 #pprint.pprint(backend.get_callback_numbers())
833 #for data in backend.get_recent():
834 # pprint.pprint(data)
835 #for data in sort_messages(backend.get_recent()):
836 # pprint.pprint(decorate_recent(data))
837 #pprint.pprint(list(backend.get_recent()))
840 #for contact in backend.get_contacts():
842 # pprint.pprint(list(backend.get_contact_details(contact[0])))
845 #for message in backend.get_messages():
846 # message["messageParts"] = list(message["messageParts"])
847 # pprint.pprint(message)
848 #for message in sort_messages(backend.get_messages()):
849 # pprint.pprint(decorate_message(message))
854 def grab_debug_info(username, password):
855 cookieFile = os.path.join(".", "raw_cookies.txt")
857 os.remove(cookieFile)
861 backend = GVDialer(cookieFile)
862 browser = backend._browser
865 ("forward", backend._forwardURL),
866 ("token", backend._tokenURL),
867 ("login", backend._loginURL),
868 ("isdnd", backend._isDndURL),
869 ("contacts", backend._contactsURL),
871 ("account", backend._XML_ACCOUNT_URL),
872 ("voicemail", backend._XML_VOICEMAIL_URL),
873 ("sms", backend._XML_SMS_URL),
875 ("recent", backend._XML_RECENT_URL),
876 ("placed", backend._XML_PLACED_URL),
877 ("recieved", backend._XML_RECEIVED_URL),
878 ("missed", backend._XML_MISSED_URL),
882 print "Grabbing pre-login pages"
883 for name, url in _TEST_WEBPAGES:
885 page = browser.download(url)
886 except StandardError, e:
889 print "\tWriting to file"
890 with open("not_loggedin_%s.txt" % name, "w") as f:
894 print "Attempting login"
895 galxToken = backend._get_token()
896 loginSuccessOrFailurePage = backend._login(username, password, galxToken)
897 with open("loggingin.txt", "w") as f:
898 print "\tWriting to file"
899 f.write(loginSuccessOrFailurePage)
901 backend._grab_account_info(loginSuccessOrFailurePage)
903 # Retry in case the redirect failed
904 # luckily is_authed does everything we need for a retry
905 loggedIn = backend.is_authed(True)
910 print "Grabbing post-login pages"
911 for name, url in _TEST_WEBPAGES:
913 page = browser.download(url)
914 except StandardError, e:
917 print "\tWriting to file"
918 with open("loggedin_%s.txt" % name, "w") as f:
922 browser.cookies.save()
923 print "\tWriting cookies to file"
924 with open("cookies.txt", "w") as f:
926 "%s: %s\n" % (c.name, c.value)
927 for c in browser.cookies
931 if __name__ == "__main__":
933 logging.basicConfig(level=logging.DEBUG)
935 grab_debug_info(sys.argv[1], sys.argv[2])
937 test_backend(sys.argv[1], sys.argv[2])