deskbar-applet r2289 - in trunk: . deskbar/core deskbar/handlers



Author: kamstrup
Date: Tue Aug  5 21:04:11 2008
New Revision: 2289
URL: http://svn.gnome.org/viewvc/deskbar-applet?rev=2289&view=rev

Log:
    * deskbar/core/Utils.py
    Add method load_base64_icon. Handy for inlining icons inside modules
    for easier distribution
    
	* deskbar/handlers/twitter.py
    * deskbar/handlers/Makefile.am
    * deskbar/core/Web.py
    * deskbar/core/Makefile.am
    Introduce a Twitter micro blogging module
    
    Introduce a new utility package deskbar.core.Web with tools for dealing
    with online services. Mostly the authentication parts. Highlights include:
    
     - Abstraction of online account storing credentials in gnome keyring
     - A dialog for managing an account
     - An asynchronous url opener that hooks into the account framework
       mentioned above to ask for credentials


Added:
   trunk/deskbar/core/Web.py
   trunk/deskbar/handlers/twitter.py
Modified:
   trunk/ChangeLog
   trunk/deskbar/core/Makefile.am
   trunk/deskbar/core/Utils.py
   trunk/deskbar/handlers/Makefile.am

Modified: trunk/deskbar/core/Makefile.am
==============================================================================
--- trunk/deskbar/core/Makefile.am	(original)
+++ trunk/deskbar/core/Makefile.am	Tue Aug  5 21:04:11 2008
@@ -15,4 +15,5 @@
 	ModuleLoader.py \
 	ThreadPool.py \
 	Utils.py \
-	Watcher.py
+	Watcher.py \
+	Web.py

Modified: trunk/deskbar/core/Utils.py
==============================================================================
--- trunk/deskbar/core/Utils.py	(original)
+++ trunk/deskbar/core/Utils.py	Tue Aug  5 21:04:11 2008
@@ -15,6 +15,7 @@
 import logging
 import os
 import re
+import base64
 
 LOGGER = logging.getLogger(__name__)
 
@@ -125,6 +126,32 @@
 def load_icon_from_icon_theme(iconname, size):
     return ICON_THEME.load_icon(iconname, size, gtk.ICON_LOOKUP_USE_BUILTIN)
 
