X-Git-Url: http://git.maemo.org/git/?a=blobdiff_plain;f=src%2Fgvoice%2Fbackend.py;h=0661d0c0af2f164d8e46466eeb5124316f9fd24e;hb=4329f6e4cdab994dbe81ea03e1c36341be37d253;hp=2457af57f67e7f2346ac33ffe70e73e050f0ae85;hpb=d079b698c676f1ef3f05d8b553d9e09c32699a7d;p=theonering diff --git a/src/gvoice/backend.py b/src/gvoice/backend.py index 2457af5..0661d0c 100755 --- a/src/gvoice/backend.py +++ b/src/gvoice/backend.py @@ -48,7 +48,7 @@ except ImportError: import browser_emu -_moduleLogger = logging.getLogger("gvoice.backend") +_moduleLogger = logging.getLogger(__name__) class NetworkError(RuntimeError): @@ -235,6 +235,7 @@ class GVoiceBackend(object): Attempts to detect a current session @note Once logged in try not to reauth more than once a minute. @returns If authenticated + @blocks """ isRecentledAuthed = (time.time() - self._lastAuthed) < 120 isPreviouslyAuthed = self._token is not None @@ -282,6 +283,7 @@ class GVoiceBackend(object): """ Attempt to login to GoogleVoice @returns Whether login was successful or not + @blocks """ self.logout() galxToken = self._get_token() @@ -302,6 +304,11 @@ class GVoiceBackend(object): self._lastAuthed = time.time() return True + def shutdown(self): + self._browser.save_cookies() + self._token = None + self._lastAuthed = 0.0 + def logout(self): self._browser.clear_cookies() self._browser.save_cookies() @@ -309,6 +316,9 @@ class GVoiceBackend(object): self._lastAuthed = 0.0 def is_dnd(self): + """ + @blocks + """ isDndPage = self._get_page(self._isDndURL) dndGroup = self._isDndRe.search(isDndPage) @@ -319,6 +329,9 @@ class GVoiceBackend(object): return isDnd def set_dnd(self, doNotDisturb): + """ + @blocks + """ dndPostData = { "doNotDisturb": 1 if doNotDisturb else 0, } @@ -328,6 +341,7 @@ class GVoiceBackend(object): def call(self, outgoingNumber): """ This is the main function responsible for initating the callback + @blocks """ outgoingNumber = self._send_validation(outgoingNumber) subscriberNumber = None @@ -353,6 +367,7 @@ class GVoiceBackend(object): """ Cancels a call matching outgoing and forwarding numbers (if given). Will raise an error if no matching call is being placed + @blocks """ page = self._get_page_with_token( self._callCancelURL, @@ -364,12 +379,19 @@ class GVoiceBackend(object): ) self._parse_with_validation(page) - def send_sms(self, phoneNumber, message): - phoneNumber = self._send_validation(phoneNumber) + def send_sms(self, phoneNumbers, message): + """ + @blocks + """ + validatedPhoneNumbers = [ + self._send_validation(phoneNumber) + for phoneNumber in phoneNumbers + ] + flattenedPhoneNumbers = ",".join(validatedPhoneNumbers) page = self._get_page_with_token( self._sendSmsURL, { - 'phoneNumber': phoneNumber, + 'phoneNumber': flattenedPhoneNumbers, 'text': message }, ) @@ -379,6 +401,7 @@ class GVoiceBackend(object): """ Search your Google Voice Account history for calls, voicemails, and sms Returns ``Folder`` instance containting matching messages + @blocks """ page = self._get_page( self._XML_SEARCH_URL, @@ -388,6 +411,9 @@ class GVoiceBackend(object): return json def get_feed(self, feed): + """ + @blocks + """ actualFeed = "_XML_%s_URL" % feed.upper() feedUrl = getattr(self, actualFeed) @@ -402,7 +428,8 @@ class GVoiceBackend(object): which can either be a ``Message`` instance, or a SHA1 identifier. Saves files to ``adir`` (defaults to current directory). Message hashes can be found in ``self.voicemail().messages`` for example. - Returns location of saved file. + @returns location of saved file. + @blocks """ page = self._get_page(self._downloadVoicemailURL, {"id": messageId}) fn = os.path.join(adir, '%s.mp3' % messageId) @@ -449,35 +476,30 @@ class GVoiceBackend(object): def get_recent(self): """ @returns Iterable of (personsName, phoneNumber, exact date, relative date, action) + @blocks """ - for action, url in ( - ("Received", self._XML_RECEIVED_URL), - ("Missed", self._XML_MISSED_URL), - ("Placed", self._XML_PLACED_URL), - ): - flatXml = self._get_page(url) - - allRecentHtml = self._grab_html(flatXml) - allRecentData = self._parse_history(allRecentHtml) - for recentCallData in allRecentData: - recentCallData["action"] = action - yield recentCallData + recentPages = [ + (action, self._get_page(url)) + for action, url in ( + ("Received", self._XML_RECEIVED_URL), + ("Missed", self._XML_MISSED_URL), + ("Placed", self._XML_PLACED_URL), + ) + ] + return self._parse_recent(recentPages) def get_contacts(self): """ @returns Iterable of (contact id, contact name) + @blocks """ page = self._get_page(self._XML_CONTACTS_URL) - contactsBody = self._contactsBodyRe.search(page) - if contactsBody is None: - raise RuntimeError("Could not extract contact information") - accountData = _fake_parse_json(contactsBody.group(1)) - for contactId, contactDetails in accountData["contacts"].iteritems(): - # A zero contact id is the catch all for unknown contacts - if contactId != "0": - yield contactId, contactDetails + return self._process_contacts(page) def get_voicemails(self): + """ + @blocks + """ voicemailPage = self._get_page(self._XML_VOICEMAIL_URL) voicemailHtml = self._grab_html(voicemailPage) voicemailJson = self._grab_json(voicemailPage) @@ -486,6 +508,9 @@ class GVoiceBackend(object): return voicemails def get_texts(self): + """ + @blocks + """ smsPage = self._get_page(self._XML_SMS_URL) smsHtml = self._grab_html(smsPage) smsJson = self._grab_json(smsPage) @@ -494,6 +519,9 @@ class GVoiceBackend(object): return smss def mark_message(self, messageId, asRead): + """ + @blocks + """ postData = { "read": 1 if asRead else 0, "id": messageId, @@ -502,6 +530,9 @@ class GVoiceBackend(object): markPage = self._get_page(self._markAsReadURL, postData) def archive_message(self, messageId): + """ + @blocks + """ postData = { "id": messageId, } @@ -546,18 +577,32 @@ class GVoiceBackend(object): raise ValueError('Number is not valid: "%s"' % number) elif not self.is_authed(): raise RuntimeError("Not Authenticated") - - if len(number) == 11 and number[0] == 1: - # Strip leading 1 from 11 digit dialing - number = number[1:] return number + def _parse_recent(self, recentPages): + for action, flatXml in recentPages: + allRecentHtml = self._grab_html(flatXml) + allRecentData = self._parse_history(allRecentHtml) + for recentCallData in allRecentData: + recentCallData["action"] = action + yield recentCallData + + def _process_contacts(self, page): + contactsBody = self._contactsBodyRe.search(page) + if contactsBody is None: + raise RuntimeError("Could not extract contact information") + accountData = _fake_parse_json(contactsBody.group(1)) + for contactId, contactDetails in accountData["contacts"].iteritems(): + # A zero contact id is the catch all for unknown contacts + if contactId != "0": + yield contactId, contactDetails + def _parse_history(self, historyHtml): splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml) for messageId, messageHtml in itergroup(splitVoicemail[1:], 2): exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else "" - exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p") + exactTime = google_strptime(exactTime) relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" locationGroup = self._voicemailLocationRegex.search(messageHtml) @@ -605,7 +650,7 @@ class GVoiceBackend(object): exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else "" - conv.time = datetime.datetime.strptime(exactTimeText, "%m/%d/%y %I:%M %p") + conv.time = google_strptime(exactTimeText) relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" locationGroup = self._voicemailLocationRegex.search(messageHtml) @@ -655,7 +700,7 @@ class GVoiceBackend(object): exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml) exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else "" - conv.time = datetime.datetime.strptime(exactTimeText, "%m/%d/%y %I:%M %p") + conv.time = google_strptime(exactTimeText) relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml) conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else "" conv.location = "" @@ -722,6 +767,19 @@ class GVoiceBackend(object): return json +def google_strptime(time): + """ + Hack: Google always returns the time in the same locale. Sadly if the + local system's locale is different, there isn't a way to perfectly handle + the time. So instead we handle implement some time formatting + """ + abbrevTime = time[:-3] + parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M") + if time[-2] == "PN": + parsedTime += datetime.timedelta(hours=12) + return parsedTime + + def itergroup(iterator, count, padValue = None): """ Iterate in groups of 'count' values. If there @@ -801,7 +859,7 @@ def validate_response(response): def guess_phone_type(number): - if number.startswith("747") or number.startswith("1747"): + if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"): return GVoiceBackend.PHONE_TYPE_GIZMO else: return GVoiceBackend.PHONE_TYPE_MOBILE @@ -818,7 +876,9 @@ def get_sane_callback(backend): numbers = backend.get_callback_numbers() priorityOrderedCriteria = [ + ("\+1747", None), ("1747", None), + ("747", None), (None, "gizmo"), (None, "computer"), (None, "sip"), @@ -826,10 +886,17 @@ def get_sane_callback(backend): ] for numberCriteria, descriptionCriteria in priorityOrderedCriteria: + numberMatcher = None + descriptionMatcher = None + if numberCriteria is not None: + numberMatcher = re.compile(numberCriteria) + elif descriptionCriteria is not None: + descriptionMatcher = re.compile(descriptionCriteria, re.I) + for number, description in numbers.iteritems(): - if numberCriteria is not None and re.compile(numberCriteria).match(number) is None: + if numberMatcher is not None and numberMatcher.match(number) is None: continue - if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None: + if descriptionMatcher is not None and descriptionMatcher.match(description) is None: continue return number