from deskbar.core.GconfStore import GconfStore from deskbar.core.Utils import strip_html, get_proxy 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 base64 import deskbar import deskbar.interfaces.Action import deskbar.interfaces.Match import deskbar.interfaces.Module import gtk import gobject import logging import threading import re import urllib import xml.sax import xml.sax.handler import gnomekeyring 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 lixbuf _twitter_pixbuf = None LOGGER = logging.getLogger(__name__) HANDLERS = ["TwitterHandler"] VERSION = "0.1" MIN_MESSAGE_LEN = 2 def load_base64_icon (string): loader = gtk.gdk.PixbufLoader() try: loader.set_size(deskbar.ICON_HEIGHT, deskbar.ICON_HEIGHT) loader.write(base64.b64decode(TWITTER_ICON)) except Exception, msg: LOGGER.warning ("Failed to read base64 encoded image: " + msg) finally: loader.close() return loader.get_pixbuf() def load_twitter_icon (): global _twitter_pixbuf _twitter_pixbuf = load_base64_icon (TWITTER_ICON) if not _twitter_pixbuf: _twitter_pixbuf = deskbar.core.Utils.ICON_THEME.load_icon("stock-unknown", deskbar.ICON_HEIGHT, gtk.ICON_LOOKUP_USE_BUILTIN) return _twitter_pixbuf 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 was cpoied (almost) verbatim from Sebastian Rittau's blog found on http://www.rittau.org/blog/20070726-00. """ 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): 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): 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): 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): def __init__ (self, account): gtk.MessageDialog.__init__(self, None, type=gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_OK_CANCEL) self._account = account self.connect ("response", self._on_response) self.set_markup (_("Login for %s") % account.get_host()) self.format_secondary_markup (_("Please provide your credentials for %s") % 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): 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() class ConcurrentRequestsException (Exception): """ Raised by GnomeURLopener if there are multiple concurrent requests to open_async() """ def __init__ (self): Exception.__init__ (self) class GnomeURLopener (urllib.FancyURLopener, gobject.GObject): """ We need to provide a subclass of URLopener to urllib to be able to intercept user/password requests """ __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 def prompt_user_passwd (self, host, realm): LOGGER.debug ("Requesting credentials for host: '%s', realm '%s'" % (host, realm)) gtk.gdk.threads_enter () if not self._account.has_credentials (): LOGGER.debug ("No twitter credentials in keyring. Asking for them...") 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, *args): print "ASYNC:", url, args if self._thread : raise ConcurrentRequestsException() if len(args) > 1 : raise TypeError("To many optional arguments %s. Max 1" % len(args)) if len(args) == 1 : async_args = (url, args[0]) 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): info = self.open (*args) self._thread = None gtk.gdk.threads_enter() self.emit ("done", info) gtk.gdk.threads_leave() 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 innternet connection is working - You can connect to http://twitter.com 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 "%(msg)s"') def get_tooltip(self, text=None): return _("Update your Twitter account with the message:\n\n\t%s") % 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 TwitterHandler(deskbar.interfaces.Module): INFOS = {'icon': load_twitter_icon (), '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()