Lots more bug fixes
authorEd Page <eopage@byu.net>
Fri, 18 Dec 2009 05:18:11 +0000 (23:18 -0600)
committerEd Page <eopage@byu.net>
Fri, 18 Dec 2009 05:18:11 +0000 (23:18 -0600)
I switched how I implemented the addressbook so I don't feel bad about a
separate thread for the state machine.

Had strange issues with using a better contact source, so hacks are in
place.

I switched the state machine away from threads because I consistent
issues with the thread being inactive as long as the main thread was
active.

As all this got implemented, I found weird object binding issues.  For
soem reason, self wasn't binding for the callbacks but instead I was
getting the other side's object.  I put in some hacks to work around
this issue

src/channel/contact_list.py
src/channel/text.py
src/connection.py
src/gvoice/addressbook.py
src/gvoice/backend.py
src/gvoice/browser_emu.py
src/gvoice/state_machine.py
tests/test_addressbook.py

index a66e4cd..f82c090 100644 (file)
@@ -31,8 +31,13 @@ class AllContactsListChannel(AbstractListChannel):
 
        def __init__(self, connection, h):
                AbstractListChannel.__init__(self, connection, h)
+               self._callback = coroutines.func_sink(
+                       coroutines.expand_positional(
+                               self._on_contacts_refreshed
+                       )
+               )
                self._session.addressbook.updateSignalHandler.register_sink(
-                       self._on_contacts_refreshed
+                       self._callback
                )
                self.GroupFlagsChanged(0, 0)
 
@@ -45,11 +50,9 @@ class AllContactsListChannel(AbstractListChannel):
                telepathy.server.ChannelTypeContactList.Close(self)
                self.remove_from_connection()
                self._session.addressbook.updateSignalHandler.unregister_sink(
-                       self._on_contacts_refreshed
+                       self._callback
                )
 
-       @coroutines.func_sink
-       @coroutines.expand_positional
        @gobject_utils.async
        @gtk_toolbox.log_exception(_moduleLogger)
        def _on_contacts_refreshed(self, addressbook, added, removed, changed):
index b9f6fe5..050b6a8 100644 (file)
@@ -24,8 +24,13 @@ class TextChannel(telepathy.server.ChannelTypeText):
 
                self._otherHandle = h
 
+               self._callback = coroutines.func_sink(
+                       coroutines.expand_positional(
+                               self._on_conversations_updated
+                       )
+               )
                self._conn.session.conversations.updateSignalHandler.register_sink(
-                       self._on_conversations_updated
+                       self._callback
                )
 
                # The only reason there should be anything in the conversation is if
@@ -53,7 +58,7 @@ class TextChannel(telepathy.server.ChannelTypeText):
                        self._conn.session.conversations.clear_conversation(self._contactKey)
 
                        self._conn.session.conversations.updateSignalHandler.unregister_sink(
-                               self._on_conversations_updated
+                               self._callback
                        )
                finally:
                        telepathy.server.ChannelTypeText.Close(self)
@@ -64,11 +69,9 @@ class TextChannel(telepathy.server.ChannelTypeText):
                contactKey = self._otherHandle.contactID, self._otherHandle.phoneNumber
                return contactKey
 
-       @coroutines.func_sink
-       @coroutines.expand_positional
        @gobject_utils.async
        @gtk_toolbox.log_exception(_moduleLogger)
-       def _on_conversations_updated(self, conversationIds):
+       def _on_conversations_updated(self, conv, conversationIds):
                if self._contactKey not in conversationIds:
                        return
                _moduleLogger.info("Incoming messages from %r for existing conversation" % (self._contactKey, ))
@@ -84,11 +87,11 @@ class TextChannel(telepathy.server.ChannelTypeText):
                self._report_new_message(formattedMessage)
 
        def _filter_seen_messages(self, messages):
-               return (
+               return [
                        message
                        for message in messages
                        if self._lastMessageTimestamp < message[0]
-               )
+               ]
 
        def _format_messages(self, messages):
                return "\n".join(message[1] for message in messages)
@@ -98,9 +101,8 @@ class TextChannel(telepathy.server.ChannelTypeText):
 
                timestamp = int(time.time())
                type = telepathy.CHANNEL_TEXT_MESSAGE_TYPE_NORMAL
