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 xmlrpclib import xml.sax import xml.sax.handler import gnomekeyring JAIKU_UPDATE_URL = "http://jaiku.com/statuses/update.xml" # Base64 encoded Jaiku logo JAIKU_ICON = \ """iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9gGEggwDnEEFBoAAAJ4SURBVDjLrZPNa5R3EMc/83ueTZ593xizG3WzJqa6CBIlQUFKQYlCSylE2pOn4sEe/BO8xENPPfgHlOIfIHjyIih4UxDpC6JIAyZrY5I1T5J93zy7v9/04KpUrJd2YOYwzHzny3xn4D+afJhouKXRsLG6dyOs0O5s41ASQYaxkSKFXKmZNuXVjwI8fXVrNpFNfdOuNRc/NdFLyGI5d+HaPwBWtu5dqrU2f/p15U6upasGE4EooDCIooLYIYbJu9nJr3dzmbGrk7lz12Wt8zAIt15uPli6mex5IYh906xv4fU9URXAkDYlZopfdoJY8iuzU69e/KNyN9kzIWcOXcE3SbAx1HkDN+AEVUVxqFoauszj5dvxnrXfGrX9H9tRiOJY2rmPc5bZ0nccPTBPzCSYHv2CbLw4ABFOlb4nZpJEro4SXfHb/dq4o4c6oZQ5SSZV4MVfz5jaf4xyYZ5ELEdkI5q7Idb1eL5xD7E+KhHOWWP07TbVoOrY2tngUPEonvGpRRWs6/FZ8Tgpb4IjxdOUC/NgQFEi7eLH/ZF1YWg8FQQMe2mqjee8rv+JikXF0mrXmTafc2TfaVpSoa9tBB/RPk77Xd/zhq4mhvf+0uwv8/vGLaxGgCIDBeq7KyxvGmrdNUQERInsLnFvnGQs/7Osdx8FL6tPOr+9uEPPew1icaLIOxUVRN6oMZDTs2nmphaYzs/tM+PBye50/sTZmYPnd1JMOmwc0ffFAyqIKKI+gR5wc1MLlMbKF/YEM+vvTvlR5cZsJj16ud4If/jUKWeyI4vl7MK1f32mplvKhY21QnW7QqtTQ50jHk+TH5kgn53YTpnDVf5P+xt+FxV3gR11mAAAAABJRU5ErkJggg==""" # Singleton ref to the loaded lixbuf _jaiku_pixbuf = None LOGGER = logging.getLogger(__name__) HANDLERS = ["JaikuHandler"] 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(JAIKU_ICON)) except Exception, msg: LOGGER.warning ("Failed to read base64 encoded image: " + msg) finally: loader.close() return loader.get_pixbuf() def load_jaiku_icon (): global _jaiku_pixbuf _jaiku_pixbuf = load_base64_icon (JAIKU_ICON) if not _jaiku_pixbuf: _jaiku_pixbuf = deskbar.core.Utils.ICON_THEME.load_icon("stock-unknown", deskbar.ICON_HEIGHT, gtk.ICON_LOOKUP_USE_BUILTIN) return _jaiku_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 (_("Personal API Key:")) 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 JaikuClient (gobject.GObject) : __gsignals__ = { "message-reply" : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [gobject.TYPE_PYOBJECT]), } def __init__ (self): gobject.GObject.__init__ (self) self._account = Account("jaiku.com", "Jaiku API") self._jaiku = xmlrpclib.ServerProxy ("http://api.jaiku.com/xmlrpc") self._thread = None self.connect ("message-reply", lambda client, reply : self._on_message_reply(reply)) def send_message (self, msg): 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() if self._thread : raise ConcurrentRequestsException() self._thread = threading.Thread (target=self._do_send_message, args=(msg,), name="JaikuClient") self._thread.start() def _on_message_reply (self, reply): LOGGER.debug ("Got reply from Jaiku '%s'", reply) def _do_send_message (self, *args): self._thread = None gtk.gdk.threads_enter() user, password = self._account.get_credentials() msg = args[0] gtk.gdk.threads_leave() reply = self._jaiku.presence.send ({"user" : user, "personal_key" : password, "message" : msg}) gtk.gdk.threads_enter() self.emit ("message-reply", reply) gtk.gdk.threads_leave() _FAIL_POST = _( """Failed to post update to jaiku.com. Please make sure that: - Your innternet connection is working - You can connect to http://jaiku.com in your web browser """ ) class JaikuUpdateAction(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 "jaiku:"+self._msg def get_icon(self): # We use only pixbufs return None def get_pixbuf(self) : global _jaiku_pixbuf return _jaiku_pixbuf def activate(self, text=None): LOGGER.info ("Posting: '%s'" % self._msg) try: self._client.send_message (self._msg) except IOError, e: LOGGER.warning ("Failed to post to jaiku.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 jaiku.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 Jaiku 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 JaikuMatch(deskbar.interfaces.Match): def __init__(self, msg, client, **args): global _jaiku_pixbuf deskbar.interfaces.Match.__init__ (self, category="web", pixbuf=_jaiku_pixbuf, name=msg, **args) self.add_action( JaikuUpdateAction(self.get_name(), client) ) def get_hash(self): return "jaiku:"+self.get_name() class JaikuHandler(deskbar.interfaces.Module): INFOS = {'icon': load_jaiku_icon (), 'name': _("Jaiku"), 'description': _("Post updates to your Jaiku account"), 'version': VERSION} def __init__(self): deskbar.interfaces.Module.__init__(self) self._client = JaikuClient() def query(self, qstring): if len (qstring) <= MIN_MESSAGE_LEN and \ len (qstring) > 140: return None self._emit_query_ready(qstring, [JaikuMatch(qstring, self._client)]) def has_config(self): return True def show_config(self, parent): LOGGER.debug ("Showing config") account = Account ("jaiku.com", "Jaiku API") login_dialog = AccountDialog(account) login_dialog.show_all() login_dialog.run() login_dialog.destroy()