From fa6ae3b4be40d5eaa12a9f5ecf6794856342cfca Mon Sep 17 00:00:00 2001 From: Tomasz Melcer Date: Fri, 21 Jul 2006 13:52:36 +0000 Subject: [PATCH] Simple support for pubsub.com notifications. --- data/glade/atom_entry_window.glade | 371 +++++++++++++++++++++++++++++ src/atom_window.py | 115 +++++++++ src/common/atom.py | 138 +++++++++++ src/common/connection_handlers.py | 31 +++ src/gajim.py | 6 + 5 files changed, 661 insertions(+) create mode 100644 data/glade/atom_entry_window.glade create mode 100644 src/atom_window.py create mode 100644 src/common/atom.py diff --git a/data/glade/atom_entry_window.glade b/data/glade/atom_entry_window.glade new file mode 100644 index 000000000..97f893363 --- /dev/null +++ b/data/glade/atom_entry_window.glade @@ -0,0 +1,371 @@ + + + + + + + True + New entry received + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + True + False + True + False + False + GDK_WINDOW_TYPE_HINT_NORMAL + GDK_GRAVITY_NORTH_WEST + True + + + + 6 + True + False + 0 + + + + True + You have received new entry: + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + False + False + + + + + + True + + + 0 + False + True + + + + + + True + 4 + 2 + False + 0 + 6 + + + + True + Feed name: + False + False + GTK_JUSTIFY_RIGHT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 0 + 1 + fill + + + + + + + True + Entry: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 2 + 3 + fill + + + + + + + True + Last modified: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 3 + 4 + fill + + + + + + + True + <small>Romeo and Juliet</small> + False + True + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 1 + 2 + 1 + 2 + + + + + + + True + 2003-12-13T18:30:02Z + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 1 + 2 + 3 + 4 + + + + + + + True + True + False + + + + + True + Old stories + False + True + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + + + 1 + 2 + 0 + 1 + fill + fill + + + + + + True + True + False + + + + + True + Soliloquy + False + True + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + + + 1 + 2 + 2 + 3 + fill + fill + + + + + 0 + True + True + + + + + + True + + + 0 + False + True + + + + + + True + False + 0 + + + + True + False + 0 + + + + True + True + gtk-close + True + GTK_RELIEF_NORMAL + True + + + + 0 + False + False + + + + + + + + + 0 + True + True + + + + + + True + True + Next entry + True + GTK_RELIEF_NORMAL + True + + + + 0 + False + False + GTK_PACK_END + + + + + 0 + False + True + + + + + + + diff --git a/src/atom_window.py b/src/atom_window.py new file mode 100644 index 000000000..54bf32806 --- /dev/null +++ b/src/atom_window.py @@ -0,0 +1,115 @@ +'''atom_window.py - a window to display atom entries from pubsub. For now greatly simplified, +supports only simple feeds like the one from pubsub.com. ''' + +import gtk +import gtk.gdk + +import gtkgui_helpers +from common import helpers + +class AtomWindow: + window = None + entries = [] + + @classmethod # python2.4 decorator + def newAtomEntry(cls, entry): + ''' Queue new entry, open window if there's no one opened. ''' + cls.entries.append(entry) + + if cls.window is None: + cls.window = AtomWindow() + else: + cls.window.updateCounter() + + def __init__(self): + ''' Create new window... only if we have anything to show. ''' + assert len(self.__class__.entries)>0 + + self.entry = None # the entry actually displayed + + self.xml = gtkgui_helpers.get_glade('atom_entry_window.glade') + self.window = self.xml.get_widget('atom_entry_window') + for name in ('new_entry_label', 'feed_title_label', 'feed_title_eventbox', + 'feed_tagline_label', 'entry_title_label', 'entry_title_eventbox', + 'last_modified_label', 'close_button', 'next_button'): + self.__dict__[name] = self.xml.get_widget(name) + + self.displayNextEntry() + + self.xml.signal_autoconnect(self) + self.window.show_all() + + self.entry_title_eventbox.add_events(gtk.gdk.BUTTON_PRESS_MASK) + self.feed_title_eventbox.add_events(gtk.gdk.BUTTON_PRESS_MASK) + + def displayNextEntry(self): + ''' Get next entry from the queue and display it in the window. ''' + assert len(self.__class__.entries)>0 + + newentry = self.__class__.entries.pop(0) + + # fill the fields + if newentry.feed_link is not None: + self.feed_title_label.set_markup( + u'%s' % \ + gtkgui_helpers.escape_for_pango_markup(newentry.feed_title)) + else: + self.feed_title_label.set_markup( + gtkgui_helpers.escape_for_pango_markup(newentry.feed_title)) + + self.feed_tagline_label.set_markup( + u'%s' % \ + gtkgui_helpers.escape_for_pango_markup(newentry.feed_tagline)) + + if newentry.uri is not None: + self.entry_title_label.set_markup( + u'%s' % \ + gtkgui_helpers.escape_for_pango_markup(newentry.title)) + else: + self.entry_title_label.set_markup( + gtkgui_helpers.escape_for_pango_markup(newentry.title)) + + self.last_modified_label.set_text(newentry.updated) + + # update the counters + self.updateCounter() + + self.entry = newentry + + def updateCounter(self): + ''' We display number of events on the top of window, sometimes it needs to be + changed...''' + count = len(self.__class__.entries) + # TODO: translate + if count>0: + self.new_entry_label.set_text( \ + 'You have received new entries (and %(count)d not displayed):' % \ + {'count': count}) + self.next_button.set_sensitive(True) + else: + self.new_entry_label.set_text('You have received new entry:') + self.next_button.set_sensitive(False) + + def on_close_button_clicked(self, widget): + self.window.destroy() + + def on_next_button_clicked(self, widget): + self.displayNextEntry() + + def on_entry_title_button_press_event(self, widget, event): + # TODO: make it using special gtk2.10 widget + print 1 + if event.button == 1: # left click + uri = self.entry.uri + if uri is not None: + helpers.launch_browser_mailer('url', uri) + return True + + def on_feed_title_button_press_event(self, widget, event): + # TODO: make it using special gtk2.10 widget + print 2 + if event.button == 1: # left click + uri = self.entry.feed_uri + if uri is not None: + helpers.launch_browser_mailer('url', uri) + return True diff --git a/src/common/atom.py b/src/common/atom.py new file mode 100644 index 000000000..508b3db5c --- /dev/null +++ b/src/common/atom.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +''' Atom (rfc 4287) feed parser, used to read data from atom-over-pubsub transports +and services. Very simple. Actually implements only atom:entry. Implement more features +if you need. ''' + +# suggestion: rewrite functions that return dates to return standard python time tuples, +# exteneded to contain timezone + +import xmpp +import time + +class PersonConstruct(xmpp.Node, object): + ''' Not used for now, as we don't need authors/contributors in pubsub.com feeds. + They rarely exist there. ''' + def __init__(self, node): + ''' Create person construct from node. ''' + xmpp.Node.__init__(self, node=node) + + def get_name(self): + return self.getTagData('name') + + name = property(get_name, None, None, + '''Conveys a human-readable name for the person. Should not be None, + although some badly generated atom feeds don't put anything here + (this is non-standard behavior, still pubsub.com sometimes does that.)''') + + def get_uri(self): + return self.getTagData('uri') + + uri = property(get_uri, None, None, + '''Conveys an IRI associated with the person. Might be None when not set.''') + + def get_email(self): + return self.getTagData('email') + + email = property(get_email, None, None, + '''Conveys an e-mail address associated with the person. Might be None when + not set.''') + +class Entry(xmpp.Node, object): + def __init__(self, node=None): + ''' Create new atom entry object. ''' + xmpp.Node.__init__(self, 'entry', node=node) + + def __repr__(self): + return '' % self.id + +class OldEntry(xmpp.Node, object): + ''' Parser for feeds from pubsub.com. They use old Atom 0.3 format with + their extensions. ''' + def __init__(self, node=None): + ''' Create new Atom 0.3 entry object. ''' + xmpp.Node.__init__(self, 'entry', node=node) + + def __repr__(self): + return '' % self.id + + def get_feed_title(self): + ''' Returns title of feed, where the entry was created. The result is the feed name + concatenated with source-feed title. ''' + title = u'' + + if self.parent is not None: + main_feed = self.parent.getTagData('title') + else: + main_feed = None + + if self.getTag('source-feed') is not None: + source_feed = self.getTag('source-feed').getTagData('title') + else: + source_feed = None + + if main_feed is not None and source_feed is not None: + return u'%s: %s' % (main_feed, source_feed) + elif main_feed is not None: + return main_feed + elif source_feed is not None: + return source_feed + else: + return u'' + + feed_title = property(get_feed_title, None, None, + ''' Title of feed. It is built from entry's original feed title and title of feed + which delivered this entry. ''') + + def get_feed_link(self): + ''' Get a link to main page of feed (in pubsub.com: second link of rel='alternate', + first contains raw xml data). ''' + try: + return self.getTag('source-feed').getTags('link', {'rel':'alternate'})[1].getData() + except: + return None + + feed_link = property(get_feed_link, None, None, + ''' Link to main webpage of the feed. ''') + + def get_title(self): + ''' Get an entry's title. ''' + return self.getTagData('title') + + title = property(get_title, None, None, + ''' Entry's title. ''') + + def get_uri(self): + ''' Get the uri the entry points to (entry's first link element with rel='alternate' + or without rel attribute). ''' + for element in self.getTags('link'): + if element.attrs.has_key('rel') and element.attrs['rel']<>'alternate': continue + try: + return element.attrs['href'] + except AttributeError: + pass + return None + + uri = property(get_uri, None, None, + ''' URI that is pointed by the entry. ''') + + def get_updated(self): + ''' Get the time the entry was updated last time. This should be standarized, + but pubsub.com sends it in human-readable format. We won't try to parse it. + (Atom 0.3 uses the word «modified» for that). + + If there's no time given in the entry, we try with + and elements. ''' + for name in ('updated', 'modified', 'published', 'issued'): + date = self.getTagData(name) + if date is not None: break + + if date is None: + # it is not in the standard format + return time.asctime() + + return date + + updated = property(get_updated, None, None, + ''' Last significant modification time. ''') + + feed_tagline = u'' diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 29b5bac6c..bc43958e9 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -33,6 +33,7 @@ from common import GnuPG from common import helpers from common import gajim from common import dataforms +from common import atom from common.commands import ConnectionCommands STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', @@ -1275,6 +1276,10 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, def _messageCB(self, con, msg): '''Called when we receive a message''' + # check if the message is pubsub#event + if msg.getTag('event') is not None: + self._pubsubEventCB(con, msg) + return msgtxt = msg.getBody() mtype = msg.getType() subject = msg.getSubject() # if not there, it's None @@ -1394,6 +1399,32 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, subject, chatstate, msg_id, composing_jep, user_nick)) # END messageCB + def _pubsubEventCB(self, con, msg): + ''' Called when we receive with pubsub event. ''' + # TODO: Logging? (actually services where logging would be useful, should + # TODO: allow to access archives remotely...) + event = msg.getTag('event') + + items = event.getTag('items') + if items is None: return + + for item in items.getTags('item'): + # check for event type (for now only one type supported: pubsub.com events) + child = item.getTag('pubsub-message') + if child is not None: + # we have pubsub.com notification + child = child.getTag('feed') + if child is None: continue + + for entry in child.getTags('entry'): + # for each entry in feed (there shouldn't be more than one, + # but to be sure... + self.dispatch('ATOM_ENTRY', (atom.OldEntry(node=entry),)) + continue + # unknown type... probably user has another client who understands that event + + raise common.xmpp.NodeProcessed + def _presenceCB(self, con, prs): '''Called when we receive a presence''' ptype = prs.getType() diff --git a/src/gajim.py b/src/gajim.py index d0fd625cb..1d2e6fa16 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -38,6 +38,7 @@ from common import i18n import message_control from chat_control import ChatControlBase +from atom_window import AtomWindow from common import exceptions @@ -1346,6 +1347,10 @@ class Interface: def handle_event_metacontacts(self, account, tags_list): gajim.contacts.define_metacontacts(account, tags_list) + def handle_atom_entry(self, account, data): + atom_entry, = data + AtomWindow.newAtomEntry(atom_entry) + def handle_event_privacy_lists_received(self, account, data): # ('PRIVACY_LISTS_RECEIVED', account, list) if not self.instances.has_key(account): @@ -1671,6 +1676,7 @@ class Interface: 'ASK_NEW_NICK': self.handle_event_ask_new_nick, 'SIGNED_IN': self.handle_event_signed_in, 'METACONTACTS': self.handle_event_metacontacts, + 'ATOM_ENTRY': self.handle_atom_entry, 'PRIVACY_LISTS_RECEIVED': self.handle_event_privacy_lists_received, 'PRIVACY_LIST_RECEIVED': self.handle_event_privacy_list_received, 'PRIVACY_LISTS_ACTIVE_DEFAULT': \