-               message = message.content
 
                _moduleLogger.info("Received message from User %r" % self._otherHandle)
-               self.Received(id, timestamp, self._otherHandle, type, 0, message)
+               self.Received(currentReceivedId, timestamp, self._otherHandle, type, 0, message)
 
                self._nextRecievedId += 1
index fa87b31..69ed50c 100644 (file)
@@ -64,6 +64,11 @@ class TheOneRingConnection(
 
                        self.set_self_handle(handle.create_handle(self, 'connection'))
 
+                       self._callback = coroutines.func_sink(
+                               coroutines.expand_positional(
+                                       self._on_conversations_updated
+                               )
+                       )
                        _moduleLogger.info("Connection to the account %s created" % account)
                except Exception, e:
                        _moduleLogger.exception("Failed to create Connection")
@@ -97,7 +102,7 @@ class TheOneRingConnection(
                )
                try:
                        self.session.conversations.updateSignalHandler.register_sink(
-                               self._on_conversations_updated
+                               self._callback
                        )
                        self.session.login(*self._credentials)
                        self.session.backend.set_callback_number(self._callbackNumber)
@@ -129,7 +134,7 @@ class TheOneRingConnection(
                _moduleLogger.info("Disconnecting")
                try:
                        self.session.conversations.updateSignalHandler.unregister_sink(
-                               self._on_conversations_updated
+                               self._callback
                        )
                        self._channelManager.close()
                        self.session.logout()
@@ -205,17 +210,15 @@ class TheOneRingConnection(
                h = handle.create_handle(self, 'contact', requestedContactId, requestedContactNumber)
                return h
 
-       @coroutines.func_sink
-       @coroutines.expand_positional
        @gobject_utils.async
        @gtk_toolbox.log_exception(_moduleLogger)
-       def _on_conversations_updated(self, conversationIds):
+       def _on_conversations_updated(self, conv, conversationIds):
                # @todo get conversations update running
                # @todo test conversatiuons
                _moduleLogger.info("Incoming messages from: %r" % (conversationIds, ))
                channelManager = self._channelManager
                for contactId, phoneNumber in conversationIds:
-                       h = self._create_contact_handle(contactId, phoneNumber)
+                       h = handle.create_handle(self, 'contact', contactId, phoneNumber)
                        # if its new, __init__ will take care of things
                        # if its old, its own update will take care of it
-                       channel = channelManager.channel_for_text(handle)
+                       channel = channelManager.channel_for_text(h)
index 4684ed1..f9f713b 100644 (file)
@@ -56,8 +56,13 @@ class Addressbook(object):
                if self._contacts:
                        return
                contacts = self._backend.get_contacts()
-               for contactId, contactName in contacts:
-                       self._contacts[contactId] = (contactName, [])
+               for contactId, contactDetails in contacts:
+                       contactName = contactDetails["name"]
+                       contactNumbers = [
+                               (numberDetails.get("phoneType", "Mobile"), numberDetails["phoneNumber"])
+                               for numberDetails in contactDetails["numbers"]
+                       ]
+                       self._contacts[contactId] = (contactName, contactNumbers)
 
        def _populate_contact_details(self, contactId):
                if self._get_contact_details(contactId):
index 435ebb8..ad3a20e 100755 (executable)
@@ -98,7 +98,8 @@ class GVoiceBackend(object):
                self._downloadVoicemailURL = SECURE_URL_BASE + "media/send_voicemail/"
 
                self._XML_SEARCH_URL = SECURE_URL_BASE + "inbox/search/"
-               self._XML_ACCOUNT_URL = SECURE_URL_BASE + "inbox/contacts/"
+               self._XML_ACCOUNT_URL = SECURE_URL_BASE + "contacts/"
+               self._XML_CONTACTS_URL = "http://www.google.com/voice/inbox/search/contact"
                self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
 
                self.XML_FEEDS = (
@@ -117,18 +118,12 @@ class GVoiceBackend(object):
                self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
                self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
 
-               self._contactsURL = SECURE_MOBILE_URL_BASE + "contacts"
-               self._contactDetailURL = SECURE_MOBILE_URL_BASE + "contact"
-
                self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
                self._tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
                self._accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
                self._callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
 
-               self._contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
-               self._contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
-               self._contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
-
+               self._contactsBodyRe = re.compile(r"""gcData\s*=\s*({.*?});""", re.MULTILINE | re.DOTALL)
                self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
                self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
                self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
@@ -385,30 +380,13 @@ class GVoiceBackend(object):
                """
                @returns Iterable of (contact id, contact name)
                """
-               contactsPagesUrls = [self._contactsURL]
-               for contactsPageUrl in contactsPagesUrls:
-                       contactsPage = self._get_page(contactsPageUrl)
-                       for contact_match in self._contactsRe.finditer(contactsPage):
-                               contactId = contact_match.group(1)
-                               contactName = saxutils.unescape(contact_match.group(2))
-                               contact = contactId, contactName
-                               yield contact
-
-                       next_match = self._contactsNextRe.match(contactsPage)
-                       if next_match is not None:
-                               newContactsPageUrl = self._contactsURL + next_match.group(1)
-                               contactsPagesUrls.append(newContactsPageUrl)
-
-       def get_contact_details(self, contactId):
-               """
-               @returns Iterable of (Phone Type, Phone Number)
-               """
-               detailPage = self._get_page(self._contactDetailURL + '/' + contactId)
-
-               for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
-                       phoneNumber = detail_match.group(1)
-                       phoneType = saxutils.unescape(detail_match.group(2))
-                       yield (phoneType, phoneNumber)
+               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():
+                       yield contactId, contactDetails
 
        def get_messages(self):
                voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
@@ -799,7 +777,7 @@ def test_backend(username, password):
        if not backend.is_authed():
                print "Login?: ", backend.login(username, password)
        print "Authenticated: ", backend.is_authed()
-       print "Is Dnd: ", backend.is_dnd()
+       #print "Is Dnd: ", backend.is_dnd()
        #print "Setting Dnd", backend.set_dnd(True)
        #print "Is Dnd: ", backend.is_dnd()
        #print "Setting Dnd", backend.set_dnd(False)
@@ -809,7 +787,7 @@ def test_backend(username, password):
        #print "Account: ", backend.get_account_number()
        #print "Callback: ", backend.get_callback_number()
        #print "All Callback: ",
-       #import pprint
+       import pprint
        #pprint.pprint(backend.get_callback_numbers())
 
        #print "Recent: "
@@ -819,10 +797,9 @@ def test_backend(username, password):
        #       pprint.pprint(decorate_recent(data))
        #pprint.pprint(list(backend.get_recent()))
 
-       #print "Contacts: ",
-       #for contact in backend.get_contacts():
-       #       print contact
-       #       pprint.pprint(list(backend.get_contact_details(contact[0])))
+       print "Contacts: ",
+       for contact in backend.get_contacts():
+               pprint.pprint(contact)
 
        #print "Messages: ",
        #for message in backend.get_messages():
@@ -848,9 +825,9 @@ def grab_debug_info(username, password):
                ("token", backend._tokenURL),
                ("login", backend._loginURL),
                ("isdnd", backend._isDndURL),
-               ("contacts", backend._contactsURL),
-
                ("account", backend._XML_ACCOUNT_URL),
+               ("contacts", backend._XML_CONTACTS_URL),
+
                ("voicemail", backend._XML_VOICEMAIL_URL),
                ("sms", backend._XML_SMS_URL),
 
@@ -894,19 +871,19 @@ def grab_debug_info(username, password):
                try:
                        page = browser.download(url)
                except StandardError, e:
-                       print e.message
+                       print str(e)
                        continue
                print "\tWriting to file"
                with open("loggedin_%s.txt" % name, "w") as f:
                        f.write(page)
 
        # Cookies
-       browser.cookies.save()
+       browser.save_cookies()
        print "\tWriting cookies to file"
        with open("cookies.txt", "w") as f:
                f.writelines(
                        "%s: %s\n" % (c.name, c.value)
-                       for c in browser.cookies
+                       for c in browser._cookies
                )
 
 
index 108fb5c..d574e6a 100644 (file)
@@ -115,13 +115,14 @@ class MozillaEmulator(object):
                                        return openerdirector
 
                                return self._read(openerdirector, trycount)
-                       except urllib2.URLError:
+                       except urllib2.URLError, e:
+                               _moduleLogger.info("%s: %s" % (e, url))
                                cnt += 1
                                if (-1 < trycount) and (trycount < cnt):
                                        raise
 
                        # Retry :-)
-                       _moduleLogger.info("MozillaEmulator: urllib2.URLError, retryting %d" % cnt)
+                       _moduleLogger.info("MozillaEmulator: urllib2.URLError, retrying %d" % cnt)
 
        def _build_opener(self, url, postdata = None, extraheaders = None, forbidRedirect = False):
                if extraheaders is None:
index 8e81cb7..b24633f 100644 (file)
@@ -6,13 +6,14 @@
 """
 
 import Queue
-import threading
 import logging
 
 import gobject
 
 import util.algorithms as algorithms
+import util.go_utils as gobject_utils
 import util.coroutines as coroutines
+import gtk_toolbox
 
 
 _moduleLogger = logging.getLogger("gvoice.state_machine")
@@ -49,25 +50,31 @@ class StateMachine(object):
                self._initItems = initItems
                self._updateItems = updateItems
 
-               self._actions = Queue.Queue()
                self._state = self.STATE_ACTIVE
                self._timeoutId = None
-               self._thread = None
                self._currentPeriod = self._INITIAL_ACTIVE_PERIOD
                self._set_initial_period()
 
+               self._callback = coroutines.func_sink(
+                       coroutines.expand_positional(
+                               self._request_reset_timers
+                       )
+               )
+
+       @gobject_utils.async
+       @gtk_toolbox.log_exception(_moduleLogger)
        def start(self):
-               assert self._thread is None
-               self._thread = threading.Thread(target=self._run)
-               self._thread.setDaemon(self._IS_DAEMON)
-               self._thread.start()
+               _moduleLogger.info("Starting State Machine")
+               for item in self._initItems:
+                       try:
+                               item.update()
+                       except Exception:
+                               _moduleLogger.exception("Initial update failed for %r" % item)
+               self._schedule_update()
 
        def stop(self):
-               if self._thread is not None:
-                       self._actions.put(self._ACTION_STOP)
-                       self._thread = None
-               else:
-                       _moduleLogger.info("Stopping an already stopped state machine")
+               _moduleLogger.info("Stopping an already stopped state machine")
+               self._stop_update()
 
        def set_state(self, state):
                self._state = state
@@ -77,55 +84,20 @@ class StateMachine(object):
                return self._state
 
        def reset_timers(self):
-               self._actions.put(self._ACTION_RESET)
+               self._reset_timers()
 
-       @coroutines.func_sink
-       def request_reset_timers(self, args):
-               self.reset_timers()
+       @property
+       def request_reset_timers(self):
+               return self._callback
 
-       def _run(self):
-               logging.basicConfig(level=logging.DEBUG)
-               _moduleLogger.info("Starting State Machine")
-               for item in self._initItems:
-                       try:
-                               item.update()
-                       except Exception:
-                               _moduleLogger.exception("Initial update failed for %r" % item)
-
-               # empty the task queue
-               actions = list(algorithms.itr_available(self._actions, initiallyBlock = False))
-               self._schedule_update()
-               if len(self._updateItems) == 0:
-                       self.stop()
-
-               while True:
-                       # block till we get a task, or get all the tasks if we were late 
-                       actions = list(algorithms.itr_available(self._actions, initiallyBlock = True))
-
-                       if self._ACTION_STOP in actions:
-                               _moduleLogger.info("Requested to stop")
-                               self._stop_update()
-                               break
-                       elif self._ACTION_RESET in actions:
-                               _moduleLogger.info("Reseting timers")
-                               self._reset_timers()
-                       elif self._ACTION_UPDATE in actions:
-                               _moduleLogger.info("Update")
-                               for item in self._updateItems:
-                                       try:
-                                               item.update(force=True)
-                                       except Exception:
-                                               _moduleLogger.exception("Update failed for %r" % item)
-                               self._schedule_update()
+       @gobject_utils.async
+       @gtk_toolbox.log_exception(_moduleLogger)
+       def _request_reset_timers(self, *args):
+               self.reset_timers()
 
        def _set_initial_period(self):
                self._currentPeriod = self._INITIAL_ACTIVE_PERIOD / 2 # We will double it later
 
-       def _reset_timers(self):
-               self._stop_update()
-               self._set_initial_period()
-               self._schedule_update()
-
        def _schedule_update(self):
                nextTimeout = self._calculate_step(self._state, self._currentPeriod)
                nextTimeout = int(nextTimeout)
@@ -139,8 +111,19 @@ class StateMachine(object):
                gobject.source_remove(self._timeoutId)
                self._timeoutId = None
 
+       def _reset_timers(self):
+               self._stop_update()
+               self._set_initial_period()
+               self._schedule_update()
+
        def _on_timeout(self):
-               self._actions.put(self._ACTION_UPDATE)
+               _moduleLogger.info("Update")
+               for item in self._updateItems:
+                       try:
+                               item.update(force=True)
+                       except Exception:
+                               _moduleLogger.exception("Update failed for %r" % item)
+               self._schedule_update()
                return False # do not continue
 
        @classmethod
index 318126d..0702c1b 100644 (file)
@@ -20,13 +20,10 @@ class MockBackend(object):
 
        def get_contacts(self):
                return (
-                       (i, contactData["name"])
+                       (i, contactData)
                        for (i, contactData) in enumerate(self.contactsData)
                )
 
-       def get_contact_details(self, contactId):
-               return self.contactsData[contactId]["details"]
-
 
 def generate_update_callback(callbackData):
 
@@ -64,7 +61,7 @@ def test_one_contact_no_details():
        backend = MockBackend([
                {
                        "name": "One",
-                       "details": [],
+                       "numbers": [],
                },
        ])
        book = gvoice.addressbook.Addressbook(backend)
@@ -95,9 +92,6 @@ def test_one_contact_no_details():
        name = book.get_contact_name(id)
        assert name == backend.contactsData[id]["name"]
 
-       contactDetails = list(book.get_contact_details(id))
-       assert len(contactDetails) == 0
-
 
 def test_one_contact_with_details():
        callbackData = []
@@ -106,7 +100,10 @@ def test_one_contact_with_details():
        backend = MockBackend([
                {
                        "name": "One",
-                       "details": [("Type A", "123"), ("Type B", "456"), ("Type C", "789")],
+                       "numbers": [
+                               {"phoneType": "Type A", "phoneNumber": "123"},
+                               {"phoneType": "Type B", "phoneNumber": "456"},
+                               {"phoneType": "Type C", "phoneNumber": "789"}],
                },
        ])
        book = gvoice.addressbook.Addressbook(backend)
@@ -137,16 +134,6 @@ def test_one_contact_with_details():
        name = book.get_contact_name(id)
        assert name == backend.contactsData[id]["name"]
 
-       contactDetails = list(book.get_contact_details(id))
-       print "%r" % contactDetails
-       assert len(contactDetails) == 3
-       assert contactDetails[0][0] == "Type A"
-       assert contactDetails[0][1] == "123"
-       assert contactDetails[1][0] == "Type B"
-       assert contactDetails[1][1] == "456"
-       assert contactDetails[2][0] == "Type C"
-       assert contactDetails[2][1] == "789"
-
 
 def test_adding_a_contact():
        callbackData = []
@@ -155,7 +142,7 @@ def test_adding_a_contact():
        backend = MockBackend([
                {
                        "name": "One",
-                       "details": [],
+                       "numbers": [],
                },
        ])
        book = gvoice.addressbook.Addressbook(backend)
@@ -174,7 +161,7 @@ def test_adding_a_contact():
 
        backend.contactsData.append({
                "name": "Two",
-               "details": [],
+               "numbers": [],
        })
 
        book.update()
@@ -198,7 +185,7 @@ def test_removing_a_contact():
        backend = MockBackend([
                {
                        "name": "One",
-                       "details": [],
+                       "numbers": [],
                },
        ])
        book = gvoice.addressbook.Addressbook(backend)