Simple support for pubsub.com notifications.

This commit is contained in:
Tomasz Melcer 2006-07-21 13:52:36 +00:00
parent fd5294bd21
commit fa6ae3b4be
5 changed files with 661 additions and 0 deletions

View File

@ -0,0 +1,371 @@
<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd">
<glade-interface>
<widget class="GtkWindow" id="atom_entry_window">
<property name="visible">True</property>
<property name="title" translatable="yes">New entry received</property>
<property name="type">GTK_WINDOW_TOPLEVEL</property>
<property name="window_position">GTK_WIN_POS_NONE</property>
<property name="modal">False</property>
<property name="resizable">True</property>
<property name="destroy_with_parent">False</property>
<property name="decorated">True</property>
<property name="skip_taskbar_hint">False</property>
<property name="skip_pager_hint">False</property>
<property name="type_hint">GDK_WINDOW_TYPE_HINT_NORMAL</property>
<property name="gravity">GDK_GRAVITY_NORTH_WEST</property>
<property name="focus_on_map">True</property>
<child>
<widget class="GtkVBox" id="vbox112">
<property name="border_width">6</property>
<property name="visible">True</property>
<property name="homogeneous">False</property>
<property name="spacing">0</property>
<child>
<widget class="GtkLabel" id="new_entry_label">
<property name="visible">True</property>
<property name="label" translatable="yes">You have received new entry:</property>
<property name="use_underline">False</property>
<property name="use_markup">False</property>
<property name="justify">GTK_JUSTIFY_LEFT</property>
<property name="wrap">False</property>
<property name="selectable">False</property>
<property name="xalign">0.5</property>
<property name="yalign">0.5</property>
<property name="xpad">0</property>
<property name="ypad">0</property>
<property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
<property name="width_chars">-1</property>
<property name="single_line_mode">False</property>
<property name="angle">0</property>
</widget>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">False</property>
</packing>
</child>
<child>
<widget class="GtkHSeparator" id="hseparator14">
<property name="visible">True</property>
</widget>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">True</property>
</packing>
</child>
<child>
<widget class="GtkTable" id="table4">
<property name="visible">True</property>
<property name="n_rows">4</property>
<property name="n_columns">2</property>
<property name="homogeneous">False</property>
<property name="row_spacing">0</property>
<property name="column_spacing">6</property>
<child>
<widget class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="label" translatable="yes">Feed name:</property>
<property name="use_underline">False</property>
<property name="use_markup">False</property>
<property name="justify">GTK_JUSTIFY_RIGHT</property>
<property name="wrap">False</property>
<property name="selectable">False</property>
<property name="xalign">0</property>
<property name="yalign">0.5</property>
<property name="xpad">0</property>
<property name="ypad">0</property>
<property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
<property name="width_chars">-1</property>
<property name="single_line_mode">False</property>
<property name="angle">0</property>
</widget>
<packing>
<property name="left_attach">0</property>
<property name="right_attach">1</property>
<property name="top_attach">0</property>
<property name="bottom_attach">1</property>
<property name="x_options">fill</property>
<property name="y_options"></property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="label" translatable="yes">Entry:</property>
<property name="use_underline">False</property>
<property name="use_markup">False</property>
<property name="justify">GTK_JUSTIFY_LEFT</property>
<property name="wrap">False</property>
<property name="selectable">False</property>
<property name="xalign">0</property>
<property name="yalign">0.5</property>
<property name="xpad">0</property>
<property name="ypad">0</property>
<property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
<property name="width_chars">-1</property>
<property name="single_line_mode">False</property>
<property name="angle">0</property>
</widget>
<packing>
<property name="left_attach">0</property>
<property name="right_attach">1</property>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
<property name="x_options">fill</property>
<property name="y_options"></property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="label" translatable="yes">Last modified:</property>
<property name="use_underline">False</property>
<property name="use_markup">False</property>
<property name="justify">GTK_JUSTIFY_LEFT</property>
<property name="wrap">False</property>
<property name="selectable">False</property>
<property name="xalign">0</property>
<property name="yalign">0.5</property>
<property name="xpad">0</property>
<property name="ypad">0</property>
<property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
<property name="width_chars">-1</property>
<property name="single_line_mode">False</property>
<property name="angle">0</property>
</widget>
<packing>
<property name="left_attach">0</property>
<property name="right_attach">1</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
<property name="x_options">fill</property>
<property name="y_options"></property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="feed_tagline_label">
<property name="visible">True</property>
<property name="label" translatable="yes">&lt;small&gt;Romeo and Juliet&lt;/small&gt;</property>
<property name="use_underline">False</property>
<property name="use_markup">True</property>
<property name="justify">GTK_JUSTIFY_LEFT</property>
<property name="wrap">False</property>
<property name="selectable">False</property>
<property name="xalign">0</property>
<property name="yalign">0.5</property>
<property name="xpad">0</property>
<property name="ypad">0</property>
<property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
<property name="width_chars">-1</property>
<property name="single_line_mode">False</property>
<property name="angle">0</property>
</widget>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="y_options"></property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="last_modified_label">
<property name="visible">True</property>
<property name="label" translatable="yes">2003-12-13T18:30:02Z</property>
<property name="use_underline">False</property>
<property name="use_markup">False</property>
<property name="justify">GTK_JUSTIFY_LEFT</property>
<property name="wrap">False</property>
<property name="selectable">False</property>
<property name="xalign">0</property>
<property name="yalign">0.5</property>
<property name="xpad">0</property>
<property name="ypad">0</property>
<property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
<property name="width_chars">-1</property>
<property name="single_line_mode">False</property>
<property name="angle">0</property>
</widget>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
<property name="y_options"></property>
</packing>
</child>
<child>
<widget class="GtkEventBox" id="feed_title_eventbox">
<property name="visible">True</property>
<property name="visible_window">True</property>
<property name="above_child">False</property>
<signal name="button_press_event" handler="on_feed_title_eventbox_button_press_event" last_modification_time="Thu, 20 Jul 2006 21:53:07 GMT"/>
<child>
<widget class="GtkLabel" id="feed_title_label">
<property name="visible">True</property>
<property name="label" translatable="yes">Old stories</property>
<property name="use_underline">False</property>
<property name="use_markup">True</property>
<property name="justify">GTK_JUSTIFY_LEFT</property>
<property name="wrap">False</property>
<property name="selectable">False</property>
<property name="xalign">0</property>
<property name="yalign">0.5</property>
<property name="xpad">0</property>
<property name="ypad">0</property>
<property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
<property name="width_chars">-1</property>
<property name="single_line_mode">False</property>
<property name="angle">0</property>
</widget>
</child>
</widget>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">0</property>
<property name="bottom_attach">1</property>
<property name="x_options">fill</property>
<property name="y_options">fill</property>
</packing>
</child>
<child>
<widget class="GtkEventBox" id="entry_title_eventbox">
<property name="visible">True</property>
<property name="visible_window">True</property>
<property name="above_child">False</property>
<signal name="button_press_event" handler="on_entry_title_eventbox_button_press_event" last_modification_time="Thu, 20 Jul 2006 21:53:12 GMT"/>
<child>
<widget class="GtkLabel" id="entry_title_label">
<property name="visible">True</property>
<property name="label" translatable="yes">Soliloquy</property>
<property name="use_underline">False</property>
<property name="use_markup">True</property>
<property name="justify">GTK_JUSTIFY_LEFT</property>
<property name="wrap">False</property>
<property name="selectable">False</property>
<property name="xalign">0</property>
<property name="yalign">0.5</property>
<property name="xpad">0</property>
<property name="ypad">0</property>
<property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
<property name="width_chars">-1</property>
<property name="single_line_mode">False</property>
<property name="angle">0</property>
</widget>
</child>
</widget>
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
<property name="x_options">fill</property>
<property name="y_options">fill</property>
</packing>
</child>
</widget>
<packing>
<property name="padding">0</property>
<property name="expand">True</property>
<property name="fill">True</property>
</packing>
</child>
<child>
<widget class="GtkHSeparator" id="hseparator15">
<property name="visible">True</property>
</widget>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">True</property>
</packing>
</child>
<child>
<widget class="GtkHBox" id="hbox1">
<property name="visible">True</property>
<property name="homogeneous">False</property>
<property name="spacing">0</property>
<child>
<widget class="GtkHBox" id="hbox2">
<property name="visible">True</property>
<property name="homogeneous">False</property>
<property name="spacing">0</property>
<child>
<widget class="GtkButton" id="close_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="label">gtk-close</property>
<property name="use_stock">True</property>
<property name="relief">GTK_RELIEF_NORMAL</property>
<property name="focus_on_click">True</property>
<signal name="clicked" handler="on_close_button_clicked" last_modification_time="Thu, 20 Jul 2006 21:29:17 GMT"/>
</widget>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">False</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</widget>
<packing>
<property name="padding">0</property>
<property name="expand">True</property>
<property name="fill">True</property>
</packing>
</child>
<child>
<widget class="GtkButton" id="next_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="label">Next entry</property>
<property name="use_underline">True</property>
<property name="relief">GTK_RELIEF_NORMAL</property>
<property name="focus_on_click">True</property>
<signal name="clicked" handler="on_next_button_clicked" last_modification_time="Thu, 20 Jul 2006 21:29:21 GMT"/>
</widget>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">GTK_PACK_END</property>
</packing>
</child>
</widget>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">True</property>
</packing>
</child>
</widget>
</child>
</widget>
</glade-interface>

