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()