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 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 <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):
'''Called when we receive a presence'''
ptype = prs.getType()

View File

@ -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': \