115
src/atom_window.py Normal file
View File

@ -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'<span foreground="blue" underline="single">%s</span>' % \
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'<small>%s</small>' % \
gtkgui_helpers.escape_for_pango_markup(newentry.feed_tagline))
if newentry.uri is not None:
self.entry_title_label.set_markup(
u'<span foreground="blue" underline="single">%s</span>' % \
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

138
src/common/atom.py Normal file
View File

@ -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 '<Atom:Entry object of id="%r">' % 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 '<Atom0.3:Entry object of id="%r">' % 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 <published>
and <issued> 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''

View File

@ -33,6 +33,7 @@ from common import GnuPG
from common import helpers from common import helpers
from common import gajim from common import gajim
from common import dataforms from common import dataforms
from common import atom
from common.commands import ConnectionCommands from common.commands import ConnectionCommands
STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd',
@ -1275,6 +1276,10 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
def _messageCB(self, con, msg): def _messageCB(self, con, msg):
'''Called when we receive a message''' '''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() msgtxt = msg.getBody()
mtype = msg.getType() mtype = msg.getType()
subject = msg.getSubject() # if not there, it's None 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)) subject, chatstate, msg_id, composing_jep, user_nick))
# END messageCB # END messageCB
def _pubsubEventCB(self, con, msg):
''' Called when we receive <message/> 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): def _presenceCB(self, con, prs):
'''Called when we receive a presence''' '''Called when we receive a presence'''
ptype = prs.getType() ptype = prs.getType()

