deskbar-applet r2289 - in trunk: . deskbar/core deskbar/handlers
- From: kamstrup svn gnome org
- To: svn-commits-list gnome org
- Subject: deskbar-applet r2289 - in trunk: . deskbar/core deskbar/handlers
- Date: Tue, 5 Aug 2008 21:04:11 +0000 (UTC)
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]