+def load_base64_icon (base64_str):
+    """
+    Load a base64 encoded image as a C{gtk.gdk.Pixbuf}.
+    
+    @param base64_str: a C{string} with a base64 encoded image
+    @return: A C{gtk.gdk.Pixbuf} or a fallback icon in case there are errors
+        parsing C{base64_str}.
+    """
+    loader = gtk.gdk.PixbufLoader()
+    
+    try:
+        loader.set_size(deskbar.ICON_HEIGHT, deskbar.ICON_HEIGHT)
+        loader.write(base64.b64decode(base64_str))
+    except Exception, e:
+        LOGGER.warning ("Failed to read base64 encoded image: %s" % e)
+    except gobject.GError, ee:
+        LOGGER.warning ("Failed to read base64 encoded image: %s" % ee)
+    finally:
+        loader.close()
+        
+    pixbuf = loader.get_pixbuf()
+    if pixbuf :
+        return pixbuf
+    
+    return _get_fall_back_icon()
+
 def _get_fall_back_icon():
     """
     @return: stock_unknown icon or C{None}

Added: trunk/deskbar/core/Web.py
==============================================================================
--- (empty file)
+++ trunk/deskbar/core/Web.py	Tue Aug  5 21:04:11 2008
@@ -0,0 +1,280 @@
+from deskbar.defs import VERSION
+from gettext import gettext as _
+import base64
+import deskbar
+import gtk
+import gobject
+import logging
+import threading
+import re
+import urllib
+import gnomekeyring
+
+LOGGER = logging.getLogger(__name__)
+
+class Account :
+    """
+    This is an abstraction used to make it easier to move
+    away from a GConf password storage solution (Seahorse anyone?)
+    
+    WARNING: This API is synchronous. This does not matter much to deskbar since
+             web based modules will likely run in threads anyway.
+    
+    This class is based on work found in Sebastian Rittau's blog
+    found on http://www.rittau.org/blog/20070726-00. Copied with permission.
+    """
+    def __init__(self, host, realm):
+        self._realm = realm
+        self._host = host
+        self._protocol = "http"
+        self._keyring = gnomekeyring.get_default_keyring_sync()
+
+    def has_credentials(self):
+        """
+        @returns: True if and only if the credentials for this account is known
+        """
+        try:
+            attrs = {"server": self._host, "protocol": self._protocol}
+            items = gnomekeyring.find_items_sync(gnomekeyring.ITEM_NETWORK_PASSWORD, attrs)
+            if len(items) > 0 :
+                if items[0].attributes["user"] != "" and \
+                   items[0].secret != "" :
+                   return True
+                else :
+                    return False
+        except gnomekeyring.DeniedError:
+            return False
+        except gnomekeyring.NoMatchError:
+            return False
+    
+    def get_host (self):
+        return self._host
+    
+    def get_realm (self):
+        return self._realm
+    
+    def get_credentials(self):
+        """
+        @return: A tuple C{(user, password)} or throws an exception if the
+            credentials for the account are not known
+        """
+        attrs = {"server": self._host, "protocol": self._protocol}
+        items = gnomekeyring.find_items_sync(gnomekeyring.ITEM_NETWORK_PASSWORD, attrs)
+        return (items[0].attributes["user"], items[0].secret)
+
+    def set_credentials(self, user, pw):
+        """
+        Store or update username and password for account
+        """
+        attrs = {
+                "user": user,
+                "server": self._host,
+                "protocol": self._protocol,
+            }
+        gnomekeyring.item_create_sync(gnomekeyring.get_default_keyring_sync(),
+                gnomekeyring.ITEM_NETWORK_PASSWORD, self._realm, attrs, pw, True)
+
+class AccountDialog (gtk.MessageDialog):
+    """
+    A simple dialog for managing an L{Account}. It must be used like any other
+    gtk dialog, like:
+    
+        dialog.show_all()
+        dialog.run()            
+        dialog.destroy()
+    
+    """
+    def __init__ (self, account, dialog_type=gtk.MESSAGE_QUESTION):
+        """
+        @param account: L{Account} to manage
+        """
+        gtk.MessageDialog.__init__(self, None,
+                                   type=dialog_type,
+                                   buttons=gtk.BUTTONS_OK_CANCEL)
+        
+        self._account = account
+        self._response = None
+        
+        self.connect ("response", self._on_response)
+        self.set_markup (_("<big><b>Login for %s</b></big>") % account.get_host())
+        self.format_secondary_markup (_("Please provide your credentials for <b>%s</b>") % account.get_host())
+        self.set_title (_("Credentials for %s") % account.get_host())
+        
+        self._user_entry = gtk.Entry()
+        self._password_entry = gtk.Entry()
+        self._password_entry.set_property("visibility", False) # Show '*' instead of text
+        
+        user_label = gtk.Label (_("User name:"))
+        password_label = gtk.Label (_("Password:"))
+        
+        table = gtk.Table (2, 2)
+        table.attach (user_label, 0, 1, 0, 1)
+        table.attach (self._user_entry, 1, 2, 0, 1)
+        table.attach (password_label, 0, 1, 1, 2)
+        table.attach (self._password_entry, 1, 2, 1, 2)
+        
+        self.vbox.pack_end (table)
+        
+        if self._account.has_credentials():
+            user, password = self._account.get_credentials()
+            self._user_entry.set_text(user)
+            self._password_entry.set_text(password)
+        
+        self._set_ok_sensitivity ()
+        self._user_entry.connect ("changed", lambda entry : self._set_ok_sensitivity())
+        self._password_entry.connect ("changed", lambda entry : self._set_ok_sensitivity())
+            
+    def _on_response (self, dialog, response_id):
+        self._response = response_id
+        if response_id == gtk.RESPONSE_OK:
+            LOGGER.debug ("Registering credentials for %s on %s" % (self._account.get_realm(), self._account.get_host()))
+            self._account.set_credentials(self.get_user(),
+                                          self.get_password())
+        else:
+            LOGGER.debug ("Credential registration for %s cancelled" % self._account.get_host())
+    
+    def _set_ok_sensitivity (self):
+        if self._user_entry.get_text() != "" and self._password_entry.get_text() != "":
+            self.set_response_sensitive(gtk.RESPONSE_OK, True)
+        else:
+            self.set_response_sensitive(gtk.RESPONSE_OK, False)
+    
+    def get_user (self):
+        return self._user_entry.get_text()
+    
+    def get_password (self):
+        return self._password_entry.get_text()
+    
+    def get_response (self):
+        """
+        @return: C{gtk.RESPONSE_OK} if the user pressed OK or 
+            C{gtk.RESPONSE_CANCEL} on cancellation. C{None} if no response
+            has been recorded yet
+        """
+        return self._response
+        
+class ConcurrentRequestsException (Exception):
+    """
+    Raised by L{GnomeURLopener} if there are multiple concurrent
+    requests to L{GnomeURLopener.open_async}.
+    """
+    def __init__ (self):
+        Exception.__init__ (self)
+
+class AuthenticationAborted (Exception):
+    """
+    Raised by L{GnomeURLopener} if the user cancels a request for
+    providing credentials
+    """
+    def __init__ (self):
+        Exception.__init__ (self)
+
+
+class GnomeURLopener (urllib.FancyURLopener, gobject.GObject):
+    """
+    A subclass of C{urllib.URLopener} able to intercept user/password requests
+    and pass them through an L{Account}, displaying a L{AccountDialog} if
+    necessary.
+    """
+    
+    __gsignals__ = {
+        "done" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [gobject.TYPE_PYOBJECT]),
+    }
+    
+    def __init__ (self, account):
+        urllib.FancyURLopener.__init__ (self)
+        gobject.GObject.__init__ (self)
+        self._account = account
+        self._thread = None
+        self._authentication_retries = 0
+        
+    def prompt_user_passwd (self, host, realm):
+        """
+        Override of the same method in C{urllib.FancyURLopener} to display
+        and L{AccountDialog} on user/pass requests.
+        """
+        LOGGER.debug ("Requesting credentials for host: '%s', realm '%s'" % (host, realm))
+        
+        self._authentication_retries = self._authentication_retries + 1
+        
+        gtk.gdk.threads_enter ()
+        
+        # If these credentials have failed before, prompt the user
+        if self._authentication_retries > 1:
+            LOGGER.debug ("Invalid credentials for %s in keyring. Asking for them again..." %
+                          self._account.get_host())
+            login_dialog = AccountDialog(self._account,
+                                         dialog_type=gtk.MESSAGE_WARNING)
+            login_dialog.set_markup (_("<big><b>Login to %s rejected</b></big>") % self._account.get_host())
+            login_dialog.format_secondary_markup (_("Please verify your credentials for <b>%s</b>") % self._account.get_host())
+            login_dialog.show_all()
+            login_dialog.run()            
+            login_dialog.destroy()
+            self._authentication_retries = 0
+            if login_dialog.get_response() == gtk.RESPONSE_CANCEL:
+                LOGGER.debug ("Login to %s aborted" % self._account.get_host())
+                gtk.gdk.threads_leave ()
+                raise AuthenticationAborted()
+        
+        # Make sure we do have the credentials
+        if not self._account.has_credentials ():
+            LOGGER.debug ("No credentials for %s in keyring. Asking for them..." %
+                          self._account.get_host())
+            login_dialog = AccountDialog(self._account)
+            login_dialog.show_all()
+            login_dialog.run()            
+            login_dialog.destroy()
+        
+        creds = self._account.get_credentials()
+        
+        gtk.gdk.threads_leave ()
+        
+        return creds
+    
+    def open_async (self, url, payload=None):
+        """
+        Open a URL asynchronously. When the request has been completed the
+        C{"done"} signal of this class is emitted.
+        
+        If C{payload} is not C{None} the http request
+        will be a C{POST} with the given payload. The way to construct the
+        post payload is typically by calling C{urllib.urlencode} on a key-value
+        C{dict}. For example:
+        
+            urllib.urlencode({"status" : msg})
+        
+        This method will raise a L{ConcurrentRequestsException} if there is
+        already a pending open request when a new one is issued.
+        
+        @param url: The URL to open asynchronously
+        @param payload: Optional payload in case of a POST request. See above
+        """
+        LOGGER.debug ("Async open on: %s with payload %s" % (url,payload))
+        if self._thread :
+            raise ConcurrentRequestsException()
+    
+        if payload != None :
+            async_args = (url, payload)
+        else :
+            async_args = (url, )
+        
+        self._thread = threading.Thread (target=self._do_open_async,
+                                         args=async_args,
+                                         name="GnomeURLopener")
+        
+        self._thread.start()
+        
+    def _do_open_async (self, *args):
+        self._authentication_retries = 0
+        self._thread = None
+        
+        try:
+            info = self.open (*args)
+        except AuthenticationAborted:
+            LOGGER.debug ("Detected authentication abort")
+            return
+            
+        gtk.gdk.threads_enter()
+        self.emit ("done", info)
+        gtk.gdk.threads_leave()
+

Modified: trunk/deskbar/handlers/Makefile.am
==============================================================================
--- trunk/deskbar/handlers/Makefile.am	(original)
+++ trunk/deskbar/handlers/Makefile.am	Tue Aug  5 21:04:11 2008
@@ -25,6 +25,7 @@
 	recent.py \
 	templates.py \
 	tomboy.py \
+	twitter.py \
 	web_address.py \
 	wikipedia-suggest.py \
 	yahoo.py

Added: trunk/deskbar/handlers/twitter.py
==============================================================================
--- (empty file)
+++ trunk/deskbar/handlers/twitter.py	Tue Aug  5 21:04:11 2008
@@ -0,0 +1,154 @@
+from deskbar.core.GconfStore import GconfStore
+from deskbar.core.Utils import strip_html, get_proxy, load_base64_icon
+from deskbar.core.Web import GnomeURLopener, Account, AccountDialog, ConcurrentRequestsException
+from deskbar.defs import VERSION
+from deskbar.handlers.actions.CopyToClipboardAction import CopyToClipboardAction
+from deskbar.handlers.actions.ShowUrlAction import ShowUrlAction
+from gettext import gettext as _
+from xml.sax.saxutils import unescape
+import deskbar
+import deskbar.interfaces.Action
+import deskbar.interfaces.Match
+import deskbar.interfaces.Module
+import gtk
+import gobject
+import logging
+import urllib
+
+LOGGER = logging.getLogger (__name__)
+
+TWITTER_UPDATE_URL = "http://twitter.com/statuses/update.xml";
+
+# Base64 encoded Twitter logo
+TWITTER_ICON = \
+"""iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAcVJREFUOI2FkztuFEEQhr/q7nnIa2GQVgiDCYCEAzhAHMY3gASJgAtwAXLuQ0AGRiQbOEBeGdsy4PXOTNdPMLOrWVbCJVXUVf+ru42h2pzVZEdmBEQwo07JuKUSgEta5szMjfNOTKLxvIDsrhjCf0ESABJXDl+WHTVw0sE0JB7gtwkYAIBG4mcWCVgCGTA5N22rngTMjBiMFKNtATjwx0Vh0Am+NpmzGPB+FwBDPEzQdFll6kESQAjBZjetfrsozDDEp0U3MmBrmAS8uldvW8jAlaBESGvVYIZJgDAz5tmZuzYB3F3fl5kLd+oRoQbZq/FO4mkZeFKETQADMuLSndqM6yxe3605rBJLaW0gI6YxUo6uNq0sNoK5i12DXy52gjExcSdFGCw5kP55FwH68wI4dXHiYubiW+skA7n3AxK44xoFMA7xcWGUZhxngcHbiwVnueIgBroVO/CyTuN91nKUO72/bHh3fg1xCGmDTCBjPxqfD/bYL/t3sI7TLfBmr+Jot4LO+9SCjTpANH50znGbNzMAiCFYNPh4f4cP0wnPklFJVBL10Lh4UScOq7htYVXZXblrWRA5deGjIQGPolEaVMNX/wuhBOJI5bQAKAAAAABJRU5ErkJggg=="""
+
+# Singleton ref to the loaded pixbuf
+_twitter_pixbuf = load_base64_icon (TWITTER_ICON)
+
+HANDLERS = ["TwitterModule"]
+VERSION = "0.2"
+
+MIN_MESSAGE_LEN = 2
+        
+class TwitterClient :
+    def __init__ (self):
+        self._account = Account ("twitter.com", "Twitter API")
+        self._opener = GnomeURLopener (self._account)
+        self._thread = None
+        
+        self._opener.connect ("done", self._on_opener_done)
+        
+    def update (self, msg):
+        try:
+            post_payload = urllib.urlencode({"status" : msg})
+            self._opener.open_async (TWITTER_UPDATE_URL, post_payload)
+        except ConcurrentRequestsException :
+            LOGGER.warning ("Attempting to post while another post is already running. Ignoring")
+            error = gtk.MessageDialog (None,
+                                       type=gtk.MESSAGE_WARNING,
+                                       buttons=gtk.BUTTONS_OK)
+            error.set_markup (_("A post is already awaiting submission, please wait before you post another message"))
+            error.set_title (_("Error posting to twitter.com"))
+            error.show_all()
+            error.run()
+            error.destroy()
+            return
+            
+    def _on_opener_done (self, opener, info):
+        LOGGER.debug ("Got reply from Twitter")
+        #for s in info.readlines() : print s
+
+_FAIL_POST = _(
+"""Failed to post update to twitter.com. Please make sure that:
+
+  - Your internet connection is working
+  - You can connect to <i>http://twitter.com</i> in your web browser
+"""
+)
+
+class TwitterUpdateAction(deskbar.interfaces.Action):
+    
+    def __init__(self, msg, client):
+        deskbar.interfaces.Action.__init__ (self, msg)
+        self._msg = msg
+        self._client = client
+    
+    def get_hash(self):
+        return "twitter:"+self._msg
+        
+    def get_icon(self):
+        # We use only pixbufs
+        return None
+    
+    def get_pixbuf(self) :
+        global _twitter_pixbuf
+        return _twitter_pixbuf
+    
+    def activate(self, text=None):
+        LOGGER.info ("Posting: '%s'" % self._msg)
+        try:
+            self._client.update (self._msg)
+        except IOError, e:
+            LOGGER.warning ("Failed to post to twitter.com: %s" % e)
+            error = gtk.MessageDialog (None,
+                                       type=gtk.MESSAGE_WARNING,
+                                       buttons=gtk.BUTTONS_OK)
+            error.set_markup (_FAIL_POST)
+            error.set_title (_("Error posting to twitter.com"))
+            error.show_all()
+            error.run()
+            error.destroy()
+        
+    def get_verb(self):
+        return _('Post <i>"%(msg)s"</i>')
+
+    def get_tooltip(self, text=None):
+        return _("Update your Twitter account with the message:\n\n\t<i>%s</i>") % self._msg
+        
+    def get_name(self, text=None):
+        return {"name": self._msg, "msg" : self._msg}
+    
+    def skip_history(self):
+        return True
+
+class TwitterMatch(deskbar.interfaces.Match):
+    
+    def __init__(self, msg, client, **args):
+        global _twitter_pixbuf
+        
+        deskbar.interfaces.Match.__init__ (self, category="web", pixbuf=_twitter_pixbuf, name=msg, **args)
+        self.add_action( TwitterUpdateAction(self.get_name(), client) )
+    
+    def get_hash(self):
+        return "twitter:"+self.get_name()
+
+class TwitterModule(deskbar.interfaces.Module):
+    
+    INFOS = {'icon': _twitter_pixbuf,
+             'name': _("Twitter"),
+             'description': _("Post updates to your Twitter account"),
+             'version': VERSION}
+    
+    def __init__(self):
+        deskbar.interfaces.Module.__init__(self)
+        self._client = TwitterClient()
+    
+    def query(self, qstring):
+        if len (qstring) <= MIN_MESSAGE_LEN and \
+           len (qstring) > 140: return None
+        
+        self._emit_query_ready(qstring, [TwitterMatch(qstring, self._client)])
+    
+    def has_config(self):        
+        return True
+    
+    def show_config(self, parent):
+        LOGGER.debug ("Showing config")
+        account = Account ("twitter.com", "Twitter API")
+        
+        login_dialog = AccountDialog(account)
+        login_dialog.show_all()
+        login_dialog.run()            
+        login_dialog.destroy()
+   



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]