View File

@ -38,6 +38,7 @@ from common import i18n
import message_control import message_control
from chat_control import ChatControlBase from chat_control import ChatControlBase
from atom_window import AtomWindow
from common import exceptions from common import exceptions
@ -1346,6 +1347,10 @@ class Interface:
def handle_event_metacontacts(self, account, tags_list): def handle_event_metacontacts(self, account, tags_list):
gajim.contacts.define_metacontacts(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): def handle_event_privacy_lists_received(self, account, data):
# ('PRIVACY_LISTS_RECEIVED', account, list) # ('PRIVACY_LISTS_RECEIVED', account, list)
if not self.instances.has_key(account): if not self.instances.has_key(account):
@ -1671,6 +1676,7 @@ class Interface:
'ASK_NEW_NICK': self.handle_event_ask_new_nick, 'ASK_NEW_NICK': self.handle_event_ask_new_nick,
'SIGNED_IN': self.handle_event_signed_in, 'SIGNED_IN': self.handle_event_signed_in,
'METACONTACTS': self.handle_event_metacontacts, 'METACONTACTS': self.handle_event_metacontacts,
'ATOM_ENTRY': self.handle_atom_entry,
'PRIVACY_LISTS_RECEIVED': self.handle_event_privacy_lists_received, 'PRIVACY_LISTS_RECEIVED': self.handle_event_privacy_lists_received,
'PRIVACY_LIST_RECEIVED': self.handle_event_privacy_list_received, 'PRIVACY_LIST_RECEIVED': self.handle_event_privacy_list_received,
'PRIVACY_LISTS_ACTIVE_DEFAULT': \ 'PRIVACY_LISTS_ACTIVE_DEFAULT': \