From 44ab50d0802bdf31eae3d67e9fec4479db832127 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Fri, 13 Nov 2009 20:01:44 +0100 Subject: [PATCH 01/73] blacklist jingle in caps. TYhis mean that if clientr don't support caps, we assume it doesn't support jingle --- src/common/caps.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/common/caps.py b/src/common/caps.py index b4a8d2a05..f34dfcda4 100644 --- a/src/common/caps.py +++ b/src/common/caps.py @@ -37,8 +37,11 @@ import base64 import hashlib from common.xmpp import NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION, NS_CHATSTATES +from common.xmpp import NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO +from common.xmpp import NS_JINGLE_RTP_VIDEO # Features where we cannot safely assume that the other side supports them -FEATURE_BLACKLIST = [NS_CHATSTATES, NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION] +FEATURE_BLACKLIST = [NS_CHATSTATES, NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION, + NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO] # Query entry status codes NEW = 0 From e5062f77ead7bb9ac4ad80c5a5240e533708074b Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Fri, 13 Nov 2009 22:58:02 +0100 Subject: [PATCH 02/73] [Jingle] User can send DTMF tones using the /dtmf command --- src/command_system/implementation/standard.py | 16 ++++++++++ src/common/jingle.py | 30 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/command_system/implementation/standard.py b/src/command_system/implementation/standard.py index 4cb19c466..8afa044e5 100644 --- a/src/command_system/implementation/standard.py +++ b/src/command_system/implementation/standard.py @@ -98,6 +98,22 @@ class StandardChatCommands(CommandContainer): raise CommandError(_('Command is not supported for zeroconf accounts')) gajim.connections[self.account].sendPing(self.contact) + @command('dtmf') + @documentation(_("Sends DTMF events through an open audio session")) + def dtmf(self, events): + if not self.audio_sid: + raise CommandError(_("There is no open audio session with this contact")) + # Valid values for DTMF tones are *, # or a number + events = [event for event in events + if event in ('*', '#') or event.isdigit()] + if events: + session = gajim.connections[self.account].get_jingle_session( + self.contact.get_full_jid(), self.audio_sid) + content = session.get_content('audio') + content.batch_dtmf(events) + else: + raise CommandError(_("No valid DTMF event specified")) + @command('audio') @documentation(_("Toggle audio session")) def audio(self): diff --git a/src/common/jingle.py b/src/common/jingle.py index f05886533..6549ee10c 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -47,6 +47,8 @@ # * handle different kinds of sink and src elements +import gobject + import gajim import xmpp import helpers @@ -831,6 +833,7 @@ class JingleRTPContent(JingleContent): def __init__(self, session, media, node=None): JingleContent.__init__(self, session, node) self.media = media + self._dtmf_running = False self.farsight_media = {'audio': farsight.MEDIA_TYPE_AUDIO, 'video': farsight.MEDIA_TYPE_VIDEO}[media] self.got_codecs = False @@ -873,6 +876,33 @@ class JingleRTPContent(JingleContent): self.p2pstream = self.p2psession.new_stream(participant, farsight.DIRECTION_RECV, 'nice', params) + def batch_dtmf(self, events): + if self._dtmf_running: + raise Exception #TODO: Proper exception + self._dtmf_running = True + self._start_dtmf(events.pop(0)) + gobject.timeout_add(500, self._next_dtmf, events) + + def _next_dtmf(self, events): + self._stop_dtmf() + if events: + self._start_dtmf(events.pop(0)) + gobject.timeout_add(500, self._next_dtmf, events) + else: + self._dtmf_running = False + + def _start_dtmf(self, event): + if event in ('*', '#'): + event = {'*': farsight.DTMF_EVENT_STAR, + '#': farsight.DTMF_EVENT_POUND}[event] + else: + event = int(event) + self.p2psession.start_telephony_event(event, 2, + farsight.DTMF_METHOD_RTP_RFC4733) + + def _stop_dtmf(self): + self.p2psession.stop_telephony_event(farsight.DTMF_METHOD_RTP_RFC4733) + def _fillContent(self, content): content.addChild(xmpp.NS_JINGLE_RTP + ' description', attrs={'media': self.media}, payload=self.iter_codecs()) From ac5d0f24da2eb854492d8634896114193638088d Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sat, 14 Nov 2009 19:54:33 +0100 Subject: [PATCH 03/73] Fix error in the documentation of our xmpp dispatcher fork. Raise NodeProcessed if the stanza should NOT be handled by other user handlers. --- src/common/xmpp/dispatcher_nb.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/common/xmpp/dispatcher_nb.py b/src/common/xmpp/dispatcher_nb.py index 8f4746f68..477d3e6c4 100644 --- a/src/common/xmpp/dispatcher_nb.py +++ b/src/common/xmpp/dispatcher_nb.py @@ -247,25 +247,23 @@ class XMPPDispatcher(PlugIn): ''' Register user callback as stanzas handler of declared type. - Callback must take (if chained, see later) arguments: + Callback arguments: dispatcher instance (for replying), incoming return of previous handlers. The callback must raise xmpp.NodeProcessed just before return if it wants - other callbacks to be called with the same stanza as argument _and_, more - importantly library from returning stanza to sender with error set. + to prevent other callbacks to be called with the same stanza as argument + _and_, more importantly library from returning stanza to sender with error set. :param name: name of stanza. F.e. "iq". :param handler: user callback. :param typ: value of stanza's "type" attribute. If not specified any value will match :param ns: namespace of child that stanza must contain. - :param chained: chain together output of several handlers. - :param makefirst: insert handler in the beginning of handlers list instea + :param makefirst: insert handler in the beginning of handlers list instead of adding it to the end. Note that more common handlers i.e. w/o "typ" and " will be called first nevertheless. :param system: call handler even if NodeProcessed Exception were raised already. ''' - # FIXME: What does chain mean and where is it handled? if not xmlns: xmlns=self._owner.defaultNamespace log.debug('Registering handler %s for "%s" type->%s ns->%s(%s)' % From 30580702d54b92cf3f177ca62dd8171e17a7c6f1 Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sat, 14 Nov 2009 19:56:15 +0100 Subject: [PATCH 04/73] Create a ConnectionPEP class and directly register its callback to handle pep events. --- src/common/connection_handlers.py | 51 +++---------------------------- src/common/pep.py | 50 ++++++++++++++++++++++++++++++ src/common/xmpp/protocol.py | 1 + 3 files changed, 55 insertions(+), 47 deletions(-) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index e134aa003..cb9197d5e 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -46,11 +46,10 @@ import common.xmpp from common import helpers from common import gajim -from common import atom -from common import pep from common import exceptions from common.commands import ConnectionCommands from common.pubsub import ConnectionPubSub +from common.pep import ConnectionPEP from common.caps import ConnectionCaps if gajim.HAVE_FARSIGHT: from common.jingle import ConnectionJingle @@ -1453,7 +1452,7 @@ sent a message to.''' return sess -class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, ConnectionCommands, ConnectionPubSub, ConnectionCaps, ConnectionHandlersBase, ConnectionJingle): +class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, ConnectionCommands, ConnectionPubSub, ConnectionPEP, ConnectionCaps, ConnectionHandlersBase, ConnectionJingle): def __init__(self): ConnectionVcard.__init__(self) ConnectionBytestream.__init__(self) @@ -1874,15 +1873,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, def _messageCB(self, con, msg): '''Called when we receive a message''' log.debug('MessageCB') - mtype = msg.getType() - # check if the message is pubsub#event - if msg.getTag('event') is not None: - if mtype == 'groupchat': - return - if msg.getTag('error') is None: - self._pubsubEventCB(con, msg) - return # check if the message is a roster item exchange (XEP-0144) if msg.getTag('x', namespace=common.xmpp.NS_ROSTERX): @@ -2148,42 +2139,6 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, self.dispatch('GC_INVITATION',(frm, jid_from, reason, password, is_continued)) - 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...) - jid = helpers.get_full_jid_from_iq(msg) - event = msg.getTag('event') - - # XEP-0107: User Mood - items = event.getTag('items', {'node': common.xmpp.NS_MOOD}) - if items: pep.user_mood(items, self.name, jid) - # XEP-0118: User Tune - items = event.getTag('items', {'node': common.xmpp.NS_TUNE}) - if items: pep.user_tune(items, self.name, jid) - # XEP-0080: User Geolocation - items = event.getTag('items', {'node': common.xmpp.NS_GEOLOC}) - if items: pep.user_geoloc(items, self.name, jid) - # XEP-0108: User Activity - items = event.getTag('items', {'node': common.xmpp.NS_ACTIVITY}) - if items: pep.user_activity(items, self.name, jid) - # XEP-0172: User Nickname - items = event.getTag('items', {'node': common.xmpp.NS_NICK}) - if items: pep.user_nickname(items, self.name, jid) - - items = event.getTag('items') - if items is None: return - - for item in items.getTags('item'): - entry = item.getTag('entry') - if entry is not None: - # 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() @@ -2751,6 +2706,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, con.RegisterHandler('message', self._messageCB) con.RegisterHandler('presence', self._presenceCB) con.RegisterHandler('presence', self._capsPresenceCB) + con.RegisterHandler('message', self._pubsubEventCB, + ns=common.xmpp.NS_PUBSUB_EVENT) con.RegisterHandler('iq', self._vCardCB, 'result', common.xmpp.NS_VCARD) con.RegisterHandler('iq', self._rosterSetCB, 'set', diff --git a/src/common/pep.py b/src/common/pep.py index 54012514f..fe67d578a 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -192,6 +192,56 @@ ACTIVITIES = { 'studying': _('Studying'), 'writing': _('Writing')}} +import logging +log = logging.getLogger('gajim.c.pep') + +import helpers +import atom + +class ConnectionPEP: + + def _pubsubEventCB(self, xmpp_dispatcher, msg): + ''' Called when we receive with pubsub event. ''' + + if msg.getTag('error'): + log.warning('Pep Error CB') + return + + # TODO: Logging? (actually services where logging would be useful, should + # TODO: allow to access archives remotely...) + jid = helpers.get_full_jid_from_iq(msg) + event = msg.getTag('event') + + # XEP-0107: User Mood + items = event.getTag('items', {'node': common.xmpp.NS_MOOD}) + if items: user_mood(items, self.name, jid) + # XEP-0118: User Tune + items = event.getTag('items', {'node': common.xmpp.NS_TUNE}) + if items: user_tune(items, self.name, jid) + # XEP-0080: User Geolocation + items = event.getTag('items', {'node': common.xmpp.NS_GEOLOC}) + if items: user_geoloc(items, self.name, jid) + # XEP-0108: User Activity + items = event.getTag('items', {'node': common.xmpp.NS_ACTIVITY}) + if items: user_activity(items, self.name, jid) + # XEP-0172: User Nickname + items = event.getTag('items', {'node': common.xmpp.NS_NICK}) + if items: user_nickname(items, self.name, jid) + + items = event.getTag('items') + if items is None: return + + for item in items.getTags('item'): + entry = item.getTag('entry') + if entry is not None: + # 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 user_mood(items, name, jid): has_child = False retract = False diff --git a/src/common/xmpp/protocol.py b/src/common/xmpp/protocol.py index 619771409..5eb31b7d3 100644 --- a/src/common/xmpp/protocol.py +++ b/src/common/xmpp/protocol.py @@ -88,6 +88,7 @@ NS_PRIVACY ='jabber:iq:privacy' NS_PRIVATE ='jabber:iq:private' NS_PROFILE ='http://jabber.org/protocol/profile' # XEP-0154 NS_PUBSUB ='http://jabber.org/protocol/pubsub' # XEP-0060 +NS_PUBSUB_EVENT = 'http://jabber.org/protocol/pubsub#event' NS_PUBSUB_PUBLISH_OPTIONS = NS_PUBSUB + '#publish-options' # XEP-0060 NS_PUBSUB_OWNER ='http://jabber.org/protocol/pubsub#owner' # JEP-0060 NS_REGISTER ='jabber:iq:register' From 85b7b89b49a7db6b077e1c01a3811d92556e3092 Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sat, 14 Nov 2009 20:48:33 +0100 Subject: [PATCH 05/73] Create a class for each PEP XEP that we support. Dispatch an event to the Interface() handlers when we have have received a PEP event. --- src/common/pep.py | 100 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 79 insertions(+), 21 deletions(-) diff --git a/src/common/pep.py b/src/common/pep.py index fe67d578a..03f262ce3 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -198,37 +198,94 @@ log = logging.getLogger('gajim.c.pep') import helpers import atom + +class AbstractPEP(object): + + type = '' + namespace = '' + + @classmethod + def get_tag_as_PEP(cls, jid, account, event_tag): + items = event_tag.getTag('items', {'node': cls.namespace}) + if items: + log.debug("Received PEP 'user %s' from %s" % (cls.type, jid)) + return cls(jid, account, items) + else: + return None + + +class UserMoodPEP(AbstractPEP): + '''XEP-0107: User Mood''' + + type = 'mood' + namespace = common.xmpp.NS_MOOD + + def __init__(self, jid, account, items): + user_mood(items, account, jid) + + +class UserTunePEP(AbstractPEP): + '''XEP-0118: User Tune''' + + type = 'tune' + namespace = common.xmpp.NS_TUNE + + def __init__(self, jid, account, items): + user_tune(items, account, jid) + + +class UserGeolocationPEP(AbstractPEP): + '''XEP-0080: User Geolocation''' + + type = 'geolocation' + namespace = common.xmpp.NS_GEOLOC + + def __init__(self, jid, account, items): + user_geoloc(items, account, jid) + + +class UserActivityPEP(AbstractPEP): + '''XEP-0108: User Activity''' + + type = 'activity' + namespace = common.xmpp.NS_ACTIVITY + + def __init__(self, jid, account, items): + user_activity(items, account, jid) + + +class UserNicknamePEP(AbstractPEP): + '''XEP-0172: User Nickname''' + + type = 'activity' + namespace = common.xmpp.NS_NICK + + def __init__(self, jid, account, items): + user_nickname(items, self.name, jid) + + +SUPPORTED_PERSONAL_USER_EVENTS = [UserMoodPEP, UserTunePEP, UserGeolocationPEP, + UserActivityPEP, UserNicknamePEP] + class ConnectionPEP: def _pubsubEventCB(self, xmpp_dispatcher, msg): ''' Called when we receive with pubsub event. ''' - if msg.getTag('error'): - log.warning('Pep Error CB') + log.warning('PubsubEventCB received error stanza') return - + # TODO: Logging? (actually services where logging would be useful, should # TODO: allow to access archives remotely...) jid = helpers.get_full_jid_from_iq(msg) - event = msg.getTag('event') + event_tag = msg.getTag('event') - # XEP-0107: User Mood - items = event.getTag('items', {'node': common.xmpp.NS_MOOD}) - if items: user_mood(items, self.name, jid) - # XEP-0118: User Tune - items = event.getTag('items', {'node': common.xmpp.NS_TUNE}) - if items: user_tune(items, self.name, jid) - # XEP-0080: User Geolocation - items = event.getTag('items', {'node': common.xmpp.NS_GEOLOC}) - if items: user_geoloc(items, self.name, jid) - # XEP-0108: User Activity - items = event.getTag('items', {'node': common.xmpp.NS_ACTIVITY}) - if items: user_activity(items, self.name, jid) - # XEP-0172: User Nickname - items = event.getTag('items', {'node': common.xmpp.NS_NICK}) - if items: user_nickname(items, self.name, jid) - - items = event.getTag('items') + for pep_class in SUPPORTED_PERSONAL_USER_EVENTS: + pep = pep_class.get_tag_as_PEP(jid, self.name, event_tag) + if pep: + self.dispatch('PEP_RECEIVED', (pep.type, pep)) + + items = event_tag.getTag('items') if items is None: return for item in items.getTags('item'): @@ -242,6 +299,7 @@ class ConnectionPEP: raise common.xmpp.NodeProcessed + def user_mood(items, name, jid): has_child = False retract = False From 7c6dc424afc4039ee92c0314c7008ae87928ed35 Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sat, 14 Nov 2009 20:54:42 +0100 Subject: [PATCH 06/73] Make user_tune a instance method, not a function. --- src/common/pep.py | 119 +++++++++++++++++++++++----------------------- 1 file changed, 60 insertions(+), 59 deletions(-) diff --git a/src/common/pep.py b/src/common/pep.py index 03f262ce3..7f04f4139 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -221,7 +221,66 @@ class UserMoodPEP(AbstractPEP): namespace = common.xmpp.NS_MOOD def __init__(self, jid, account, items): - user_mood(items, account, jid) + self.user_mood(items, account, jid) + + + def user_mood(self, items, name, jid): + has_child = False + retract = False + mood = None + text = None + for item in items.getTags('item'): + child = item.getTag('mood') + if child is not None: + has_child = True + for ch in child.getChildren(): + if ch.getName() != 'text': + mood = ch.getName() + else: + text = ch.getData() + if items.getTag('retract') is not None: + retract = True + + if jid == common.gajim.get_jid_from_account(name): + acc = common.gajim.connections[name] + if has_child: + if 'mood' in acc.mood: + del acc.mood['mood'] + if 'text' in acc.mood: + del acc.mood['text'] + if mood is not None: + acc.mood['mood'] = mood + if text is not None: + acc.mood['text'] = text + elif retract: + if 'mood' in acc.mood: + del acc.mood['mood'] + if 'text' in acc.mood: + del acc.mood['text'] + + (user, resource) = common.gajim.get_room_and_nick_from_fjid(jid) + for contact in common.gajim.contacts.get_contacts(name, user): + if has_child: + if 'mood' in contact.mood: + del contact.mood['mood'] + if 'text' in contact.mood: + del contact.mood['text'] + if mood is not None: + contact.mood['mood'] = mood + if text is not None: + contact.mood['text'] = text + elif retract: + if 'mood' in contact.mood: + del contact.mood['mood'] + if 'text' in contact.mood: + del contact.mood['text'] + + if jid == common.gajim.get_jid_from_account(name): + common.gajim.interface.roster.draw_account(name) + common.gajim.interface.roster.draw_mood(user, name) + ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) + if ctrl: + ctrl.update_mood() class UserTunePEP(AbstractPEP): @@ -300,64 +359,6 @@ class ConnectionPEP: raise common.xmpp.NodeProcessed -def user_mood(items, name, jid): - has_child = False - retract = False - mood = None - text = None - for item in items.getTags('item'): - child = item.getTag('mood') - if child is not None: - has_child = True - for ch in child.getChildren(): - if ch.getName() != 'text': - mood = ch.getName() - else: - text = ch.getData() - if items.getTag('retract') is not None: - retract = True - - if jid == common.gajim.get_jid_from_account(name): - acc = common.gajim.connections[name] - if has_child: - if 'mood' in acc.mood: - del acc.mood['mood'] - if 'text' in acc.mood: - del acc.mood['text'] - if mood is not None: - acc.mood['mood'] = mood - if text is not None: - acc.mood['text'] = text - elif retract: - if 'mood' in acc.mood: - del acc.mood['mood'] - if 'text' in acc.mood: - del acc.mood['text'] - - (user, resource) = common.gajim.get_room_and_nick_from_fjid(jid) - for contact in common.gajim.contacts.get_contacts(name, user): - if has_child: - if 'mood' in contact.mood: - del contact.mood['mood'] - if 'text' in contact.mood: - del contact.mood['text'] - if mood is not None: - contact.mood['mood'] = mood - if text is not None: - contact.mood['text'] = text - elif retract: - if 'mood' in contact.mood: - del contact.mood['mood'] - if 'text' in contact.mood: - del contact.mood['text'] - - if jid == common.gajim.get_jid_from_account(name): - common.gajim.interface.roster.draw_account(name) - common.gajim.interface.roster.draw_mood(user, name) - ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) - if ctrl: - ctrl.update_mood() - def user_tune(items, name, jid): has_child = False retract = False From 99e718583aaedebe03bbd0e8efd6a87f605d860f Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sat, 14 Nov 2009 22:31:27 +0100 Subject: [PATCH 07/73] Initial simplification of PEP data extraction by moving the extraction methods to the newly created PEP classess. If-else-retract complexity is substituted by dictionaries. --- src/common/pep.py | 407 +++++++++++++++------------------------------- 1 file changed, 131 insertions(+), 276 deletions(-) diff --git a/src/common/pep.py b/src/common/pep.py index 7f04f4139..9e99a5a24 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -192,6 +192,8 @@ ACTIVITIES = { 'studying': _('Studying'), 'writing': _('Writing')}} +TUNE_DATA = ['artist', 'title', 'source', 'track', 'length'] + import logging log = logging.getLogger('gajim.c.pep') @@ -221,59 +223,36 @@ class UserMoodPEP(AbstractPEP): namespace = common.xmpp.NS_MOOD def __init__(self, jid, account, items): + self._mood_dict, self._retracted = self._extract_info(items) + self.user_mood(items, account, jid) + def _extract_info(self, items): + mood_dict = {} - def user_mood(self, items, name, jid): - has_child = False - retract = False - mood = None - text = None for item in items.getTags('item'): - child = item.getTag('mood') - if child is not None: - has_child = True - for ch in child.getChildren(): - if ch.getName() != 'text': - mood = ch.getName() - else: - text = ch.getData() - if items.getTag('retract') is not None: - retract = True + mood_tag = item.getTag('mood') + if mood_tag: + for child in mood_tag.getChildren(): + if child.getName() == 'text': + mood_dict['text'] = child.getData() + elif child.getName() in MOODS : + mood_dict['mood'] = child.getName() + + retracted = items.getTag('retract') or not mood_dict + return (mood_dict, retracted) + + def user_mood(self, items, name, jid): + + mood_dict = {} if self._retracted else self._mood_dict if jid == common.gajim.get_jid_from_account(name): acc = common.gajim.connections[name] - if has_child: - if 'mood' in acc.mood: - del acc.mood['mood'] - if 'text' in acc.mood: - del acc.mood['text'] - if mood is not None: - acc.mood['mood'] = mood - if text is not None: - acc.mood['text'] = text - elif retract: - if 'mood' in acc.mood: - del acc.mood['mood'] - if 'text' in acc.mood: - del acc.mood['text'] + acc.mood = mood_dict - (user, resource) = common.gajim.get_room_and_nick_from_fjid(jid) + user = common.gajim.get_room_and_nick_from_fjid(jid)[0] for contact in common.gajim.contacts.get_contacts(name, user): - if has_child: - if 'mood' in contact.mood: - del contact.mood['mood'] - if 'text' in contact.mood: - del contact.mood['text'] - if mood is not None: - contact.mood['mood'] = mood - if text is not None: - contact.mood['text'] = text - elif retract: - if 'mood' in contact.mood: - del contact.mood['mood'] - if 'text' in contact.mood: - del contact.mood['text'] + contact.mood = mood_dict if jid == common.gajim.get_jid_from_account(name): common.gajim.interface.roster.draw_account(name) @@ -290,18 +269,42 @@ class UserTunePEP(AbstractPEP): namespace = common.xmpp.NS_TUNE def __init__(self, jid, account, items): - user_tune(items, account, jid) - + self._tune_dict, self._retracted = self._extract_info(items) -class UserGeolocationPEP(AbstractPEP): - '''XEP-0080: User Geolocation''' + self.user_tune(items, account, jid) + + def _extract_info(self, items): + tune_dict = {} - type = 'geolocation' - namespace = common.xmpp.NS_GEOLOC - - def __init__(self, jid, account, items): - user_geoloc(items, account, jid) + for item in items.getTags('item'): + tune_tag = item.getTag('tune') + if tune_tag: + for child in tune_tag.getChildren(): + if child.getName() in TUNE_DATA: + tune_dict[child.getName()] = child.getData() + + retracted = items.getTag('retract') or not tune_dict + return (tune_dict, retracted) + + + def user_tune(self, items, name, jid): + tune_dict = {} if self._retracted else self._tune_dict + if jid == common.gajim.get_jid_from_account(name): + acc = common.gajim.connections[name] + acc.tune = tune_dict + + user = common.gajim.get_room_and_nick_from_fjid(jid)[0] + for contact in common.gajim.contacts.get_contacts(name, user): + contact.tune = tune_dict + + if jid == common.gajim.get_jid_from_account(name): + common.gajim.interface.roster.draw_account(name) + common.gajim.interface.roster.draw_tune(user, name) + ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) + if ctrl: + ctrl.update_tune() + class UserActivityPEP(AbstractPEP): '''XEP-0108: User Activity''' @@ -310,7 +313,45 @@ class UserActivityPEP(AbstractPEP): namespace = common.xmpp.NS_ACTIVITY def __init__(self, jid, account, items): - user_activity(items, account, jid) + self._activity_dict, self._retracted = self._extract_info(items) + + self.user_activity(items, account, jid) + + def _extract_info(self, items): + activity_dict = {} + + for item in items.getTags('item'): + activity_tag = item.getTag('activity') + if activity_tag: + for child in child.getChildren(): + if child.getName() == 'text': + activity_dict['text'] = child.getData() + elif child.getName() in ACTIVITIES: + activity_dict['activity'] = child.getName() + for subactivity in child.getChildren(): + if subactivity.getName() in ACTIVITIES[child.getName()]: + activity_dict['subactivity'] = subactivity.getName() + + retracted = items.getTag('retract') or not activity_dict + return (activity_dict, retracted) + + def user_activity(self, items, name, jid): + activity_dict = {} if self._retracted else self._activity_dict + + if jid == common.gajim.get_jid_from_account(name): + acc = common.gajim.connections[name] + acc.activity = activity_dict + + user = common.gajim.get_room_and_nick_from_fjid(jid)[0] + for contact in common.gajim.contacts.get_contacts(name, user): + contact.activity = activity_dict + + if jid == common.gajim.get_jid_from_account(name): + common.gajim.interface.roster.draw_account(name) + common.gajim.interface.roster.draw_activity(user, name) + ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) + if ctrl: + ctrl.update_activity() class UserNicknamePEP(AbstractPEP): @@ -320,11 +361,44 @@ class UserNicknamePEP(AbstractPEP): namespace = common.xmpp.NS_NICK def __init__(self, jid, account, items): - user_nickname(items, self.name, jid) + self._nick, self._retracted = self._extract_info(items) + self.user_nickname(items, account, jid) + + def _extract_info(self, items): + nick = '' + for item in items.getTags('item'): + child = item.getTag('nick') + if child: + nick = child.getData() + break + + retracted = items.getTag('retract') or not nick + return (nick, retracted) + + def user_nickname(self, items, name, jid): + if jid == common.gajim.get_jid_from_account(name): + if self._retracted: + common.gajim.nicks[name] = common.gajim.config.get_per('accounts', + name, 'name') + else: + common.gajim.nicks[name] = self._nick + + nick = '' if self._retracted else self._nick + + user = common.gajim.get_room_and_nick_from_fjid(jid)[0] + for contact in common.gajim.contacts.get_contacts(name, user): + contact.contact_name = nick + common.gajim.interface.roster.draw_contact(user, name) + + ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) + if ctrl: + ctrl.update_ui() + win = ctrl.parent_win + win.redraw_tab(ctrl) + win.show_title() -SUPPORTED_PERSONAL_USER_EVENTS = [UserMoodPEP, UserTunePEP, UserGeolocationPEP, - UserActivityPEP, UserNicknamePEP] +SUPPORTED_PERSONAL_USER_EVENTS = [UserMoodPEP, UserTunePEP, UserActivityPEP, UserNicknamePEP] class ConnectionPEP: @@ -359,225 +433,6 @@ class ConnectionPEP: raise common.xmpp.NodeProcessed -def user_tune(items, name, jid): - has_child = False - retract = False - artist = None - title = None - source = None - track = None - length = None - - for item in items.getTags('item'): - child = item.getTag('tune') - if child is not None: - has_child = True - for ch in child.getChildren(): - if ch.getName() == 'artist': - artist = ch.getData() - elif ch.getName() == 'title': - title = ch.getData() - elif ch.getName() == 'source': - source = ch.getData() - elif ch.getName() == 'track': - track = ch.getData() - elif ch.getName() == 'length': - length = ch.getData() - if items.getTag('retract') is not None: - retract = True - - if jid == common.gajim.get_jid_from_account(name): - acc = common.gajim.connections[name] - if has_child: - if 'artist' in acc.tune: - del acc.tune['artist'] - if 'title' in acc.tune: - del acc.tune['title'] - if 'source' in acc.tune: - del acc.tune['source'] - if 'track' in acc.tune: - del acc.tune['track'] - if 'length' in acc.tune: - del acc.tune['length'] - if artist is not None: - acc.tune['artist'] = artist - if title is not None: - acc.tune['title'] = title - if source is not None: - acc.tune['source'] = source - if track is not None: - acc.tune['track'] = track - if length is not None: - acc.tune['length'] = length - elif retract: - if 'artist' in acc.tune: - del acc.tune['artist'] - if 'title' in acc.tune: - del acc.tune['title'] - if 'source' in acc.tune: - del acc.tune['source'] - if 'track' in acc.tune: - del acc.tune['track'] - if 'length' in acc.tune: - del acc.tune['length'] - - user = common.gajim.get_room_and_nick_from_fjid(jid)[0] - for contact in common.gajim.contacts.get_contacts(name, user): - if has_child: - if 'artist' in contact.tune: - del contact.tune['artist'] - if 'title' in contact.tune: - del contact.tune['title'] - if 'source' in contact.tune: - del contact.tune['source'] - if 'track' in contact.tune: - del contact.tune['track'] - if 'length' in contact.tune: - del contact.tune['length'] - if artist is not None: - contact.tune['artist'] = artist - if title is not None: - contact.tune['title'] = title - if source is not None: - contact.tune['source'] = source - if track is not None: - contact.tune['track'] = track - if length is not None: - contact.tune['length'] = length - elif retract: - if 'artist' in contact.tune: - del contact.tune['artist'] - if 'title' in contact.tune: - del contact.tune['title'] - if 'source' in contact.tune: - del contact.tune['source'] - if 'track' in contact.tune: - del contact.tune['track'] - if 'length' in contact.tune: - del contact.tune['length'] - - if jid == common.gajim.get_jid_from_account(name): - common.gajim.interface.roster.draw_account(name) - common.gajim.interface.roster.draw_tune(user, name) - ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) - if ctrl: - ctrl.update_tune() - -def user_geoloc(items, name, jid): - pass - -def user_activity(items, name, jid): - has_child = False - retract = False - activity = None - subactivity = None - text = None - - for item in items.getTags('item'): - child = item.getTag('activity') - if child is not None: - has_child = True - for ch in child.getChildren(): - if ch.getName() != 'text': - activity = ch.getName() - for chi in ch.getChildren(): - subactivity = chi.getName() - else: - text = ch.getData() - if items.getTag('retract') is not None: - retract = True - - if jid == common.gajim.get_jid_from_account(name): - acc = common.gajim.connections[name] - if has_child: - if 'activity' in acc.activity: - del acc.activity['activity'] - if 'subactivity' in acc.activity: - del acc.activity['subactivity'] - if 'text' in acc.activity: - del acc.activity['text'] - if activity is not None: - acc.activity['activity'] = activity - if subactivity is not None and subactivity != 'other': - acc.activity['subactivity'] = subactivity - if text is not None: - acc.activity['text'] = text - elif retract: - if 'activity' in acc.activity: - del acc.activity['activity'] - if 'subactivity' in acc.activity: - del acc.activity['subactivity'] - if 'text' in acc.activity: - del acc.activity['text'] - - user = common.gajim.get_room_and_nick_from_fjid(jid)[0] - for contact in common.gajim.contacts.get_contacts(name, user): - if has_child: - if 'activity' in contact.activity: - del contact.activity['activity'] - if 'subactivity' in contact.activity: - del contact.activity['subactivity'] - if 'text' in contact.activity: - del contact.activity['text'] - if activity is not None: - contact.activity['activity'] = activity - if subactivity is not None and subactivity != 'other': - contact.activity['subactivity'] = subactivity - if text is not None: - contact.activity['text'] = text - elif retract: - if 'activity' in contact.activity: - del contact.activity['activity'] - if 'subactivity' in contact.activity: - del contact.activity['subactivity'] - if 'text' in contact.activity: - del contact.activity['text'] - - if jid == common.gajim.get_jid_from_account(name): - common.gajim.interface.roster.draw_account(name) - common.gajim.interface.roster.draw_activity(user, name) - ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) - if ctrl: - ctrl.update_activity() - -def user_nickname(items, name, jid): - has_child = False - retract = False - nick = None - - for item in items.getTags('item'): - child = item.getTag('nick') - if child is not None: - has_child = True - nick = child.getData() - break - - if items.getTag('retract') is not None: - retract = True - - if jid == common.gajim.get_jid_from_account(name): - if has_child: - common.gajim.nicks[name] = nick - if retract: - common.gajim.nicks[name] = common.gajim.config.get_per('accounts', - name, 'name') - - user = common.gajim.get_room_and_nick_from_fjid(jid)[0] - if has_child: - if nick is not None: - for contact in common.gajim.contacts.get_contacts(name, user): - contact.contact_name = nick - common.gajim.interface.roster.draw_contact(user, name) - - ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) - if ctrl: - ctrl.update_ui() - win = ctrl.parent_win - win.redraw_tab(ctrl) - win.show_title() - elif retract: - contact.contact_name = '' - def user_send_mood(account, mood, message=''): if not common.gajim.connections[account].pep_supported: return From 3d5e8cc42737418a237d3f5812834d2b9ef9acce Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sat, 14 Nov 2009 22:47:21 +0100 Subject: [PATCH 08/73] Move common pep constructor logic to base class. --- src/common/pep.py | 55 ++++++++++++++++++----------------------------- 1 file changed, 21 insertions(+), 34 deletions(-) diff --git a/src/common/pep.py b/src/common/pep.py index 9e99a5a24..4b31fb58e 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -205,7 +205,7 @@ class AbstractPEP(object): type = '' namespace = '' - + @classmethod def get_tag_as_PEP(cls, jid, account, event_tag): items = event_tag.getTag('items', {'node': cls.namespace}) @@ -215,17 +215,19 @@ class AbstractPEP(object): else: return None + def __init__(self, jid, account, items): + self._pep_specific_data, self._retracted = self._extract_info(items) + self.do(jid, account) + + def _extract_info(self, items): + '''To be implemented by subclasses''' + raise NotImplementedError class UserMoodPEP(AbstractPEP): '''XEP-0107: User Mood''' type = 'mood' namespace = common.xmpp.NS_MOOD - - def __init__(self, jid, account, items): - self._mood_dict, self._retracted = self._extract_info(items) - - self.user_mood(items, account, jid) def _extract_info(self, items): mood_dict = {} @@ -242,9 +244,8 @@ class UserMoodPEP(AbstractPEP): retracted = items.getTag('retract') or not mood_dict return (mood_dict, retracted) - def user_mood(self, items, name, jid): - - mood_dict = {} if self._retracted else self._mood_dict + def do(self, jid, name): + mood_dict = {} if self._retracted else self._pep_specific_data if jid == common.gajim.get_jid_from_account(name): acc = common.gajim.connections[name] @@ -267,12 +268,7 @@ class UserTunePEP(AbstractPEP): type = 'tune' namespace = common.xmpp.NS_TUNE - - def __init__(self, jid, account, items): - self._tune_dict, self._retracted = self._extract_info(items) - - self.user_tune(items, account, jid) - + def _extract_info(self, items): tune_dict = {} @@ -287,8 +283,8 @@ class UserTunePEP(AbstractPEP): return (tune_dict, retracted) - def user_tune(self, items, name, jid): - tune_dict = {} if self._retracted else self._tune_dict + def do(self, jid, name): + tune_dict = {} if self._retracted else self._pep_specific_data if jid == common.gajim.get_jid_from_account(name): acc = common.gajim.connections[name] @@ -311,19 +307,14 @@ class UserActivityPEP(AbstractPEP): type = 'activity' namespace = common.xmpp.NS_ACTIVITY - - def __init__(self, jid, account, items): - self._activity_dict, self._retracted = self._extract_info(items) - - self.user_activity(items, account, jid) - + def _extract_info(self, items): activity_dict = {} for item in items.getTags('item'): activity_tag = item.getTag('activity') if activity_tag: - for child in child.getChildren(): + for child in activity_tag.getChildren(): if child.getName() == 'text': activity_dict['text'] = child.getData() elif child.getName() in ACTIVITIES: @@ -335,8 +326,8 @@ class UserActivityPEP(AbstractPEP): retracted = items.getTag('retract') or not activity_dict return (activity_dict, retracted) - def user_activity(self, items, name, jid): - activity_dict = {} if self._retracted else self._activity_dict + def do(self, jid, name): + activity_dict = {} if self._retracted else self._pep_specific_data if jid == common.gajim.get_jid_from_account(name): acc = common.gajim.connections[name] @@ -359,11 +350,7 @@ class UserNicknamePEP(AbstractPEP): type = 'activity' namespace = common.xmpp.NS_NICK - - def __init__(self, jid, account, items): - self._nick, self._retracted = self._extract_info(items) - self.user_nickname(items, account, jid) - + def _extract_info(self, items): nick = '' for item in items.getTags('item'): @@ -375,15 +362,15 @@ class UserNicknamePEP(AbstractPEP): retracted = items.getTag('retract') or not nick return (nick, retracted) - def user_nickname(self, items, name, jid): + def do(self, jid, name): if jid == common.gajim.get_jid_from_account(name): if self._retracted: common.gajim.nicks[name] = common.gajim.config.get_per('accounts', name, 'name') else: - common.gajim.nicks[name] = self._nick + common.gajim.nicks[name] = self._pep_specific_data - nick = '' if self._retracted else self._nick + nick = '' if self._retracted else self._pep_specific_data user = common.gajim.get_room_and_nick_from_fjid(jid)[0] for contact in common.gajim.contacts.get_contacts(name, user): From 088916f4e7709133023cd308972c938787376ace Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sat, 14 Nov 2009 23:07:22 +0100 Subject: [PATCH 09/73] Strip PEP info at the network level. (Currently it is done at the UI level in many, many different places) --- src/common/pep.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/common/pep.py b/src/common/pep.py index 4b31fb58e..bd17e46ba 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -236,10 +236,11 @@ class UserMoodPEP(AbstractPEP): mood_tag = item.getTag('mood') if mood_tag: for child in mood_tag.getChildren(): - if child.getName() == 'text': + name = child.getName().strip() + if name == 'text': mood_dict['text'] = child.getData() - elif child.getName() in MOODS : - mood_dict['mood'] = child.getName() + elif name in MOODS : + mood_dict['mood'] = name retracted = items.getTag('retract') or not mood_dict return (mood_dict, retracted) @@ -276,8 +277,10 @@ class UserTunePEP(AbstractPEP): tune_tag = item.getTag('tune') if tune_tag: for child in tune_tag.getChildren(): + name = child.getName().strip() + data = child.getData().strip() if child.getName() in TUNE_DATA: - tune_dict[child.getName()] = child.getData() + tune_dict[name] = data retracted = items.getTag('retract') or not tune_dict return (tune_dict, retracted) @@ -315,13 +318,16 @@ class UserActivityPEP(AbstractPEP): activity_tag = item.getTag('activity') if activity_tag: for child in activity_tag.getChildren(): - if child.getName() == 'text': - activity_dict['text'] = child.getData() - elif child.getName() in ACTIVITIES: - activity_dict['activity'] = child.getName() + name = child.getName().strip() + data = child.getData().strip() + if name == 'text': + activity_dict['text'] = data + elif name in ACTIVITIES: + activity_dict['activity'] = name for subactivity in child.getChildren(): - if subactivity.getName() in ACTIVITIES[child.getName()]: - activity_dict['subactivity'] = subactivity.getName() + subactivity_name = subactivity.getName().strip() + if subactivity_name in ACTIVITIES[name]: + activity_dict['subactivity'] = subactivity_name retracted = items.getTag('retract') or not activity_dict return (activity_dict, retracted) From e41e4848559c0446107cc378d15d8887daaea129 Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sun, 15 Nov 2009 00:12:15 +0100 Subject: [PATCH 10/73] Proof of concept: Move markup / pixbuf determination logic from the UI to the different PEP classes. Currently this is only done for UserMood. We can decide later on (if needed), to move the asPixbufIcon and asMarkupText methods to a more appropriate place. Goal is to remove as much redundant code as possible. --- src/chat_control.py | 31 +++++-------------------------- src/common/contacts.py | 1 + src/common/pep.py | 36 ++++++++++++++++++++++++++++++++---- 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 04d6abc00..fc59c2444 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1432,34 +1432,13 @@ class ChatControl(ChatControlBase): self._convert_to_gc_button.set_sensitive(False) def update_mood(self): - mood = None - text = None - if isinstance(self.contact, GC_Contact): return - - if 'mood' in self.contact.mood: - mood = self.contact.mood['mood'].strip() - if 'text' in self.contact.mood: - text = self.contact.mood['text'].strip() - - if mood is not None: - if mood in MOODS: - self._mood_image.set_from_pixbuf(gtkgui_helpers.load_mood_icon( - mood).get_pixbuf()) - # Translate standard moods - mood = MOODS[mood] - else: - self._mood_image.set_from_pixbuf(gtkgui_helpers.load_mood_icon( - 'unknown').get_pixbuf()) - - mood = gobject.markup_escape_text(mood) - - tooltip = '%s' % mood - if text: - text = gobject.markup_escape_text(text) - tooltip += '\n' + text - self._mood_image.set_tooltip_markup(tooltip) + + pep = self.contact.pep + if 'mood' in pep and not pep['mood'].was_retracted(): + self._mood_image.set_from_pixbuf(pep['mood'].asPixbufIcon()) + self._mood_image.set_tooltip_markup(pep['mood'].asMarkup()) self._mood_image.show() else: self._mood_image.hide() diff --git a/src/common/contacts.py b/src/common/contacts.py index a4a64a0dd..b8b039288 100644 --- a/src/common/contacts.py +++ b/src/common/contacts.py @@ -110,6 +110,7 @@ class Contact(CommonContact): self.msg_id = msg_id self.last_status_time = last_status_time + self.pep = {} self.mood = mood.copy() self.tune = tune.copy() self.activity = activity.copy() diff --git a/src/common/pep.py b/src/common/pep.py index bd17e46ba..f11021ec7 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -197,8 +197,10 @@ TUNE_DATA = ['artist', 'title', 'source', 'track', 'length'] import logging log = logging.getLogger('gajim.c.pep') -import helpers -import atom +import common.helpers +import common.atom +import gtkgui_helpers +import gobject class AbstractPEP(object): @@ -223,6 +225,9 @@ class AbstractPEP(object): '''To be implemented by subclasses''' raise NotImplementedError + def was_rectacted(self): + return self._retracted + class UserMoodPEP(AbstractPEP): '''XEP-0107: User Mood''' @@ -239,10 +244,10 @@ class UserMoodPEP(AbstractPEP): name = child.getName().strip() if name == 'text': mood_dict['text'] = child.getData() - elif name in MOODS : + else: mood_dict['mood'] = name - retracted = items.getTag('retract') or not mood_dict + retracted = items.getTag('retract') or not 'mood' in mood_dict return (mood_dict, retracted) def do(self, jid, name): @@ -255,6 +260,7 @@ class UserMoodPEP(AbstractPEP): user = common.gajim.get_room_and_nick_from_fjid(jid)[0] for contact in common.gajim.contacts.get_contacts(name, user): contact.mood = mood_dict + contact.pep['mood'] = self if jid == common.gajim.get_jid_from_account(name): common.gajim.interface.roster.draw_account(name) @@ -262,7 +268,29 @@ class UserMoodPEP(AbstractPEP): ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) if ctrl: ctrl.update_mood() + + + def asPixbufIcon(self): + if self._retracted: + return None + else: + received_mood = self._pep_specific_data['mood'] + mood = received_mood if received_mood in MOODS else 'unknown' + pixbuf = gtkgui_helpers.load_mood_icon(mood).get_pixbuf() + return pixbuf + def asMarkupText(self): + if self._retracted: + return None + else: + untranslated_mood = self._pep_specific_data['mood'] + mood = MOODS[untranslated_mood] if untranslated_mood in MOODS else untranslated_mood + markuptext = '%s' % gobject.markup_escape_text(mood) + if 'text' in self._pep_specific_data: + text = self._pep_specific_data['text'] + markuptext += '(%s)' + gobject.markup_escape_text(text) + return markuptext + class UserTunePEP(AbstractPEP): '''XEP-0118: User Tune''' From 234a6520dd4a2639a3fdada45d9d9fe911a1fb5e Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sun, 15 Nov 2009 00:15:31 +0100 Subject: [PATCH 11/73] Removed unused code. --- src/common/helpers.py | 49 ------------------------------------------- src/common/pep.py | 4 ++-- 2 files changed, 2 insertions(+), 51 deletions(-) diff --git a/src/common/helpers.py b/src/common/helpers.py index 70bdfc9fe..341ebb15f 100644 --- a/src/common/helpers.py +++ b/src/common/helpers.py @@ -569,8 +569,6 @@ def datetime_tuple(timestamp): # import gajim only when needed (after decode_string is defined) see #4764 import gajim -import pep - def convert_bytes(string): suffix = '' @@ -777,53 +775,6 @@ def get_global_status(): status = gajim.connections[account].status return status -def get_pep_dict(account): - pep_dict = {} - con = gajim.connections[account] - # activity - if 'activity' in con.activity and con.activity['activity'] in pep.ACTIVITIES: - activity = con.activity['activity'] - if 'subactivity' in con.activity and con.activity['subactivity'] in \ - pep.ACTIVITIES[activity]: - subactivity = con.activity['subactivity'] - else: - subactivity = 'other' - else: - activity = '' - subactivity = '' - if 'text' in con.activity: - text = con.activity['text'] - else: - text = '' - pep_dict['activity'] = activity - pep_dict['subactivity'] = subactivity - pep_dict['activity_text'] = text - - # mood - if 'mood' in con.mood and con.mood['mood'] in pep.MOODS: - mood = con.mood['mood'] - else: - mood = '' - if 'text' in con.mood: - text = con.mood['text'] - else: - text = '' - pep_dict['mood'] = mood - pep_dict['mood_text'] = text - return pep_dict - -def get_global_pep(): - maxi = 0 - pep_dict = {'activity': '', 'mood': ''} - for account in gajim.connections: - if not gajim.config.get_per('accounts', account, - 'sync_with_global_status'): - continue - connected = gajim.connections[account].connected - if connected > maxi: - maxi = connected - pep_dict = get_pep_dict(account) - return pep_dict def statuses_unified(): '''testing if all statuses are the same.''' diff --git a/src/common/pep.py b/src/common/pep.py index f11021ec7..e32c2c444 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -197,8 +197,8 @@ TUNE_DATA = ['artist', 'title', 'source', 'track', 'length'] import logging log = logging.getLogger('gajim.c.pep') -import common.helpers -import common.atom +import helpers +import atom import gtkgui_helpers import gobject From 5f4db2eed9c93e378317479cf3f2b28e6758017d Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sun, 15 Nov 2009 10:55:31 +0100 Subject: [PATCH 12/73] Unify updating of accounts and contact pep information. Implement and use asMarkupText() for tunes. --- src/chat_control.py | 36 ++-------- src/common/connection.py | 1 + src/common/pep.py | 144 +++++++++++++++++++++++---------------- src/roster_window.py | 11 +-- src/tooltips.py | 36 ++-------- 5 files changed, 101 insertions(+), 127 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index fc59c2444..ab6f10213 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1434,11 +1434,10 @@ class ChatControl(ChatControlBase): def update_mood(self): if isinstance(self.contact, GC_Contact): return - pep = self.contact.pep - if 'mood' in pep and not pep['mood'].was_retracted(): + if 'mood' in pep: self._mood_image.set_from_pixbuf(pep['mood'].asPixbufIcon()) - self._mood_image.set_tooltip_markup(pep['mood'].asMarkup()) + self._mood_image.set_tooltip_markup(pep['mood'].asMarkupText()) self._mood_image.show() else: self._mood_image.hide() @@ -1489,35 +1488,12 @@ class ChatControl(ChatControlBase): self._activity_image.hide() def update_tune(self): - artist = None - title = None - source = None - if isinstance(self.contact, GC_Contact): return - - if 'artist' in self.contact.tune: - artist = self.contact.tune['artist'].strip() - artist = gobject.markup_escape_text(artist) - if 'title' in self.contact.tune: - title = self.contact.tune['title'].strip() - title = gobject.markup_escape_text(title) - if 'source' in self.contact.tune: - source = self.contact.tune['source'].strip() - source = gobject.markup_escape_text(source) - - if artist or title: - if not artist: - artist = _('Unknown Artist') - if not title: - title = _('Unknown Title') - if not source: - source = _('Unknown Source') - - self._tune_image.set_tooltip_markup( - _('"%(title)s" by %(artist)s\n' - 'from %(source)s') % {'title': title, 'artist': artist, - 'source': source}) + + pep = self.contact.pep + if 'tune' in pep: + self._tune_image.set_tooltip_markup(pep['tune'].asMarkupText()) self._tune_image.show() else: self._tune_image.hide() diff --git a/src/common/connection.py b/src/common/connection.py index 717684a6b..90df73d08 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -171,6 +171,7 @@ class Connection(ConnectionHandlers): self.mood = {} self.tune = {} self.activity = {} + self.pep = {} # Do we continue connection when we get roster (send presence,get vcard..) self.continue_connect_info = None # Do we auto accept insecure connection diff --git a/src/common/pep.py b/src/common/pep.py index e32c2c444..bbc388f2a 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -203,6 +203,13 @@ import gtkgui_helpers import gobject +def translate_mood(mood): + if mood in MOODS: + return MOODS[mood] + else: + return mood + + class AbstractPEP(object): type = '' @@ -219,14 +226,49 @@ class AbstractPEP(object): def __init__(self, jid, account, items): self._pep_specific_data, self._retracted = self._extract_info(items) + + self._update_contacts(jid, account) + if jid == common.gajim.get_jid_from_account(account): + self._update_account(account) + self.do(jid, account) def _extract_info(self, items): '''To be implemented by subclasses''' raise NotImplementedError - def was_rectacted(self): - return self._retracted + def _update_contacts(self, jid, account): + dict = {} if self._retracted else self._pep_specific_data + + for contact in common.gajim.contacts.get_contacts(account, jid): + setattr(contact, self.type, dict) + + if self._retracted: + if self.type in contact.pep: + del contact.pep[self.type] + else: + contact.pep[self.type] = self + + def _update_account(self, account): + dict = {} if self._retracted else self._pep_specific_data + + acc = common.gajim.connections[account] + setattr(acc, self.type, dict) + + if self._retracted: + if self.type in acc.pep: + del acc.pep[self.type] + else: + acc.pep[self.type] = self + + def asPixbufIcon(self): + '''To be implemented by subclasses''' + raise NotImplementedError + + def asMarkupText(self): + '''To be implemented by subclasses''' + raise NotImplementedError + class UserMoodPEP(AbstractPEP): '''XEP-0107: User Mood''' @@ -251,45 +293,31 @@ class UserMoodPEP(AbstractPEP): return (mood_dict, retracted) def do(self, jid, name): - mood_dict = {} if self._retracted else self._pep_specific_data - - if jid == common.gajim.get_jid_from_account(name): - acc = common.gajim.connections[name] - acc.mood = mood_dict - - user = common.gajim.get_room_and_nick_from_fjid(jid)[0] - for contact in common.gajim.contacts.get_contacts(name, user): - contact.mood = mood_dict - contact.pep['mood'] = self - + if jid == common.gajim.get_jid_from_account(name): common.gajim.interface.roster.draw_account(name) - common.gajim.interface.roster.draw_mood(user, name) - ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) + common.gajim.interface.roster.draw_mood(jid, name) + ctrl = common.gajim.interface.msg_win_mgr.get_control(jid, name) if ctrl: ctrl.update_mood() def asPixbufIcon(self): - if self._retracted: - return None - else: - received_mood = self._pep_specific_data['mood'] - mood = received_mood if received_mood in MOODS else 'unknown' - pixbuf = gtkgui_helpers.load_mood_icon(mood).get_pixbuf() - return pixbuf + assert not self._retracted + received_mood = self._pep_specific_data['mood'] + mood = received_mood if received_mood in MOODS else 'unknown' + pixbuf = gtkgui_helpers.load_mood_icon(mood).get_pixbuf() + return pixbuf def asMarkupText(self): - if self._retracted: - return None - else: - untranslated_mood = self._pep_specific_data['mood'] - mood = MOODS[untranslated_mood] if untranslated_mood in MOODS else untranslated_mood - markuptext = '%s' % gobject.markup_escape_text(mood) - if 'text' in self._pep_specific_data: - text = self._pep_specific_data['text'] - markuptext += '(%s)' + gobject.markup_escape_text(text) - return markuptext + assert not self._retracted + untranslated_mood = self._pep_specific_data['mood'] + mood = translate_mood(untranslated_mood) + markuptext = '%s' % gobject.markup_escape_text(mood) + if 'text' in self._pep_specific_data: + text = self._pep_specific_data['text'] + markuptext += ' (%s)' % gobject.markup_escape_text(text) + return markuptext class UserTunePEP(AbstractPEP): @@ -310,27 +338,36 @@ class UserTunePEP(AbstractPEP): if child.getName() in TUNE_DATA: tune_dict[name] = data - retracted = items.getTag('retract') or not tune_dict + retracted = items.getTag('retract') or not ('artist' in tune_dict or + 'title' in tune_dict) return (tune_dict, retracted) - def do(self, jid, name): - tune_dict = {} if self._retracted else self._pep_specific_data - - if jid == common.gajim.get_jid_from_account(name): - acc = common.gajim.connections[name] - acc.tune = tune_dict - - user = common.gajim.get_room_and_nick_from_fjid(jid)[0] - for contact in common.gajim.contacts.get_contacts(name, user): - contact.tune = tune_dict - + def do(self, jid, name): if jid == common.gajim.get_jid_from_account(name): common.gajim.interface.roster.draw_account(name) - common.gajim.interface.roster.draw_tune(user, name) - ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) + common.gajim.interface.roster.draw_tune(jid, name) + ctrl = common.gajim.interface.msg_win_mgr.get_control(jid, name) if ctrl: ctrl.update_tune() + + def asMarkupText(self): + assert not self._retracted + tune = self._pep_specific_data + + artist = tune.get('artist', _('Unknown Artist')) + artist = gobject.markup_escape_text(artist) + + title = tune.get('title', _('Unknown Title')) + title = gobject.markup_escape_text(title) + + source = tune.get('source', _('Unknown Source')) + source = gobject.markup_escape_text(source) + + tune_string = _('"%(title)s" by %(artist)s\n' + 'from %(source)s') % {'title': title, + 'artist': artist, 'source': source} + return tune_string class UserActivityPEP(AbstractPEP): @@ -361,20 +398,11 @@ class UserActivityPEP(AbstractPEP): return (activity_dict, retracted) def do(self, jid, name): - activity_dict = {} if self._retracted else self._pep_specific_data - - if jid == common.gajim.get_jid_from_account(name): - acc = common.gajim.connections[name] - acc.activity = activity_dict - - user = common.gajim.get_room_and_nick_from_fjid(jid)[0] - for contact in common.gajim.contacts.get_contacts(name, user): - contact.activity = activity_dict if jid == common.gajim.get_jid_from_account(name): common.gajim.interface.roster.draw_account(name) - common.gajim.interface.roster.draw_activity(user, name) - ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) + common.gajim.interface.roster.draw_activity(jid, name) + ctrl = common.gajim.interface.msg_win_mgr.get_control(jid, name) if ctrl: ctrl.update_activity() @@ -437,7 +465,7 @@ class ConnectionPEP: for pep_class in SUPPORTED_PERSONAL_USER_EVENTS: pep = pep_class.get_tag_as_PEP(jid, self.name, event_tag) if pep: - self.dispatch('PEP_RECEIVED', (pep.type, pep)) + self.dispatch('PEP_RECEIVED', (jid, pep.type)) items = event_tag.getTag('items') if items is None: return diff --git a/src/roster_window.py b/src/roster_window.py index 4ea9d884a..93bdef75a 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1281,15 +1281,10 @@ class RosterWindow: iters = self._get_contact_iter(jid, account, model=self.model) if not iters or not gajim.config.get('show_mood_in_roster'): return - jid = self.model[iters[0]][C_JID] - jid = jid.decode('utf-8') + jid = self.model[iters[0]][C_JID].decode('utf-8') contact = gajim.contacts.get_contact(account, jid) - if 'mood' in contact.mood and contact.mood['mood'].strip() in MOODS: - pixbuf = gtkgui_helpers.load_mood_icon( - contact.mood['mood'].strip()).get_pixbuf() - elif 'mood' in contact.mood: - pixbuf = gtkgui_helpers.load_mood_icon( - 'unknown').get_pixbuf() + if 'mood' in contact.pep: + pixbuf = contact.pep['mood'].asPixbufIcon() else: pixbuf = None for child_iter in iters: diff --git a/src/tooltips.py b/src/tooltips.py index fa96cbd8c..47f44b1ff 100644 --- a/src/tooltips.py +++ b/src/tooltips.py @@ -579,17 +579,9 @@ class RosterTooltip(NotificationAreaTooltip): Append Tune, Mood, Activity information of the specified contact to the given property list. ''' - if 'mood' in contact.mood: - mood = contact.mood['mood'].strip() - mood = MOODS.get(mood, mood) - mood = gobject.markup_escape_text(mood) + if 'mood' in contact.pep: + mood = contact.pep['mood'].asMarkupText() mood_string = _('Mood:') + ' %s' % mood - if 'text' in contact.mood \ - and contact.mood['text'] != '': - mood_text = contact.mood['text'].strip() - mood_text = \ - gobject.markup_escape_text(mood_text) - mood_string += ' (%s)' % mood_text properties.append((mood_string, None)) if 'activity' in contact.activity: @@ -617,27 +609,9 @@ class RosterTooltip(NotificationAreaTooltip): activity_string += ' (%s)' % activity_text properties.append((activity_string, None)) - if 'artist' in contact.tune \ - or 'title' in contact.tune: - if 'artist' in contact.tune: - artist = contact.tune['artist'].strip() - artist = gobject.markup_escape_text(artist) - else: - artist = _('Unknown Artist') - if 'title' in contact.tune: - title = contact.tune['title'].strip() - title = gobject.markup_escape_text(title) - else: - title = _('Unknown Title') - if 'source' in contact.tune: - source = contact.tune['source'].strip() - source = gobject.markup_escape_text(source) - else: - source = _('Unknown Source') - tune_string = _('Tune:') + ' ' + \ - _('"%(title)s" by %(artist)s\n' - 'from %(source)s') % {'title': title, - 'artist': artist, 'source': source} + if 'tune' in contact.pep: + tune = contact.pep['tune'].asMarkupText() + tune_string = _('Tune:') + ' %s' % tune properties.append((tune_string, None)) From 6c0fb26e58ecd746a799a4998e7fa4b6d8c45f83 Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sun, 15 Nov 2009 11:11:51 +0100 Subject: [PATCH 13/73] Use central event_handler in Interface() instead of updating the GUI directly from XMPP callbacks. --- src/common/pep.py | 70 ++++++++++---------------------------------- src/gui_interface.py | 30 +++++++++++++++++++ 2 files changed, 46 insertions(+), 54 deletions(-) diff --git a/src/common/pep.py b/src/common/pep.py index bbc388f2a..ad4d3f815 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -230,8 +230,6 @@ class AbstractPEP(object): self._update_contacts(jid, account) if jid == common.gajim.get_jid_from_account(account): self._update_account(account) - - self.do(jid, account) def _extract_info(self, items): '''To be implemented by subclasses''' @@ -291,16 +289,6 @@ class UserMoodPEP(AbstractPEP): retracted = items.getTag('retract') or not 'mood' in mood_dict return (mood_dict, retracted) - - def do(self, jid, name): - - if jid == common.gajim.get_jid_from_account(name): - common.gajim.interface.roster.draw_account(name) - common.gajim.interface.roster.draw_mood(jid, name) - ctrl = common.gajim.interface.msg_win_mgr.get_control(jid, name) - if ctrl: - ctrl.update_mood() - def asPixbufIcon(self): assert not self._retracted @@ -341,16 +329,7 @@ class UserTunePEP(AbstractPEP): retracted = items.getTag('retract') or not ('artist' in tune_dict or 'title' in tune_dict) return (tune_dict, retracted) - - - def do(self, jid, name): - if jid == common.gajim.get_jid_from_account(name): - common.gajim.interface.roster.draw_account(name) - common.gajim.interface.roster.draw_tune(jid, name) - ctrl = common.gajim.interface.msg_win_mgr.get_control(jid, name) - if ctrl: - ctrl.update_tune() - + def asMarkupText(self): assert not self._retracted tune = self._pep_specific_data @@ -397,20 +376,11 @@ class UserActivityPEP(AbstractPEP): retracted = items.getTag('retract') or not activity_dict return (activity_dict, retracted) - def do(self, jid, name): - - if jid == common.gajim.get_jid_from_account(name): - common.gajim.interface.roster.draw_account(name) - common.gajim.interface.roster.draw_activity(jid, name) - ctrl = common.gajim.interface.msg_win_mgr.get_control(jid, name) - if ctrl: - ctrl.update_activity() - - + class UserNicknamePEP(AbstractPEP): '''XEP-0172: User Nickname''' - type = 'activity' + type = 'nickname' namespace = common.xmpp.NS_NICK def _extract_info(self, items): @@ -423,29 +393,21 @@ class UserNicknamePEP(AbstractPEP): retracted = items.getTag('retract') or not nick return (nick, retracted) - - def do(self, jid, name): - if jid == common.gajim.get_jid_from_account(name): - if self._retracted: - common.gajim.nicks[name] = common.gajim.config.get_per('accounts', - name, 'name') - else: - common.gajim.nicks[name] = self._pep_specific_data - + + def _update_contacts(self, jid, account): + # TODO: use dict instead nick = '' if self._retracted else self._pep_specific_data - - user = common.gajim.get_room_and_nick_from_fjid(jid)[0] - for contact in common.gajim.contacts.get_contacts(name, user): + for contact in common.gajim.contacts.get_contacts(account, jid): contact.contact_name = nick - common.gajim.interface.roster.draw_contact(user, name) - - ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) - if ctrl: - ctrl.update_ui() - win = ctrl.parent_win - win.redraw_tab(ctrl) - win.show_title() - + + def _update_account(self, account): + # TODO: use dict instead + if self._retracted: + common.gajim.nicks[account] = common.gajim.config.get_per('accounts', + account, 'name') + else: + common.gajim.nicks[account] = self._pep_specific_data + SUPPORTED_PERSONAL_USER_EVENTS = [UserMoodPEP, UserTunePEP, UserActivityPEP, UserNicknamePEP] diff --git a/src/gui_interface.py b/src/gui_interface.py index 812aba08a..8aec6f993 100644 --- a/src/gui_interface.py +++ b/src/gui_interface.py @@ -1989,6 +1989,35 @@ class Interface: _('PEP node %(node)s was not removed: %(message)s') % { 'node': data[1], 'message': data[2]}) + def handle_event_pep_received(self, account, data): + # ('PEP_RECEIVED', account, (jid, pep_type)) + jid = data[0] + pep_type = data[1] + ctrl = common.gajim.interface.msg_win_mgr.get_control(jid, account) + + if jid == common.gajim.get_jid_from_account(account): + self.roster.draw_account(account) + + if pep_type == 'mood': + self.roster.draw_mood(jid, account) + if ctrl: + ctrl.update_mood() + elif pep_type == 'tune': + self.roster.draw_tune(jid, account) + if ctrl: + ctrl.update_tune() + elif pep_type == 'activity': + self.roster.draw_activity(jid, account) + if ctrl: + ctrl.update_activity() + elif pep_type == 'nickname': + self.roster.draw_contact(jid, account) + if ctrl: + ctrl.update_ui() + win = ctrl.parent_win + win.redraw_tab(ctrl) + win.show_title() + def register_handler(self, event, handler): if event not in self.handlers: self.handlers[event] = [] @@ -2088,6 +2117,7 @@ class Interface: 'JINGLE_CONNECTED': [self.handle_event_jingle_connected], 'JINGLE_DISCONNECTED': [self.handle_event_jingle_disconnected], 'JINGLE_ERROR': [self.handle_event_jingle_error], + 'PEP_RECEIVED': [self.handle_event_pep_received] } def dispatch(self, event, account, data): From 30191888bab4132556fd4a368271a3fa7026a4f1 Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sun, 15 Nov 2009 16:52:19 +0100 Subject: [PATCH 14/73] Create asPixbufIcon and asMarkupText functions on the UserActivity class. --- src/chat_control.py | 43 ++++------------------------------ src/common/pep.py | 55 ++++++++++++++++++++++++++++++++++++++++---- src/roster_window.py | 21 ++++------------- src/tooltips.py | 28 ++++------------------ 4 files changed, 62 insertions(+), 85 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index ab6f10213..b71033da5 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1443,46 +1443,12 @@ class ChatControl(ChatControlBase): self._mood_image.hide() def update_activity(self): - activity = None - subactivity = None - text = None - if isinstance(self.contact, GC_Contact): return - - if 'activity' in self.contact.activity: - activity = self.contact.activity['activity'].strip() - if 'subactivity' in self.contact.activity: - subactivity = self.contact.activity['subactivity'].strip() - if 'text' in self.contact.activity: - text = self.contact.activity['text'].strip() - - if activity is not None: - if activity in ACTIVITIES: - # Translate standard activities - if subactivity in ACTIVITIES[activity]: - self._activity_image.set_from_pixbuf( - gtkgui_helpers.load_activity_icon(activity, subactivity). \ - get_pixbuf()) - subactivity = ACTIVITIES[activity][subactivity] - else: - self._activity_image.set_from_pixbuf( - gtkgui_helpers.load_activity_icon(activity).get_pixbuf()) - activity = ACTIVITIES[activity]['category'] - else: - self._activity_image.set_from_pixbuf( - gtkgui_helpers.load_activity_icon('unknown').get_pixbuf()) - - # Translate standard subactivities - - tooltip = '' + gobject.markup_escape_text(activity) - if subactivity: - tooltip += ': ' + gobject.markup_escape_text(subactivity) - tooltip += '' - if text: - tooltip += '\n' + gobject.markup_escape_text(text) - self._activity_image.set_tooltip_markup(tooltip) - + pep = self.contact.pep + if 'activity' in pep: + self._activity_image.set_from_pixbuf(pep['activity'].asPixbufIcon()) + self._activity_image.set_tooltip_markup(pep['activity'].asMarkupText()) self._activity_image.show() else: self._activity_image.hide() @@ -1490,7 +1456,6 @@ class ChatControl(ChatControlBase): def update_tune(self): if isinstance(self.contact, GC_Contact): return - pep = self.contact.pep if 'tune' in pep: self._tune_image.set_tooltip_markup(pep['tune'].asMarkupText()) diff --git a/src/common/pep.py b/src/common/pep.py index ad4d3f815..13a774a1b 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -201,6 +201,8 @@ import helpers import atom import gtkgui_helpers import gobject +import gajim +import gtk def translate_mood(mood): @@ -329,6 +331,11 @@ class UserTunePEP(AbstractPEP): retracted = items.getTag('retract') or not ('artist' in tune_dict or 'title' in tune_dict) return (tune_dict, retracted) + + def asPixbufIcon(self): + import os + path = os.path.join(gajim.DATA_DIR, 'emoticons', 'static', 'music.png') + return gtk.gdk.pixbuf_new_from_file(path) def asMarkupText(self): assert not self._retracted @@ -366,16 +373,54 @@ class UserActivityPEP(AbstractPEP): data = child.getData().strip() if name == 'text': activity_dict['text'] = data - elif name in ACTIVITIES: + else: activity_dict['activity'] = name for subactivity in child.getChildren(): subactivity_name = subactivity.getName().strip() - if subactivity_name in ACTIVITIES[name]: - activity_dict['subactivity'] = subactivity_name + activity_dict['subactivity'] = subactivity_name - retracted = items.getTag('retract') or not activity_dict + retracted = items.getTag('retract') or not 'activity' in activity_dict return (activity_dict, retracted) + + def asPixbufIcon(self): + assert not self._retracted + pep = self._pep_specific_data + activity = pep['activity'] + has_known_activity = activity in ACTIVITIES + has_known_subactivity = (has_known_activity and ('subactivity' in pep) + and (pep['subactivity'] in ACTIVITIES[activity])) + + if has_known_activity: + if has_known_subactivity: + subactivity = pep['subactivity'] + return gtkgui_helpers.load_activity_icon(activity, subactivity).get_pixbuf() + else: + return gtkgui_helpers.load_activity_icon(activity).get_pixbuf() + else: + return gtkgui_helpers.load_activity_icon('unknown').get_pixbuf() + + def asMarkupText(self): + assert not self._retracted + pep = self._pep_specific_data + activity = pep['activity'] + subactivity = pep['subactivity'] if 'subactivity' in pep else None + text = pep['text'] if 'text' in pep else None + + if activity in ACTIVITIES: + # Translate standard activities + if subactivity in ACTIVITIES[activity]: + subactivity = ACTIVITIES[activity][subactivity] + activity = ACTIVITIES[activity]['category'] + + markuptext = '' + gobject.markup_escape_text(activity) + if subactivity: + markuptext += ': ' + gobject.markup_escape_text(subactivity) + markuptext += '' + if text: + markuptext += ' (%s)' + gobject.markup_escape_text(text) + return markuptext + class UserNicknamePEP(AbstractPEP): '''XEP-0172: User Nickname''' @@ -404,7 +449,7 @@ class UserNicknamePEP(AbstractPEP): # TODO: use dict instead if self._retracted: common.gajim.nicks[account] = common.gajim.config.get_per('accounts', - account, 'name') + account, 'name') else: common.gajim.nicks[account] = self._pep_specific_data diff --git a/src/roster_window.py b/src/roster_window.py index 93bdef75a..4206a2844 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1299,20 +1299,8 @@ class RosterWindow: jid = self.model[iters[0]][C_JID] jid = jid.decode('utf-8') contact = gajim.contacts.get_contact(account, jid) - if 'activity' in contact.activity \ - and contact.activity['activity'].strip() in ACTIVITIES: - if 'subactivity' in contact.activity \ - and contact.activity['subactivity'].strip() in \ - ACTIVITIES[contact.activity['activity'].strip()]: - pixbuf = gtkgui_helpers.load_activity_icon( - contact.activity['activity'].strip(), - contact.activity['subactivity'].strip()).get_pixbuf() - else: - pixbuf = gtkgui_helpers.load_activity_icon( - contact.activity['activity'].strip()).get_pixbuf() - elif 'activity' in contact.activity: - pixbuf = gtkgui_helpers.load_activity_icon( - 'unknown').get_pixbuf() + if 'activity' in contact.pep: + pixbuf = contact.pep['activity'].asPixbufIcon() else: pixbuf = None for child_iter in iters: @@ -1327,9 +1315,8 @@ class RosterWindow: jid = self.model[iters[0]][C_JID] jid = jid.decode('utf-8') contact = gajim.contacts.get_contact(account, jid) - if 'artist' in contact.tune or 'title' in contact.tune: - path = os.path.join(gajim.DATA_DIR, 'emoticons', 'static', 'music.png') - pixbuf = gtk.gdk.pixbuf_new_from_file(path) + if 'tune' in contact.pep: + pixbuf = contact.pep['tune'].asPixbufIcon() else: pixbuf = None for child_iter in iters: diff --git a/src/tooltips.py b/src/tooltips.py index 47f44b1ff..847e6a17a 100644 --- a/src/tooltips.py +++ b/src/tooltips.py @@ -581,32 +581,12 @@ class RosterTooltip(NotificationAreaTooltip): ''' if 'mood' in contact.pep: mood = contact.pep['mood'].asMarkupText() - mood_string = _('Mood:') + ' %s' % mood + mood_string = _('Mood:') + ' %s' % mood properties.append((mood_string, None)) - if 'activity' in contact.activity: - activity = act_plain = \ - contact.activity['activity'].strip() - activity = gobject.markup_escape_text(activity) - if act_plain in ACTIVITIES: - activity = ACTIVITIES[activity]['category'] - activity_string = _('Activity:') + ' %s' % activity - if 'subactivity' in contact.activity: - activity_sub = \ - contact.activity['subactivity'].strip() - if act_plain in ACTIVITIES and activity_sub in \ - ACTIVITIES[act_plain]: - activity_sub = ACTIVITIES[act_plain][activity_sub] - activity_sub = \ - gobject.markup_escape_text(activity_sub) - activity_string += ': %s' % activity_sub - else: - activity_string += '' - if 'text' in contact.activity: - activity_text = contact.activity['text'].strip() - activity_text = gobject.markup_escape_text( - activity_text) - activity_string += ' (%s)' % activity_text + if 'activity' in contact.pep: + activity = contact.pep['activity'].asMarkupText() + activity_string = _('Activity:') + ' %s' % activity properties.append((activity_string, None)) if 'tune' in contact.pep: From b7c7beafd96de0665581a7f4814c63468b24b482 Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sun, 15 Nov 2009 17:00:39 +0100 Subject: [PATCH 15/73] Unify the PEP drawing methods in the RosterWindow. --- src/roster_window.py | 46 ++++++++++++-------------------------------- 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/src/roster_window.py b/src/roster_window.py index 4206a2844..01be6b2cc 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1276,53 +1276,31 @@ class RosterWindow: return False - def draw_mood(self, jid, account): - iters = self._get_contact_iter(jid, account, model=self.model) - if not iters or not gajim.config.get('show_mood_in_roster'): - return - jid = self.model[iters[0]][C_JID].decode('utf-8') - contact = gajim.contacts.get_contact(account, jid) - if 'mood' in contact.pep: - pixbuf = contact.pep['mood'].asPixbufIcon() - else: - pixbuf = None - for child_iter in iters: - self.model[child_iter][C_MOOD_PIXBUF] = pixbuf - return False - + if gajim.config.get('show_mood_in_roster'): + self._draw_pep(jid, account, 'tune', C_MOOD_PIXBUF) def draw_activity(self, jid, account): - iters = self._get_contact_iter(jid, account, model=self.model) - if not iters or not gajim.config.get('show_activity_in_roster'): - return - jid = self.model[iters[0]][C_JID] - jid = jid.decode('utf-8') - contact = gajim.contacts.get_contact(account, jid) - if 'activity' in contact.pep: - pixbuf = contact.pep['activity'].asPixbufIcon() - else: - pixbuf = None - for child_iter in iters: - self.model[child_iter][C_ACTIVITY_PIXBUF] = pixbuf - return False - + if gajim.config.get('show_activity_in_roster'): + self._draw_pep(jid, account, 'tune', C_ACTIVITY_PIXBUF) def draw_tune(self, jid, account): + if gajim.config.get('show_tunes_in_roster'): + self._draw_pep(jid, account, 'tune', C_TUNE_PIXBUF) + + def _draw_pep(self, jid, account, pep_type, model_column): iters = self._get_contact_iter(jid, account, model=self.model) - if not iters or not gajim.config.get('show_tunes_in_roster'): + if not iters: return jid = self.model[iters[0]][C_JID] jid = jid.decode('utf-8') contact = gajim.contacts.get_contact(account, jid) - if 'tune' in contact.pep: - pixbuf = contact.pep['tune'].asPixbufIcon() + if pep_type in contact.pep: + pixbuf = contact.pep[pep_type].asPixbufIcon() else: pixbuf = None for child_iter in iters: - self.model[child_iter][C_TUNE_PIXBUF] = pixbuf - return False - + self.model[child_iter][model_column] = pixbuf def draw_avatar(self, jid, account): iters = self._get_contact_iter(jid, account, model=self.model) From 3b15d70782290a67bb32ef79bea1d2abb3ad48a5 Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sun, 15 Nov 2009 17:11:06 +0100 Subject: [PATCH 16/73] Unify PEP cell_data_functions. --- src/roster_window.py | 150 +++++-------------------------------------- 1 file changed, 16 insertions(+), 134 deletions(-) diff --git a/src/roster_window.py b/src/roster_window.py index 01be6b2cc..6d06388f8 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1278,11 +1278,11 @@ class RosterWindow: def draw_mood(self, jid, account): if gajim.config.get('show_mood_in_roster'): - self._draw_pep(jid, account, 'tune', C_MOOD_PIXBUF) + self._draw_pep(jid, account, 'mood', C_MOOD_PIXBUF) def draw_activity(self, jid, account): if gajim.config.get('show_activity_in_roster'): - self._draw_pep(jid, account, 'tune', C_ACTIVITY_PIXBUF) + self._draw_pep(jid, account, 'activity', C_ACTIVITY_PIXBUF) def draw_tune(self, jid, account): if gajim.config.get('show_tunes_in_roster'): @@ -4441,9 +4441,9 @@ class RosterWindow: renderer.set_property('xpad', 8) - def _fill_mood_pixbuf_renderer(self, column, renderer, model, titer, - data = None): - '''When a row is added, set properties for avatar renderer''' + def _fill_pep_pixbuf_renderer(self, column, renderer, model, titer, + data=None): + '''When a row is added, draw the respective pep icon''' theme = gajim.config.get('roster_theme') type_ = model[titer][C_TYPE] if type_ == 'group': @@ -4451,155 +4451,37 @@ class RosterWindow: return # allocate space for the icon only if needed - if model[titer][C_MOOD_PIXBUF]: + if model[titer][data]: renderer.set_property('visible', True) else: renderer.set_property('visible', False) if type_ == 'account': - color = gajim.config.get_per('themes', theme, - 'accountbgcolor') + color = gajim.config.get_per('themes', theme, 'accountbgcolor') if color: renderer.set_property('cell-background', color) else: - self.set_renderer_color(renderer, - gtk.STATE_ACTIVE) + self.set_renderer_color(renderer, gtk.STATE_ACTIVE) # align pixbuf to the right) renderer.set_property('xalign', 1) # prevent type_ = None, see http://trac.gajim.org/ticket/2534 elif type_: - if not model[titer][C_JID] \ - or not model[titer][C_ACCOUNT]: + if not model[titer][C_JID] or not model[titer][C_ACCOUNT]: # This can append at the moment we add the row return jid = model[titer][C_JID].decode('utf-8') account = model[titer][C_ACCOUNT].decode('utf-8') if jid in gajim.newly_added[account]: - renderer.set_property('cell-background', - gajim.config.get( + renderer.set_property('cell-background', gajim.config.get( 'just_connected_bg_color')) elif jid in gajim.to_be_removed[account]: - renderer.set_property('cell-background', - gajim.config.get( + renderer.set_property('cell-background', gajim.config.get( 'just_disconnected_bg_color')) else: - color = gajim.config.get_per('themes', - theme, 'contactbgcolor') - if color: - renderer.set_property( - 'cell-background', color) - else: - renderer.set_property( - 'cell-background', None) + color = gajim.config.get_per('themes', theme, 'contactbgcolor') + renderer.set_property('cell-background', color if color else None) # align pixbuf to the right renderer.set_property('xalign', 1) - - def _fill_activity_pixbuf_renderer(self, column, renderer, model, titer, - data = None): - '''When a row is added, set properties for avatar renderer''' - theme = gajim.config.get('roster_theme') - type_ = model[titer][C_TYPE] - if type_ == 'group': - renderer.set_property('visible', False) - return - - # allocate space for the icon only if needed - if model[titer][C_ACTIVITY_PIXBUF]: - renderer.set_property('visible', True) - else: - renderer.set_property('visible', False) - if type_ == 'account': - color = gajim.config.get_per('themes', theme, - 'accountbgcolor') - if color: - renderer.set_property('cell-background', color) - else: - self.set_renderer_color(renderer, - gtk.STATE_ACTIVE) - # align pixbuf to the right) - renderer.set_property('xalign', 1) - # prevent type_ = None, see http://trac.gajim.org/ticket/2534 - elif type_: - if not model[titer][C_JID] \ - or not model[titer][C_ACCOUNT]: - # This can append at the moment we add the row - return - jid = model[titer][C_JID].decode('utf-8') - account = model[titer][C_ACCOUNT].decode('utf-8') - if jid in gajim.newly_added[account]: - renderer.set_property('cell-background', - gajim.config.get( - 'just_connected_bg_color')) - elif jid in gajim.to_be_removed[account]: - renderer.set_property('cell-background', - gajim.config.get( - 'just_disconnected_bg_color')) - else: - color = gajim.config.get_per('themes', - theme, 'contactbgcolor') - if color: - renderer.set_property( - 'cell-background', color) - else: - renderer.set_property( - 'cell-background', None) - # align pixbuf to the right - renderer.set_property('xalign', 1) - - - def _fill_tune_pixbuf_renderer(self, column, renderer, model, titer, - data = None): - '''When a row is added, set properties for avatar renderer''' - theme = gajim.config.get('roster_theme') - type_ = model[titer][C_TYPE] - if type_ == 'group': - renderer.set_property('visible', False) - return - - # allocate space for the icon only if needed - if model[titer][C_TUNE_PIXBUF]: - renderer.set_property('visible', True) - else: - renderer.set_property('visible', False) - if type_ == 'account': - color = gajim.config.get_per('themes', theme, - 'accountbgcolor') - if color: - renderer.set_property('cell-background', color) - else: - self.set_renderer_color(renderer, - gtk.STATE_ACTIVE) - # align pixbuf to the right) - renderer.set_property('xalign', 1) - # prevent type_ = None, see http://trac.gajim.org/ticket/2534 - elif type_: - if not model[titer][C_JID] \ - or not model[titer][C_ACCOUNT]: - # This can append at the moment we add the row - return - jid = model[titer][C_JID].decode('utf-8') - account = model[titer][C_ACCOUNT].decode('utf-8') - if jid in gajim.newly_added[account]: - renderer.set_property('cell-background', - gajim.config.get( - 'just_connected_bg_color')) - elif jid in gajim.to_be_removed[account]: - renderer.set_property('cell-background', - gajim.config.get( - 'just_disconnected_bg_color')) - else: - color = gajim.config.get_per('themes', - theme, 'contactbgcolor') - if color: - renderer.set_property( - 'cell-background', color) - else: - renderer.set_property( - 'cell-background', None) - # align pixbuf to the right - renderer.set_property('xalign', 1) - - def _fill_avatar_pixbuf_renderer(self, column, renderer, model, titer, data = None): '''When a row is added, set properties for avatar renderer''' @@ -5893,19 +5775,19 @@ class RosterWindow: col.pack_start(render_pixbuf, expand=False) col.add_attribute(render_pixbuf, 'pixbuf', C_MOOD_PIXBUF) col.set_cell_data_func(render_pixbuf, - self._fill_mood_pixbuf_renderer, None) + self._fill_pep_pixbuf_renderer, C_MOOD_PIXBUF) render_pixbuf = gtk.CellRendererPixbuf() col.pack_start(render_pixbuf, expand=False) col.add_attribute(render_pixbuf, 'pixbuf', C_ACTIVITY_PIXBUF) col.set_cell_data_func(render_pixbuf, - self._fill_activity_pixbuf_renderer, None) + self._fill_pep_pixbuf_renderer, C_ACTIVITY_PIXBUF) render_pixbuf = gtk.CellRendererPixbuf() col.pack_start(render_pixbuf, expand=False) col.add_attribute(render_pixbuf, 'pixbuf', C_TUNE_PIXBUF) col.set_cell_data_func(render_pixbuf, - self._fill_tune_pixbuf_renderer, None) + self._fill_pep_pixbuf_renderer, C_TUNE_PIXBUF) if gajim.config.get('avatar_position_in_roster') == 'right': add_avatar_renderer() From b2c5810869f6a62de2dd69c893d7ad0385e79998 Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Sun, 15 Nov 2009 20:47:06 +0100 Subject: [PATCH 17/73] Refactorize a bit jingle.py and split it into different files. There is still room for improvement, but it should be better. --- src/common/jingle.py | 1074 +------------------------------- src/common/jingle_content.py | 102 +++ src/common/jingle_rtp.py | 314 ++++++++++ src/common/jingle_session.py | 613 ++++++++++++++++++ src/common/jingle_transport.py | 139 +++++ 5 files changed, 1175 insertions(+), 1067 deletions(-) create mode 100644 src/common/jingle_content.py create mode 100644 src/common/jingle_rtp.py create mode 100644 src/common/jingle_session.py create mode 100644 src/common/jingle_transport.py diff --git a/src/common/jingle.py b/src/common/jingle.py index 6549ee10c..8a369ff7e 100644 --- a/src/common/jingle.py +++ b/src/common/jingle.py @@ -13,17 +13,6 @@ ''' Handles the jingle signalling protocol. ''' #TODO: -# * things in XEP 0166, including: -# - 'senders' attribute of 'content' element -# - security preconditions -# * actions: -# - content-modify -# - description-info, session-info -# - security-info -# - transport-accept, transport-reject -# * sid/content related: -# - tiebreaking -# - if there already is a session, use it # * things in XEP 0176, including: # - http://xmpp.org/extensions/xep-0176.html#protocol-restarts # - http://xmpp.org/extensions/xep-0176.html#fallback @@ -36,1064 +25,15 @@ # - codecs # - STUN -# * DONE: figure out why it doesn't work with pidgin: -# That's a bug in pidgin: http://xmpp.org/extensions/xep-0176.html#protocol-checks +# * figure out why it doesn't work with pidgin: +# That's maybe a bug in pidgin: +# http://xmpp.org/extensions/xep-0176.html#protocol-checks -# * timeout - -# * split this file in several modules -# For example, a file dedicated for XEP0166, one for XEP0176, -# and one for XEP0167 - -# * handle different kinds of sink and src elements - -import gobject - -import gajim import xmpp import helpers -import farsight, gst - -def get_first_gst_element(elements): - ''' Returns, if it exists, the first available element of the list. ''' - for name in elements: - factory = gst.element_factory_find(name) - if factory: - return factory.create() - -#FIXME: Move it to JingleSession.States? -class JingleStates(object): - ''' States in which jingle session may exist. ''' - ended = 0 - pending = 1 - active = 2 - -#FIXME: Move it to JingleTransport.Type? -class TransportType(object): - ''' Possible types of a JingleTransport ''' - datagram = 1 - streaming = 2 - -class OutOfOrder(Exception): - ''' Exception that should be raised when an action is received when in the wrong state. ''' - -class TieBreak(Exception): - ''' Exception that should be raised in case of a tie, when we overrule the other action. ''' - -class JingleSession(object): - ''' This represents one jingle session. ''' - def __init__(self, con, weinitiate, jid, sid=None): - ''' con -- connection object, - weinitiate -- boolean, are we the initiator? - jid - jid of the other entity''' - self.contents = {} # negotiated contents - self.connection = con # connection to use - # our full jid - self.ourjid = gajim.get_jid_from_account(self.connection.name) + '/' + \ - con.server_resource - self.peerjid = jid # jid we connect to - # jid we use as the initiator - self.initiator = weinitiate and self.ourjid or self.peerjid - # jid we use as the responder - self.responder = weinitiate and self.peerjid or self.ourjid - # are we an initiator? - self.weinitiate = weinitiate - # what state is session in? (one from JingleStates) - self.state = JingleStates.ended - if not sid: - sid = con.connection.getAnID() - self.sid = sid # sessionid - - self.accepted = True # is this session accepted by user - - # callbacks to call on proper contents - # use .prepend() to add new callbacks, especially when you're going - # to send error instead of ack - self.callbacks = { - 'content-accept': [self.__contentAcceptCB, self.__broadcastCB, - self.__defaultCB], - 'content-add': [self.__contentAddCB, self.__broadcastCB, - self.__defaultCB], #TODO - 'content-modify': [self.__defaultCB], #TODO - 'content-reject': [self.__defaultCB, self.__contentRemoveCB], #TODO - 'content-remove': [self.__defaultCB, self.__contentRemoveCB], - 'description-info': [self.__broadcastCB, self.__defaultCB], #TODO - 'security-info': [self.__defaultCB], #TODO - 'session-accept': [self.__sessionAcceptCB, self.__contentAcceptCB, - self.__broadcastCB, self.__defaultCB], - 'session-info': [self.__sessionInfoCB, self.__broadcastCB, self.__defaultCB], - 'session-initiate': [self.__sessionInitiateCB, self.__broadcastCB, - self.__defaultCB], - 'session-terminate': [self.__sessionTerminateCB, self.__broadcastAllCB, - self.__defaultCB], - 'transport-info': [self.__broadcastCB, self.__defaultCB], - 'transport-replace': [self.__broadcastCB, self.__transportReplaceCB], #TODO - 'transport-accept': [self.__defaultCB], #TODO - 'transport-reject': [self.__defaultCB], #TODO - 'iq-result': [], - 'iq-error': [self.__errorCB], - } - - ''' Interaction with user ''' - def approve_session(self): - ''' Called when user accepts session in UI (when we aren't the initiator). - ''' - self.accept_session() - - def decline_session(self): - ''' Called when user declines session in UI (when we aren't the initiator) - ''' - reason = xmpp.Node('reason') - reason.addChild('decline') - self._session_terminate(reason) - - def approve_content(self, media): - content = self.get_content(media) - if content: - content.accepted = True - self.on_session_state_changed(content) - - def reject_content(self, media): - content = self.get_content(media) - if content: - if self.state == JingleStates.active: - self.__content_reject(content) - content.destroy() - self.on_session_state_changed() - - def end_session(self): - ''' Called when user stops or cancel session in UI. ''' - reason = xmpp.Node('reason') - if self.state == JingleStates.active: - reason.addChild('success') - else: - reason.addChild('cancel') - self._session_terminate(reason) - - ''' Middle-level functions to manage contents. Handle local content - cache and send change notifications. ''' - def get_content(self, media=None): - if media == 'audio': - cls = JingleVoIP - elif media == 'video': - cls = JingleVideo - #elif media == None: - # cls = JingleContent - else: - return None - - for content in self.contents.values(): - if isinstance(content, cls): - return content - - def add_content(self, name, content, creator='we'): - ''' Add new content to session. If the session is active, - this will send proper stanza to update session. - Creator must be one of ('we', 'peer', 'initiator', 'responder')''' - assert creator in ('we', 'peer', 'initiator', 'responder') - - if (creator == 'we' and self.weinitiate) or (creator == 'peer' and \ - not self.weinitiate): - creator = 'initiator' - elif (creator == 'peer' and self.weinitiate) or (creator == 'we' and \ - not self.weinitiate): - creator = 'responder' - content.creator = creator - content.name = name - self.contents[(creator, name)] = content - - if (creator == 'initiator') == self.weinitiate: - # The content is from us, accept it - content.accepted = True - - def remove_content(self, creator, name): - ''' We do not need this now ''' - #TODO: - if (creator, name) in self.contents: - content = self.contents[(creator, name)] - if len(self.contents) > 1: - self.__content_remove(content) - self.contents[(creator, name)].destroy() - if len(self.contents) == 0: - self.end_session() - - def modify_content(self, creator, name, *someother): - ''' We do not need this now ''' - pass - - def on_session_state_changed(self, content=None): - if self.state == JingleStates.ended: - # Session not yet started, only one action possible: session-initiate - if self.is_ready() and self.weinitiate: - self.__session_initiate() - elif self.state == JingleStates.pending: - # We can either send a session-accept or a content-add - if self.is_ready() and not self.weinitiate: - self.__session_accept() - elif content and (content.creator == 'initiator') == self.weinitiate: - self.__content_add(content) - elif content and self.weinitiate: - self.__content_accept(content) - elif self.state == JingleStates.active: - # We can either send a content-add or a content-accept - if not content: - return - if (content.creator == 'initiator') == self.weinitiate: - # We initiated this content. It's a pending content-add. - self.__content_add(content) - else: - # The other side created this content, we accept it. - self.__content_accept(content) - - def is_ready(self): - ''' Returns True when all codecs and candidates are ready - (for all contents). ''' - return (all((content.is_ready() for content in self.contents.itervalues())) - and self.accepted) - - ''' Middle-level function to do stanza exchange. ''' - def accept_session(self): - ''' Mark the session as accepted. ''' - self.accepted = True - self.on_session_state_changed() - - def start_session(self): - ''' Mark the session as ready to be started. ''' - self.accepted = True - self.on_session_state_changed() - - def send_session_info(self): - pass - - def send_content_accept(self, content): - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('content-accept') - jingle.addChild(node=content) - self.connection.connection.send(stanza) - - def send_transport_info(self, content): - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('transport-info') - jingle.addChild(node=content) - self.connection.connection.send(stanza) - - ''' Session callbacks. ''' - def stanzaCB(self, stanza): - ''' A callback for ConnectionJingle. It gets stanza, then - tries to send it to all internally registered callbacks. - First one to raise xmpp.NodeProcessed breaks function.''' - jingle = stanza.getTag('jingle') - error = stanza.getTag('error') - if error: - # it's an iq-error stanza - action = 'iq-error' - elif jingle: - # it's a jingle action - action = jingle.getAttr('action') - if action not in self.callbacks: - self.__send_error(stanza, 'bad_request') - return - #FIXME: If we aren't initiated and it's not a session-initiate... - if action != 'session-initiate' and self.state == JingleStates.ended: - self.__send_error(stanza, 'item-not-found', 'unknown-session') - return - else: - # it's an iq-result (ack) stanza - action = 'iq-result' - - callables = self.callbacks[action] - - try: - for callable in callables: - callable(stanza=stanza, jingle=jingle, error=error, action=action) - except xmpp.NodeProcessed: - pass - except TieBreak: - self.__send_error(stanza, 'conflict', 'tiebreak') - except OutOfOrder: - self.__send_error(stanza, 'unexpected-request', 'out-of-order')#FIXME - - def __defaultCB(self, stanza, jingle, error, action): - ''' Default callback for action stanzas -- simple ack - and stop processing. ''' - response = stanza.buildReply('result') - self.connection.connection.send(response) - - def __errorCB(self, stanza, jingle, error, action): - #FIXME - text = error.getTagData('text') - jingle_error = None - xmpp_error = None - for child in error.getChildren(): - if child.getNamespace() == xmpp.NS_JINGLE_ERRORS: - jingle_error = child.getName() - elif child.getNamespace() == xmpp.NS_STANZAS: - xmpp_error = child.getName() - self.__dispatch_error(xmpp_error, jingle_error, text) - #FIXME: Not sure when we would want to do that... - if xmpp_error == 'item-not-found': - self.connection.delete_jingle_session(self.peerjid, self.sid) - - def __transportReplaceCB(self, stanza, jingle, error, action): - for content in jingle.iterTags('content'): - creator = content['creator'] - name = content['name'] - if (creator, name) in self.contents: - transport_ns = content.getTag('transport').getNamespace() - if transport_ns == xmpp.JINGLE_ICE_UDP: - #FIXME: We don't manage anything else than ICE-UDP now... - #What was the previous transport?!? - #Anyway, content's transport is not modifiable yet - pass - else: - stanza, jingle = self.__make_jingle('transport-reject') - content = jingle.setTag('content', attrs={'creator': creator, - 'name': name}) - content.setTag('transport', namespace=transport_ns) - self.connection.connection.send(stanza) - raise xmpp.NodeProcessed - else: - #FIXME: This ressource is unknown to us, what should we do? - #For now, reject the transport - stanza, jingle = self.__make_jingle('transport-reject') - c = jingle.setTag('content', attrs={'creator': creator, - 'name': name}) - c.setTag('transport', namespace=transport_ns) - self.connection.connection.send(stanza) - raise xmpp.NodeProcessed - - def __sessionInfoCB(self, stanza, jingle, error, action): - #TODO: ringing, active, (un)hold, (un)mute - payload = jingle.getPayload() - if len(payload) > 0: - self.__send_error(stanza, 'feature-not-implemented', 'unsupported-info') - raise xmpp.NodeProcessed - - def __contentRemoveCB(self, stanza, jingle, error, action): - for content in jingle.iterTags('content'): - creator = content['creator'] - name = content['name'] - if (creator, name) in self.contents: - content = self.contents[(creator, name)] - #TODO: this will fail if content is not an RTP content - self.connection.dispatch('JINGLE_DISCONNECTED', - (self.peerjid, self.sid, content.media, 'removed')) - content.destroy() - if len(self.contents) == 0: - reason = xmpp.Node('reason') - reason.setTag('success') - self._session_terminate(reason) - - def __sessionAcceptCB(self, stanza, jingle, error, action): - if self.state != JingleStates.pending: #FIXME - raise OutOfOrder - self.state = JingleStates.active - - def __contentAcceptCB(self, stanza, jingle, error, action): - ''' Called when we get content-accept stanza or equivalent one - (like session-accept).''' - # check which contents are accepted - for content in jingle.iterTags('content'): - creator = content['creator'] - name = content['name']#TODO... - - def __contentAddCB(self, stanza, jingle, error, action): - if self.state == JingleStates.ended: - raise OutOfOrder - - parse_result = self.__parse_contents(jingle) - contents = parse_result[2] - rejected_contents = parse_result[3] - - for name, creator in rejected_contents: - #TODO: - content = JingleContent() - self.add_content(name, content, creator) - self.__content_reject(content) - self.contents[(content.creator, content.name)].destroy() - - self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid, - contents)) - - def __sessionInitiateCB(self, stanza, jingle, error, action): - ''' We got a jingle session request from other entity, - therefore we are the receiver... Unpack the data, - inform the user. ''' - - if self.state != JingleStates.ended: - raise OutOfOrder - - self.initiator = jingle['initiator'] - self.responder = self.ourjid - self.peerjid = self.initiator - self.accepted = False # user did not accept this session yet - - # TODO: If the initiator is unknown to the receiver (e.g., via presence - # subscription) and the receiver has a policy of not communicating via - # Jingle with unknown entities, it SHOULD return a - # error. - - # Lets check what kind of jingle session does the peer want - contents_ok, transports_ok, contents, pouet = self.__parse_contents(jingle) - - # If there's no content we understand... - if not contents_ok: - # TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate - reason = xmpp.Node('reason') - reason.setTag('unsupported-applications') - self.__defaultCB(stanza, jingle, error, action) - self._session_terminate(reason) - raise xmpp.NodeProcessed - - if not transports_ok: - # TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate - reason = xmpp.Node('reason') - reason.setTag('unsupported-transports') - self.__defaultCB(stanza, jingle, error, action) - self._session_terminate(reason) - raise xmpp.NodeProcessed - - self.state = JingleStates.pending - - # Send event about starting a session - self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid, - contents)) - - def __broadcastCB(self, stanza, jingle, error, action): - ''' Broadcast the stanza contents to proper content handlers. ''' - for content in jingle.iterTags('content'): - name = content['name'] - creator = content['creator'] - cn = self.contents[(creator, name)] - cn.stanzaCB(stanza, content, error, action) - - def __sessionTerminateCB(self, stanza, jingle, error, action): - self.connection.delete_jingle_session(self.peerjid, self.sid) - reason, text = self.__reason_from_stanza(jingle) - if reason not in ('success', 'cancel', 'decline'): - self.__dispatch_error(reason, reason, text) - if text: - text = '%s (%s)' % (reason, text) - else: - text = reason#TODO - self.connection.dispatch('JINGLE_DISCONNECTED', - (self.peerjid, self.sid, None, text)) - - def __broadcastAllCB(self, stanza, jingle, error, action): - ''' Broadcast the stanza to all content handlers. ''' - for content in self.contents.itervalues(): - content.stanzaCB(stanza, None, error, action) - - ''' Internal methods. ''' - def __parse_contents(self, jingle): - #TODO: Needs some reworking - contents = [] - contents_rejected = [] - contents_ok = False - transports_ok = False - - for element in jingle.iterTags('content'): - desc = element.getTag('description') - desc_ns = desc.getNamespace() - tran_ns = element.getTag('transport').getNamespace() - if desc_ns == xmpp.NS_JINGLE_RTP and desc['media'] in ('audio', 'video'): - contents_ok = True - #TODO: Everything here should be moved somewhere else - if tran_ns == xmpp.NS_JINGLE_ICE_UDP: - if desc['media'] == 'audio': - self.add_content(element['name'], JingleVoIP(self), 'peer') - else: - self.add_content(element['name'], JingleVideo(self), 'peer') - contents.append((desc['media'],)) - transports_ok = True - else: - contents_rejected.append((element['name'], 'peer')) - else: - contents_rejected.append((element['name'], 'peer')) - - return (contents_ok, transports_ok, contents, contents_rejected) - - def __dispatch_error(self, error, jingle_error=None, text=None): - if jingle_error: - error = jingle_error - if text: - text = '%s (%s)' % (error, text) - else: - text = error - self.connection.dispatch('JINGLE_ERROR', (self.peerjid, self.sid, text)) - - def __reason_from_stanza(self, stanza): - reason = 'success' - reasons = ['success', 'busy', 'cancel', 'connectivity-error', - 'decline', 'expired', 'failed-application', 'failed-transport', - 'general-error', 'gone', 'incompatible-parameters', 'media-error', - 'security-error', 'timeout', 'unsupported-applications', - 'unsupported-transports'] - tag = stanza.getTag('reason') - if tag: - text = tag.getTagData('text') - for r in reasons: - if tag.getTag(r): - reason = r - break - return (reason, text) - - ''' Methods that make/send proper pieces of XML. They check if the session - is in appropriate state. ''' - def __make_jingle(self, action): - stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.peerjid)) - attrs = {'action': action, - 'sid': self.sid} - if action == 'session-initiate': - attrs['initiator'] = self.initiator - elif action == 'session-accept': - attrs['responder'] = self.responder - jingle = stanza.addChild('jingle', attrs=attrs, namespace=xmpp.NS_JINGLE) - return stanza, jingle - - def __send_error(self, stanza, error, jingle_error=None, text=None): - err = xmpp.Error(stanza, error) - err.setNamespace(xmpp.NS_STANZAS) - if jingle_error: - err.setTag(jingle_error, namespace=xmpp.NS_JINGLE_ERRORS) - if text: - err.setTagData('text', text) - self.connection.connection.send(err) - self.__dispatch_error(error, jingle_error, text) - - def __append_content(self, jingle, content): - ''' Append element to element, - with (full=True) or without (full=False) - children. ''' - jingle.addChild('content', - attrs={'name': content.name, 'creator': content.creator}) - - def __append_contents(self, jingle): - ''' Append all elements to .''' - # TODO: integrate with __appendContent? - # TODO: parameters 'name', 'content'? - for content in self.contents.values(): - self.__append_content(jingle, content) - - def __session_initiate(self): - assert self.state == JingleStates.ended - stanza, jingle = self.__make_jingle('session-initiate') - self.__append_contents(jingle) - self.__broadcastCB(stanza, jingle, None, 'session-initiate-sent') - self.connection.connection.send(stanza) - self.state = JingleStates.pending - - def __session_accept(self): - assert self.state == JingleStates.pending - stanza, jingle = self.__make_jingle('session-accept') - self.__append_contents(jingle) - self.__broadcastCB(stanza, jingle, None, 'session-accept-sent') - self.connection.connection.send(stanza) - self.state = JingleStates.active - - def __session_info(self, payload=None): - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('session-info') - if payload: - jingle.addChild(node=payload) - self.connection.connection.send(stanza) - - def _session_terminate(self, reason=None): - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('session-terminate') - if reason is not None: - jingle.addChild(node=reason) - self.__broadcastAllCB(stanza, jingle, None, 'session-terminate-sent') - self.connection.connection.send(stanza) - reason, text = self.__reason_from_stanza(jingle) - if reason not in ('success', 'cancel', 'decline'): - self.__dispatch_error(reason, reason, text) - if text: - text = '%s (%s)' % (reason, text) - else: - text = reason - self.connection.delete_jingle_session(self.peerjid, self.sid) - self.connection.dispatch('JINGLE_DISCONNECTED', - (self.peerjid, self.sid, None, text)) - - def __content_add(self, content): - #TODO: test - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('content-add') - self.__append_content(jingle, content) - self.__broadcastCB(stanza, jingle, None, 'content-add-sent') - self.connection.connection.send(stanza) - - def __content_accept(self, content): - #TODO: test - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('content-accept') - self.__append_content(jingle, content) - self.__broadcastCB(stanza, jingle, None, 'content-accept-sent') - self.connection.connection.send(stanza) - - def __content_reject(self, content): - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('content-reject') - self.__append_content(jingle, content) - self.connection.connection.send(stanza) - #TODO: this will fail if content is not an RTP content - self.connection.dispatch('JINGLE_DISCONNECTED', - (self.peerjid, self.sid, content.media, 'rejected')) - - def __content_modify(self): - assert self.state != JingleStates.ended - - def __content_remove(self, content): - assert self.state != JingleStates.ended - stanza, jingle = self.__make_jingle('content-remove') - self.__append_content(jingle, content) - self.connection.connection.send(stanza) - #TODO: this will fail if content is not an RTP content - self.connection.dispatch('JINGLE_DISCONNECTED', - (self.peerjid, self.sid, content.media, 'removed')) - - def content_negociated(self, media): - self.connection.dispatch('JINGLE_CONNECTED', (self.peerjid, self.sid, - media)) - -#TODO: -#class JingleTransport(object): -# ''' An abstraction of a transport in Jingle sessions. ''' -# def __init__(self): -# pass - - -class JingleContent(object): - ''' An abstraction of content in Jingle sessions. ''' - def __init__(self, session, node=None): - self.session = session - # will be filled by JingleSession.add_content() - # don't uncomment these lines, we will catch more buggy code then - # (a JingleContent not added to session shouldn't send anything) - #self.creator = None - #self.name = None - self.accepted = False - self.sent = False - self.candidates = [] # Local transport candidates - self.remote_candidates = [] # Remote transport candidates - - self.senders = 'both' #FIXME - self.allow_sending = True # Used for stream direction, attribute 'senders' - - self.callbacks = { - # these are called when *we* get stanzas - 'content-accept': [self.__transportInfoCB], - 'content-add': [self.__transportInfoCB], - 'content-modify': [], - 'content-reject': [], - 'content-remove': [], - 'description-info': [], - 'security-info': [], - 'session-accept': [self.__transportInfoCB], - 'session-info': [], - 'session-initiate': [self.__transportInfoCB], - 'session-terminate': [], - 'transport-info': [self.__transportInfoCB], - 'transport-replace': [], - 'transport-accept': [], - 'transport-reject': [], - 'iq-result': [], - 'iq-error': [], - # these are called when *we* sent these stanzas - 'content-accept-sent': [self.__fillJingleStanza], - 'content-add-sent': [self.__fillJingleStanza], - 'session-initiate-sent': [self.__fillJingleStanza], - 'session-accept-sent': [self.__fillJingleStanza], - 'session-terminate-sent': [], - } - - def is_ready(self): - #print '[%s] %s, %s' % (self.media, self.candidates_ready, - # self.p2psession.get_property('codecs-ready')) - return (self.accepted and self.candidates_ready and not self.sent - and self.p2psession.get_property('codecs-ready')) - - def stanzaCB(self, stanza, content, error, action): - ''' Called when something related to our content was sent by peer. ''' - if action in self.callbacks: - for callback in self.callbacks[action]: - callback(stanza, content, error, action) - - def __transportInfoCB(self, stanza, content, error, action): - ''' Got a new transport candidate. ''' - candidates = [] - transport = content.getTag('transport') - for candidate in transport.iterTags('candidate'): - cand = farsight.Candidate() - cand.component_id = int(candidate['component']) - cand.ip = str(candidate['ip']) - cand.port = int(candidate['port']) - cand.foundation = str(candidate['foundation']) - #cand.type = farsight.CANDIDATE_TYPE_LOCAL - cand.priority = int(candidate['priority']) - - if candidate['protocol'] == 'udp': - cand.proto = farsight.NETWORK_PROTOCOL_UDP - else: - # we actually don't handle properly different tcp options in jingle - cand.proto = farsight.NETWORK_PROTOCOL_TCP - - cand.username = str(transport['ufrag']) - cand.password = str(transport['pwd']) - - #FIXME: huh? - types = {'host': farsight.CANDIDATE_TYPE_HOST, - 'srflx': farsight.CANDIDATE_TYPE_SRFLX, - 'prflx': farsight.CANDIDATE_TYPE_PRFLX, - 'relay': farsight.CANDIDATE_TYPE_RELAY, - 'multicast': farsight.CANDIDATE_TYPE_MULTICAST} - if 'type' in candidate and candidate['type'] in types: - cand.type = types[candidate['type']] - else: - print 'Unknown type %s', candidate['type'] - candidates.append(cand) - #FIXME: connectivity should not be etablished yet - # Instead, it should be etablished after session-accept! - if len(candidates) > 0: - if self.sent: - self.p2pstream.set_remote_candidates(candidates) - else: - self.remote_candidates.extend(candidates) - #self.p2pstream.set_remote_candidates(candidates) - #print self.media, self.creator, self.name, candidates - - def __content(self, payload=[]): - ''' Build a XML content-wrapper for our data. ''' - return xmpp.Node('content', - attrs={'name': self.name, 'creator': self.creator}, - payload=payload) - - def __candidate(self, candidate): - types = {farsight.CANDIDATE_TYPE_HOST: 'host', - farsight.CANDIDATE_TYPE_SRFLX: 'srflx', - farsight.CANDIDATE_TYPE_PRFLX: 'prflx', - farsight.CANDIDATE_TYPE_RELAY: 'relay', - farsight.CANDIDATE_TYPE_MULTICAST: 'multicast'} - attrs = { - 'component': candidate.component_id, - 'foundation': '1', # hack - 'generation': '0', - 'ip': candidate.ip, - 'network': '0', - 'port': candidate.port, - 'priority': int(candidate.priority), # hack - } - if candidate.type in types: - attrs['type'] = types[candidate.type] - if candidate.proto == farsight.NETWORK_PROTOCOL_UDP: - attrs['protocol'] = 'udp' - else: - # we actually don't handle properly different tcp options in jingle - attrs['protocol'] = 'tcp' - return xmpp.Node('candidate', attrs=attrs) - - def iter_candidates(self): - for candidate in self.candidates: - yield self.__candidate(candidate) - - def send_candidate(self, candidate): - content = self.__content() - transport = content.addChild(xmpp.NS_JINGLE_ICE_UDP + ' transport') - - if candidate.username and candidate.password: - transport['ufrag'] = candidate.username - transport['pwd'] = candidate.password - - transport.addChild(node=self.__candidate(candidate)) - self.session.send_transport_info(content) - - def __fillJingleStanza(self, stanza, content, error, action): - ''' Add our things to session-initiate stanza. ''' - self._fillContent(content) - - self.sent = True - - if self.candidates and self.candidates[0].username and \ - self.candidates[0].password: - attrs = {'ufrag': self.candidates[0].username, - 'pwd': self.candidates[0].password} - else: - attrs = {} - content.addChild(xmpp.NS_JINGLE_ICE_UDP + ' transport', attrs=attrs, - payload=self.iter_candidates()) - - def destroy(self): - self.callbacks = None - del self.session.contents[(self.creator, self.name)] - - -class JingleRTPContent(JingleContent): - def __init__(self, session, media, node=None): - JingleContent.__init__(self, session, node) - self.media = media - self._dtmf_running = False - self.farsight_media = {'audio': farsight.MEDIA_TYPE_AUDIO, - 'video': farsight.MEDIA_TYPE_VIDEO}[media] - self.got_codecs = False - - self.candidates_ready = False # True when local candidates are prepared - - self.callbacks['session-initiate'] += [self.__getRemoteCodecsCB] - self.callbacks['content-add'] += [self.__getRemoteCodecsCB] - self.callbacks['content-accept'] += [self.__getRemoteCodecsCB, - self.__contentAcceptCB] - self.callbacks['session-accept'] += [self.__getRemoteCodecsCB, - self.__contentAcceptCB] - self.callbacks['session-accept-sent'] += [self.__contentAcceptCB] - self.callbacks['content-accept-sent'] += [self.__contentAcceptCB] - self.callbacks['session-terminate'] += [self.__stop] - self.callbacks['session-terminate-sent'] += [self.__stop] - - def setup_stream(self): - # pipeline and bus - self.pipeline = gst.Pipeline() - bus = self.pipeline.get_bus() - bus.add_signal_watch() - bus.connect('message', self._on_gst_message) - - # conference - self.conference = gst.element_factory_make('fsrtpconference') - self.conference.set_property("sdes-cname", self.session.ourjid) - self.pipeline.add(self.conference) - self.funnel = None - - self.p2psession = self.conference.new_session(self.farsight_media) - - participant = self.conference.new_participant(self.session.peerjid) - #FIXME: Consider a workaround, here... - # pidgin and telepathy-gabble don't follow the XEP, and it won't work - # due to bad controlling-mode - params = {'controlling-mode': self.session.weinitiate,# 'debug': False} - 'stun-ip': '69.0.208.27', 'debug': False} - - self.p2pstream = self.p2psession.new_stream(participant, - farsight.DIRECTION_RECV, 'nice', params) - - def batch_dtmf(self, events): - if self._dtmf_running: - raise Exception #TODO: Proper exception - self._dtmf_running = True - self._start_dtmf(events.pop(0)) - gobject.timeout_add(500, self._next_dtmf, events) - - def _next_dtmf(self, events): - self._stop_dtmf() - if events: - self._start_dtmf(events.pop(0)) - gobject.timeout_add(500, self._next_dtmf, events) - else: - self._dtmf_running = False - - def _start_dtmf(self, event): - if event in ('*', '#'): - event = {'*': farsight.DTMF_EVENT_STAR, - '#': farsight.DTMF_EVENT_POUND}[event] - else: - event = int(event) - self.p2psession.start_telephony_event(event, 2, - farsight.DTMF_METHOD_RTP_RFC4733) - - def _stop_dtmf(self): - self.p2psession.stop_telephony_event(farsight.DTMF_METHOD_RTP_RFC4733) - - def _fillContent(self, content): - content.addChild(xmpp.NS_JINGLE_RTP + ' description', - attrs={'media': self.media}, payload=self.iter_codecs()) - - def _setup_funnel(self): - self.funnel = gst.element_factory_make('fsfunnel') - self.pipeline.add(self.funnel) - self.funnel.set_state(gst.STATE_PLAYING) - self.sink.set_state(gst.STATE_PLAYING) - self.funnel.link(self.sink) - - def _on_src_pad_added(self, stream, pad, codec): - if not self.funnel: - self._setup_funnel() - pad.link(self.funnel.get_pad('sink%d')) - - def _on_gst_message(self, bus, message): - if message.type == gst.MESSAGE_ELEMENT: - name = message.structure.get_name() - if name == 'farsight-new-active-candidate-pair': - pass - elif name == 'farsight-recv-codecs-changed': - pass - elif name == 'farsight-codecs-changed': - if self.is_ready(): - self.session.on_session_state_changed(self) - #TODO: description-info - elif name == 'farsight-local-candidates-prepared': - self.candidates_ready = True - if self.is_ready(): - self.session.on_session_state_changed(self) - elif name == 'farsight-new-local-candidate': - candidate = message.structure['candidate'] - self.candidates.append(candidate) - if self.candidates_ready: - #FIXME: Is this case even possible? - self.send_candidate(candidate) - elif name == 'farsight-component-state-changed': - state = message.structure['state'] - print message.structure['component'], state - if state == farsight.STREAM_STATE_FAILED: - reason = xmpp.Node('reason') - reason.setTag('failed-transport') - self.session._session_terminate(reason) - elif name == 'farsight-error': - print 'Farsight error #%d!' % message.structure['error-no'] - print 'Message: %s' % message.structure['error-msg'] - print 'Debug: %s' % message.structure['debug-msg'] - else: - print name - - def __contentAcceptCB(self, stanza, content, error, action): - if self.accepted: - if len(self.remote_candidates) > 0: - self.p2pstream.set_remote_candidates(self.remote_candidates) - self.remote_candidates = [] - #TODO: farsight.DIRECTION_BOTH only if senders='both' - self.p2pstream.set_property('direction', farsight.DIRECTION_BOTH) - self.session.content_negociated(self.media) - - def __getRemoteCodecsCB(self, stanza, content, error, action): - ''' Get peer codecs from what we get from peer. ''' - if self.got_codecs: - return - - codecs = [] - for codec in content.getTag('description').iterTags('payload-type'): - c = farsight.Codec(int(codec['id']), codec['name'], - self.farsight_media, int(codec['clockrate'])) - if 'channels' in codec: - c.channels = int(codec['channels']) - else: - c.channels = 1 - c.optional_params = [(str(p['name']), str(p['value'])) for p in \ - codec.iterTags('parameter')] - codecs.append(c) - - if len(codecs) > 0: - #FIXME: Handle this case: - # glib.GError: There was no intersection between the remote codecs and - # the local ones - self.p2pstream.set_remote_codecs(codecs) - self.got_codecs = True - - def iter_codecs(self): - codecs = self.p2psession.get_property('codecs') - for codec in codecs: - attrs = {'name': codec.encoding_name, - 'id': codec.id, - 'channels': codec.channels} - if codec.clock_rate: - attrs['clockrate'] = codec.clock_rate - if codec.optional_params: - payload = (xmpp.Node('parameter', {'name': name, 'value': value}) - for name, value in codec.optional_params) - else: payload = () - yield xmpp.Node('payload-type', attrs, payload) - - def __stop(self, *things): - self.pipeline.set_state(gst.STATE_NULL) - - def __del__(self): - self.__stop() - - def destroy(self): - JingleContent.destroy(self) - self.p2pstream.disconnect_by_func(self._on_src_pad_added) - self.pipeline.get_bus().disconnect_by_func(self._on_gst_message) - - -class JingleVoIP(JingleRTPContent): - ''' Jingle VoIP sessions consist of audio content transported - over an ICE UDP protocol. ''' - def __init__(self, session, node=None): - JingleRTPContent.__init__(self, session, 'audio', node) - self.setup_stream() - - - ''' Things to control the gstreamer's pipeline ''' - def setup_stream(self): - JingleRTPContent.setup_stream(self) - - # Configure SPEEX - # Workaround for psi (not needed since rev - # 147aedcea39b43402fe64c533d1866a25449888a): - # place 16kHz before 8kHz, as buggy psi versions will take in - # account only the first codec - - codecs = [farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX', - farsight.MEDIA_TYPE_AUDIO, 16000), - farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX', - farsight.MEDIA_TYPE_AUDIO, 8000)] - self.p2psession.set_codec_preferences(codecs) - - # the local parts - # TODO: use gconfaudiosink? - # sink = get_first_gst_element(['alsasink', 'osssink', 'autoaudiosink']) - self.sink = gst.element_factory_make('alsasink') - self.sink.set_property('sync', False) - #sink.set_property('latency-time', 20000) - #sink.set_property('buffer-time', 80000) - - # TODO: use gconfaudiosrc? - src_mic = gst.element_factory_make('alsasrc') - src_mic.set_property('blocksize', 320) - - self.mic_volume = gst.element_factory_make('volume') - self.mic_volume.set_property('volume', 1) - - # link gst elements - self.pipeline.add(self.sink, src_mic, self.mic_volume) - src_mic.link(self.mic_volume) - - self.mic_volume.get_pad('src').link(self.p2psession.get_property( - 'sink-pad')) - self.p2pstream.connect('src-pad-added', self._on_src_pad_added) - - # The following is needed for farsight to process ICE requests: - self.pipeline.set_state(gst.STATE_PLAYING) - - -class JingleVideo(JingleRTPContent): - def __init__(self, session, node=None): - JingleRTPContent.__init__(self, session, 'video', node) - self.setup_stream() - - ''' Things to control the gstreamer's pipeline ''' - def setup_stream(self): - #TODO: Everything is not working properly: - # sometimes, one window won't show up, - # sometimes it'll freeze... - JingleRTPContent.setup_stream(self) - # the local parts - src_vid = gst.element_factory_make('videotestsrc') - src_vid.set_property('is-live', True) - videoscale = gst.element_factory_make('videoscale') - caps = gst.element_factory_make('capsfilter') - caps.set_property('caps', gst.caps_from_string('video/x-raw-yuv, width=320, height=240')) - colorspace = gst.element_factory_make('ffmpegcolorspace') - - self.pipeline.add(src_vid, videoscale, caps, colorspace) - gst.element_link_many(src_vid, videoscale, caps, colorspace) - - self.sink = gst.element_factory_make('xvimagesink') - self.pipeline.add(self.sink) - - colorspace.get_pad('src').link(self.p2psession.get_property('sink-pad')) - self.p2pstream.connect('src-pad-added', self._on_src_pad_added) - - # The following is needed for farsight to process ICE requests: - self.pipeline.set_state(gst.STATE_PLAYING) +from jingle_session import JingleSession +from jingle_rtp import JingleAudio, JingleVideo class ConnectionJingle(object): @@ -1158,11 +98,11 @@ class ConnectionJingle(object): return self.get_jingle_session(jid, media='audio').sid jingle = self.get_jingle_session(jid, media='video') if jingle: - jingle.add_content('voice', JingleVoIP(jingle)) + jingle.add_content('voice', JingleAudio(jingle)) else: jingle = JingleSession(self, weinitiate=True, jid=jid) self.add_jingle(jingle) - jingle.add_content('voice', JingleVoIP(jingle)) + jingle.add_content('voice', JingleAudio(jingle)) jingle.start_session() return jingle.sid diff --git a/src/common/jingle_content.py b/src/common/jingle_content.py new file mode 100644 index 000000000..98b5dfa0a --- /dev/null +++ b/src/common/jingle_content.py @@ -0,0 +1,102 @@ +## +## Copyright (C) 2006 Gajim Team +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +''' Handles Jingle contents (XEP 0166). ''' + +contents = {} + +def get_jingle_content(node): + namespace = node.getNamespace() + if namespace in contents: + return contents[namespace](node) + else: + return None + + +class JingleContent(object): + ''' An abstraction of content in Jingle sessions. ''' + def __init__(self, session, transport): + self.session = session + self.transport = transport + # will be filled by JingleSession.add_content() + # don't uncomment these lines, we will catch more buggy code then + # (a JingleContent not added to session shouldn't send anything) + #self.creator = None + #self.name = None + self.accepted = False + self.sent = False + + self.media = None + + self.senders = 'both' #FIXME + self.allow_sending = True # Used for stream direction, attribute 'senders' + + self.callbacks = { + # these are called when *we* get stanzas + 'content-accept': [self.__transportInfoCB], + 'content-add': [self.__transportInfoCB], + 'content-modify': [], + 'content-reject': [], + 'content-remove': [], + 'description-info': [], + 'security-info': [], + 'session-accept': [self.__transportInfoCB], + 'session-info': [], + 'session-initiate': [self.__transportInfoCB], + 'session-terminate': [], + 'transport-info': [self.__transportInfoCB], + 'transport-replace': [], + 'transport-accept': [], + 'transport-reject': [], + 'iq-result': [], + 'iq-error': [], + # these are called when *we* sent these stanzas + 'content-accept-sent': [self.__fillJingleStanza], + 'content-add-sent': [self.__fillJingleStanza], + 'session-initiate-sent': [self.__fillJingleStanza], + 'session-accept-sent': [self.__fillJingleStanza], + 'session-terminate-sent': [], + } + + def is_ready(self): + return (self.accepted and not self.sent) + + def stanzaCB(self, stanza, content, error, action): + ''' Called when something related to our content was sent by peer. ''' + if action in self.callbacks: + for callback in self.callbacks[action]: + callback(stanza, content, error, action) + + def __transportInfoCB(self, stanza, content, error, action): + ''' Got a new transport candidate. ''' + self.transport.transportInfoCB(content.getTag('transport')) + + def __content(self, payload=[]): + ''' Build a XML content-wrapper for our data. ''' + return xmpp.Node('content', + attrs={'name': self.name, 'creator': self.creator}, + payload=payload) + + def send_candidate(self, candidate): + content = self.__content() + content.addChild(self.transport.make_transport([candidate])) + self.session.send_transport_info(content) + + def __fillJingleStanza(self, stanza, content, error, action): + ''' Add our things to session-initiate stanza. ''' + self._fillContent(content) + self.sent = True + content.addChild(node=self.transport.make_transport()) + + def destroy(self): + self.callbacks = None + del self.session.contents[(self.creator, self.name)] diff --git a/src/common/jingle_rtp.py b/src/common/jingle_rtp.py new file mode 100644 index 000000000..7fb9c2611 --- /dev/null +++ b/src/common/jingle_rtp.py @@ -0,0 +1,314 @@ +## +## Copyright (C) 2006 Gajim Team +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +''' Handles Jingle RTP sessions (XEP 0167). ''' + + +import gobject + +import xmpp +import farsight, gst + +from jingle_transport import JingleTransportICEUDP +from jingle_content import contents, JingleContent + +# TODO: Will that be even used? +def get_first_gst_element(elements): + ''' Returns, if it exists, the first available element of the list. ''' + for name in elements: + factory = gst.element_factory_find(name) + if factory: + return factory.create() + + +class JingleRTPContent(JingleContent): + def __init__(self, session, media, transport=None): + if transport is None: + transport = JingleTransportICEUDP() + JingleContent.__init__(self, session, transport) + self.media = media + self._dtmf_running = False + self.farsight_media = {'audio': farsight.MEDIA_TYPE_AUDIO, + 'video': farsight.MEDIA_TYPE_VIDEO}[media] + self.got_codecs = False + + self.candidates_ready = False # True when local candidates are prepared + + self.callbacks['session-initiate'] += [self.__getRemoteCodecsCB] + self.callbacks['content-add'] += [self.__getRemoteCodecsCB] + self.callbacks['content-accept'] += [self.__getRemoteCodecsCB, + self.__contentAcceptCB] + self.callbacks['session-accept'] += [self.__getRemoteCodecsCB, + self.__contentAcceptCB] + self.callbacks['session-accept-sent'] += [self.__contentAcceptCB] + self.callbacks['content-accept-sent'] += [self.__contentAcceptCB] + self.callbacks['session-terminate'] += [self.__stop] + self.callbacks['session-terminate-sent'] += [self.__stop] + + def setup_stream(self): + # pipeline and bus + self.pipeline = gst.Pipeline() + bus = self.pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message', self._on_gst_message) + + # conference + self.conference = gst.element_factory_make('fsrtpconference') + self.conference.set_property("sdes-cname", self.session.ourjid) + self.pipeline.add(self.conference) + self.funnel = None + + self.p2psession = self.conference.new_session(self.farsight_media) + + participant = self.conference.new_participant(self.session.peerjid) + #FIXME: Consider a workaround, here... + # pidgin and telepathy-gabble don't follow the XEP, and it won't work + # due to bad controlling-mode + params = {'controlling-mode': self.session.weinitiate,# 'debug': False} + 'stun-ip': '69.0.208.27', 'debug': False} + + self.p2pstream = self.p2psession.new_stream(participant, + farsight.DIRECTION_RECV, 'nice', params) + + def is_ready(self): + return (JingleContent.is_ready(self) and self.candidates_ready + and self.p2psession.get_property('codecs-ready')) + + def batch_dtmf(self, events): + if self._dtmf_running: + raise Exception #TODO: Proper exception + self._dtmf_running = True + self._start_dtmf(events.pop(0)) + gobject.timeout_add(500, self._next_dtmf, events) + + def _next_dtmf(self, events): + self._stop_dtmf() + if events: + self._start_dtmf(events.pop(0)) + gobject.timeout_add(500, self._next_dtmf, events) + else: + self._dtmf_running = False + + def _start_dtmf(self, event): + if event in ('*', '#'): + event = {'*': farsight.DTMF_EVENT_STAR, + '#': farsight.DTMF_EVENT_POUND}[event] + else: + event = int(event) + self.p2psession.start_telephony_event(event, 2, + farsight.DTMF_METHOD_RTP_RFC4733) + + def _stop_dtmf(self): + self.p2psession.stop_telephony_event(farsight.DTMF_METHOD_RTP_RFC4733) + + def _fillContent(self, content): + content.addChild(xmpp.NS_JINGLE_RTP + ' description', + attrs={'media': self.media}, payload=self.iter_codecs()) + + def _setup_funnel(self): + self.funnel = gst.element_factory_make('fsfunnel') + self.pipeline.add(self.funnel) + self.funnel.set_state(gst.STATE_PLAYING) + self.sink.set_state(gst.STATE_PLAYING) + self.funnel.link(self.sink) + + def _on_src_pad_added(self, stream, pad, codec): + if not self.funnel: + self._setup_funnel() + pad.link(self.funnel.get_pad('sink%d')) + + def _on_gst_message(self, bus, message): + if message.type == gst.MESSAGE_ELEMENT: + name = message.structure.get_name() + if name == 'farsight-new-active-candidate-pair': + pass + elif name == 'farsight-recv-codecs-changed': + pass + elif name == 'farsight-codecs-changed': + if self.is_ready(): + self.session.on_session_state_changed(self) + #TODO: description-info + elif name == 'farsight-local-candidates-prepared': + self.candidates_ready = True + if self.is_ready(): + self.session.on_session_state_changed(self) + elif name == 'farsight-new-local-candidate': + candidate = message.structure['candidate'] + self.transport.candidates.append(candidate) + if self.candidates_ready: + #FIXME: Is this case even possible? + self.send_candidate(candidate) + elif name == 'farsight-component-state-changed': + state = message.structure['state'] + print message.structure['component'], state + if state == farsight.STREAM_STATE_FAILED: + reason = xmpp.Node('reason') + reason.setTag('failed-transport') + self.session._session_terminate(reason) + elif name == 'farsight-error': + print 'Farsight error #%d!' % message.structure['error-no'] + print 'Message: %s' % message.structure['error-msg'] + print 'Debug: %s' % message.structure['debug-msg'] + else: + print name + + def __contentAcceptCB(self, stanza, content, error, action): + if self.accepted: + if self.transport.remote_candidates: + self.p2pstream.set_remote_candidates(self.transport.remote_candidates) + self.transport.remote_candidates = [] + #TODO: farsight.DIRECTION_BOTH only if senders='both' + self.p2pstream.set_property('direction', farsight.DIRECTION_BOTH) + self.session.content_negociated(self.media) + + def __getRemoteCodecsCB(self, stanza, content, error, action): + ''' Get peer codecs from what we get from peer. ''' + if self.got_codecs: + return + + codecs = [] + for codec in content.getTag('description').iterTags('payload-type'): + c = farsight.Codec(int(codec['id']), codec['name'], + self.farsight_media, int(codec['clockrate'])) + if 'channels' in codec: + c.channels = int(codec['channels']) + else: + c.channels = 1 + c.optional_params = [(str(p['name']), str(p['value'])) for p in \ + codec.iterTags('parameter')] + codecs.append(c) + + if len(codecs) > 0: + #FIXME: Handle this case: + # glib.GError: There was no intersection between the remote codecs and + # the local ones + self.p2pstream.set_remote_codecs(codecs) + self.got_codecs = True + + def iter_codecs(self): + codecs = self.p2psession.get_property('codecs') + for codec in codecs: + attrs = {'name': codec.encoding_name, + 'id': codec.id, + 'channels': codec.channels} + if codec.clock_rate: + attrs['clockrate'] = codec.clock_rate + if codec.optional_params: + payload = (xmpp.Node('parameter', {'name': name, 'value': value}) + for name, value in codec.optional_params) + else: payload = () + yield xmpp.Node('payload-type', attrs, payload) + + def __stop(self, *things): + self.pipeline.set_state(gst.STATE_NULL) + + def __del__(self): + self.__stop() + + def destroy(self): + JingleContent.destroy(self) + self.p2pstream.disconnect_by_func(self._on_src_pad_added) + self.pipeline.get_bus().disconnect_by_func(self._on_gst_message) + + +class JingleAudio(JingleRTPContent): + ''' Jingle VoIP sessions consist of audio content transported + over an ICE UDP protocol. ''' + def __init__(self, session, transport=None): + JingleRTPContent.__init__(self, session, 'audio', transport) + self.setup_stream() + + + ''' Things to control the gstreamer's pipeline ''' + def setup_stream(self): + JingleRTPContent.setup_stream(self) + + # Configure SPEEX + # Workaround for psi (not needed since rev + # 147aedcea39b43402fe64c533d1866a25449888a): + # place 16kHz before 8kHz, as buggy psi versions will take in + # account only the first codec + + codecs = [farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX', + farsight.MEDIA_TYPE_AUDIO, 16000), + farsight.Codec(farsight.CODEC_ID_ANY, 'SPEEX', + farsight.MEDIA_TYPE_AUDIO, 8000)] + self.p2psession.set_codec_preferences(codecs) + + # the local parts + # TODO: use gconfaudiosink? + # sink = get_first_gst_element(['alsasink', 'osssink', 'autoaudiosink']) + self.sink = gst.element_factory_make('alsasink') + self.sink.set_property('sync', False) + #sink.set_property('latency-time', 20000) + #sink.set_property('buffer-time', 80000) + + # TODO: use gconfaudiosrc? + src_mic = gst.element_factory_make('alsasrc') + src_mic.set_property('blocksize', 320) + + self.mic_volume = gst.element_factory_make('volume') + self.mic_volume.set_property('volume', 1) + + # link gst elements + self.pipeline.add(self.sink, src_mic, self.mic_volume) + src_mic.link(self.mic_volume) + + self.mic_volume.get_pad('src').link(self.p2psession.get_property( + 'sink-pad')) + self.p2pstream.connect('src-pad-added', self._on_src_pad_added) + + # The following is needed for farsight to process ICE requests: + self.pipeline.set_state(gst.STATE_PLAYING) + + +class JingleVideo(JingleRTPContent): + def __init__(self, session, transport=None): + JingleRTPContent.__init__(self, session, 'video', transport) + self.setup_stream() + + ''' Things to control the gstreamer's pipeline ''' + def setup_stream(self): + #TODO: Everything is not working properly: + # sometimes, one window won't show up, + # sometimes it'll freeze... + JingleRTPContent.setup_stream(self) + # the local parts + src_vid = gst.element_factory_make('videotestsrc') + src_vid.set_property('is-live', True) + videoscale = gst.element_factory_make('videoscale') + caps = gst.element_factory_make('capsfilter') + caps.set_property('caps', gst.caps_from_string('video/x-raw-yuv, width=320, height=240')) + colorspace = gst.element_factory_make('ffmpegcolorspace') + + self.pipeline.add(src_vid, videoscale, caps, colorspace) + gst.element_link_many(src_vid, videoscale, caps, colorspace) + + self.sink = gst.element_factory_make('xvimagesink') + self.pipeline.add(self.sink) + + colorspace.get_pad('src').link(self.p2psession.get_property('sink-pad')) + self.p2pstream.connect('src-pad-added', self._on_src_pad_added) + + # The following is needed for farsight to process ICE requests: + self.pipeline.set_state(gst.STATE_PLAYING) + + +def get_content(desc): + if desc['media'] == 'audio': + return JingleAudio + elif desc['media'] == 'video': + return JingleVideo + else: + return None + +contents[xmpp.NS_JINGLE_RTP] = get_content diff --git a/src/common/jingle_session.py b/src/common/jingle_session.py new file mode 100644 index 000000000..9118affdf --- /dev/null +++ b/src/common/jingle_session.py @@ -0,0 +1,613 @@ +## +## Copyright (C) 2006 Gajim Team +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +''' Handles Jingle sessions (XEP 0166). ''' + +#TODO: +# * Have JingleContent here +# * 'senders' attribute of 'content' element +# * security preconditions +# * actions: +# - content-modify +# - description-info, session-info +# - security-info +# - transport-accept, transport-reject +# - Tie-breaking +# * timeout + +import gajim #Get rid of that? +import xmpp +from jingle_transport import get_jingle_transport +from jingle_content import get_jingle_content + +#FIXME: Move it to JingleSession.States? +class JingleStates(object): + ''' States in which jingle session may exist. ''' + ended = 0 + pending = 1 + active = 2 + +class OutOfOrder(Exception): + ''' Exception that should be raised when an action is received when in the wrong state. ''' + +class TieBreak(Exception): + ''' Exception that should be raised in case of a tie, when we overrule the other action. ''' + +class JingleSession(object): + ''' This represents one jingle session. ''' + def __init__(self, con, weinitiate, jid, sid=None): + ''' con -- connection object, + weinitiate -- boolean, are we the initiator? + jid - jid of the other entity''' + self.contents = {} # negotiated contents + self.connection = con # connection to use + # our full jid + #FIXME: Get rid of gajim here? + self.ourjid = gajim.get_jid_from_account(self.connection.name) + '/' + \ + con.server_resource + self.peerjid = jid # jid we connect to + # jid we use as the initiator + self.initiator = weinitiate and self.ourjid or self.peerjid + # jid we use as the responder + self.responder = weinitiate and self.peerjid or self.ourjid + # are we an initiator? + self.weinitiate = weinitiate + # what state is session in? (one from JingleStates) + self.state = JingleStates.ended + if not sid: + sid = con.connection.getAnID() + self.sid = sid # sessionid + + self.accepted = True # is this session accepted by user + + # callbacks to call on proper contents + # use .prepend() to add new callbacks, especially when you're going + # to send error instead of ack + self.callbacks = { + 'content-accept': [self.__contentAcceptCB, self.__broadcastCB, + self.__defaultCB], + 'content-add': [self.__contentAddCB, self.__broadcastCB, + self.__defaultCB], #TODO + 'content-modify': [self.__defaultCB], #TODO + 'content-reject': [self.__defaultCB, self.__contentRemoveCB], #TODO + 'content-remove': [self.__defaultCB, self.__contentRemoveCB], + 'description-info': [self.__broadcastCB, self.__defaultCB], #TODO + 'security-info': [self.__defaultCB], #TODO + 'session-accept': [self.__sessionAcceptCB, self.__contentAcceptCB, + self.__broadcastCB, self.__defaultCB], + 'session-info': [self.__sessionInfoCB, self.__broadcastCB, self.__defaultCB], + 'session-initiate': [self.__sessionInitiateCB, self.__broadcastCB, + self.__defaultCB], + 'session-terminate': [self.__sessionTerminateCB, self.__broadcastAllCB, + self.__defaultCB], + 'transport-info': [self.__broadcastCB, self.__defaultCB], + 'transport-replace': [self.__broadcastCB, self.__transportReplaceCB], #TODO + 'transport-accept': [self.__defaultCB], #TODO + 'transport-reject': [self.__defaultCB], #TODO + 'iq-result': [], + 'iq-error': [self.__errorCB], + } + + ''' Interaction with user ''' + def approve_session(self): + ''' Called when user accepts session in UI (when we aren't the initiator). + ''' + self.accept_session() + + def decline_session(self): + ''' Called when user declines session in UI (when we aren't the initiator) + ''' + reason = xmpp.Node('reason') + reason.addChild('decline') + self._session_terminate(reason) + + def approve_content(self, media): + content = self.get_content(media) + if content: + content.accepted = True + self.on_session_state_changed(content) + + def reject_content(self, media): + content = self.get_content(media) + if content: + if self.state == JingleStates.active: + self.__content_reject(content) + content.destroy() + self.on_session_state_changed() + + def end_session(self): + ''' Called when user stops or cancel session in UI. ''' + reason = xmpp.Node('reason') + if self.state == JingleStates.active: + reason.addChild('success') + else: + reason.addChild('cancel') + self._session_terminate(reason) + + ''' Middle-level functions to manage contents. Handle local content + cache and send change notifications. ''' + def get_content(self, media=None): + if media is None: + return None + + for content in self.contents.values(): + if content.media == media: + return content + + def add_content(self, name, content, creator='we'): + ''' Add new content to session. If the session is active, + this will send proper stanza to update session. + Creator must be one of ('we', 'peer', 'initiator', 'responder')''' + assert creator in ('we', 'peer', 'initiator', 'responder') + + if (creator == 'we' and self.weinitiate) or (creator == 'peer' and \ + not self.weinitiate): + creator = 'initiator' + elif (creator == 'peer' and self.weinitiate) or (creator == 'we' and \ + not self.weinitiate): + creator = 'responder' + content.creator = creator + content.name = name + self.contents[(creator, name)] = content + + if (creator == 'initiator') == self.weinitiate: + # The content is from us, accept it + content.accepted = True + + def remove_content(self, creator, name): + ''' We do not need this now ''' + #TODO: + if (creator, name) in self.contents: + content = self.contents[(creator, name)] + if len(self.contents) > 1: + self.__content_remove(content) + self.contents[(creator, name)].destroy() + if len(self.contents) == 0: + self.end_session() + + def modify_content(self, creator, name, *someother): + ''' We do not need this now ''' + pass + + def on_session_state_changed(self, content=None): + if self.state == JingleStates.ended: + # Session not yet started, only one action possible: session-initiate + if self.is_ready() and self.weinitiate: + self.__session_initiate() + elif self.state == JingleStates.pending: + # We can either send a session-accept or a content-add + if self.is_ready() and not self.weinitiate: + self.__session_accept() + elif content and (content.creator == 'initiator') == self.weinitiate: + self.__content_add(content) + elif content and self.weinitiate: + self.__content_accept(content) + elif self.state == JingleStates.active: + # We can either send a content-add or a content-accept + if not content: + return + if (content.creator == 'initiator') == self.weinitiate: + # We initiated this content. It's a pending content-add. + self.__content_add(content) + else: + # The other side created this content, we accept it. + self.__content_accept(content) + + def is_ready(self): + ''' Returns True when all codecs and candidates are ready + (for all contents). ''' + return (all((content.is_ready() for content in self.contents.itervalues())) + and self.accepted) + + ''' Middle-level function to do stanza exchange. ''' + def accept_session(self): + ''' Mark the session as accepted. ''' + self.accepted = True + self.on_session_state_changed() + + def start_session(self): + ''' Mark the session as ready to be started. ''' + self.accepted = True + self.on_session_state_changed() + + def send_session_info(self): + pass + + def send_content_accept(self, content): + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('content-accept') + jingle.addChild(node=content) + self.connection.connection.send(stanza) + + def send_transport_info(self, content): + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('transport-info') + jingle.addChild(node=content) + self.connection.connection.send(stanza) + + ''' Session callbacks. ''' + def stanzaCB(self, stanza): + ''' A callback for ConnectionJingle. It gets stanza, then + tries to send it to all internally registered callbacks. + First one to raise xmpp.NodeProcessed breaks function.''' + jingle = stanza.getTag('jingle') + error = stanza.getTag('error') + if error: + # it's an iq-error stanza + action = 'iq-error' + elif jingle: + # it's a jingle action + action = jingle.getAttr('action') + if action not in self.callbacks: + self.__send_error(stanza, 'bad_request') + return + #FIXME: If we aren't initiated and it's not a session-initiate... + if action != 'session-initiate' and self.state == JingleStates.ended: + self.__send_error(stanza, 'item-not-found', 'unknown-session') + return + else: + # it's an iq-result (ack) stanza + action = 'iq-result' + + callables = self.callbacks[action] + + try: + for callable in callables: + callable(stanza=stanza, jingle=jingle, error=error, action=action) + except xmpp.NodeProcessed: + pass + except TieBreak: + self.__send_error(stanza, 'conflict', 'tiebreak') + except OutOfOrder: + self.__send_error(stanza, 'unexpected-request', 'out-of-order')#FIXME + + def __defaultCB(self, stanza, jingle, error, action): + ''' Default callback for action stanzas -- simple ack + and stop processing. ''' + response = stanza.buildReply('result') + self.connection.connection.send(response) + + def __errorCB(self, stanza, jingle, error, action): + #FIXME + text = error.getTagData('text') + jingle_error = None + xmpp_error = None + for child in error.getChildren(): + if child.getNamespace() == xmpp.NS_JINGLE_ERRORS: + jingle_error = child.getName() + elif child.getNamespace() == xmpp.NS_STANZAS: + xmpp_error = child.getName() + self.__dispatch_error(xmpp_error, jingle_error, text) + #FIXME: Not sure when we would want to do that... + if xmpp_error == 'item-not-found': + self.connection.delete_jingle_session(self.peerjid, self.sid) + + def __transportReplaceCB(self, stanza, jingle, error, action): + for content in jingle.iterTags('content'): + creator = content['creator'] + name = content['name'] + if (creator, name) in self.contents: + transport_ns = content.getTag('transport').getNamespace() + if transport_ns == xmpp.JINGLE_ICE_UDP: + #FIXME: We don't manage anything else than ICE-UDP now... + #What was the previous transport?!? + #Anyway, content's transport is not modifiable yet + pass + else: + stanza, jingle = self.__make_jingle('transport-reject') + content = jingle.setTag('content', attrs={'creator': creator, + 'name': name}) + content.setTag('transport', namespace=transport_ns) + self.connection.connection.send(stanza) + raise xmpp.NodeProcessed + else: + #FIXME: This ressource is unknown to us, what should we do? + #For now, reject the transport + stanza, jingle = self.__make_jingle('transport-reject') + c = jingle.setTag('content', attrs={'creator': creator, + 'name': name}) + c.setTag('transport', namespace=transport_ns) + self.connection.connection.send(stanza) + raise xmpp.NodeProcessed + + def __sessionInfoCB(self, stanza, jingle, error, action): + #TODO: ringing, active, (un)hold, (un)mute + payload = jingle.getPayload() + if len(payload) > 0: + self.__send_error(stanza, 'feature-not-implemented', 'unsupported-info') + raise xmpp.NodeProcessed + + def __contentRemoveCB(self, stanza, jingle, error, action): + for content in jingle.iterTags('content'): + creator = content['creator'] + name = content['name'] + if (creator, name) in self.contents: + content = self.contents[(creator, name)] + #TODO: this will fail if content is not an RTP content + self.connection.dispatch('JINGLE_DISCONNECTED', + (self.peerjid, self.sid, content.media, 'removed')) + content.destroy() + if len(self.contents) == 0: + reason = xmpp.Node('reason') + reason.setTag('success') + self._session_terminate(reason) + + def __sessionAcceptCB(self, stanza, jingle, error, action): + if self.state != JingleStates.pending: #FIXME + raise OutOfOrder + self.state = JingleStates.active + + def __contentAcceptCB(self, stanza, jingle, error, action): + ''' Called when we get content-accept stanza or equivalent one + (like session-accept).''' + # check which contents are accepted + for content in jingle.iterTags('content'): + creator = content['creator'] + name = content['name']#TODO... + + def __contentAddCB(self, stanza, jingle, error, action): + if self.state == JingleStates.ended: + raise OutOfOrder + + parse_result = self.__parse_contents(jingle) + contents = parse_result[2] + rejected_contents = parse_result[3] + + for name, creator in rejected_contents: + #TODO: + content = JingleContent() + self.add_content(name, content, creator) + self.__content_reject(content) + self.contents[(content.creator, content.name)].destroy() + + self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid, + contents)) + + def __sessionInitiateCB(self, stanza, jingle, error, action): + ''' We got a jingle session request from other entity, + therefore we are the receiver... Unpack the data, + inform the user. ''' + + if self.state != JingleStates.ended: + raise OutOfOrder + + self.initiator = jingle['initiator'] + self.responder = self.ourjid + self.peerjid = self.initiator + self.accepted = False # user did not accept this session yet + + # TODO: If the initiator is unknown to the receiver (e.g., via presence + # subscription) and the receiver has a policy of not communicating via + # Jingle with unknown entities, it SHOULD return a + # error. + + # Lets check what kind of jingle session does the peer want + contents_ok, transports_ok, contents, pouet = self.__parse_contents(jingle) + + # If there's no content we understand... + if not contents_ok: + # TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate + reason = xmpp.Node('reason') + reason.setTag('unsupported-applications') + self.__defaultCB(stanza, jingle, error, action) + self._session_terminate(reason) + raise xmpp.NodeProcessed + + if not transports_ok: + # TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate + reason = xmpp.Node('reason') + reason.setTag('unsupported-transports') + self.__defaultCB(stanza, jingle, error, action) + self._session_terminate(reason) + raise xmpp.NodeProcessed + + self.state = JingleStates.pending + + # Send event about starting a session + self.connection.dispatch('JINGLE_INCOMING', (self.peerjid, self.sid, + contents)) + + def __broadcastCB(self, stanza, jingle, error, action): + ''' Broadcast the stanza contents to proper content handlers. ''' + for content in jingle.iterTags('content'): + name = content['name'] + creator = content['creator'] + cn = self.contents[(creator, name)] + cn.stanzaCB(stanza, content, error, action) + + def __sessionTerminateCB(self, stanza, jingle, error, action): + self.connection.delete_jingle_session(self.peerjid, self.sid) + reason, text = self.__reason_from_stanza(jingle) + if reason not in ('success', 'cancel', 'decline'): + self.__dispatch_error(reason, reason, text) + if text: + text = '%s (%s)' % (reason, text) + else: + text = reason#TODO + self.connection.dispatch('JINGLE_DISCONNECTED', + (self.peerjid, self.sid, None, text)) + + def __broadcastAllCB(self, stanza, jingle, error, action): + ''' Broadcast the stanza to all content handlers. ''' + for content in self.contents.itervalues(): + content.stanzaCB(stanza, None, error, action) + + ''' Internal methods. ''' + def __parse_contents(self, jingle): + #TODO: Needs some reworking + contents = [] + contents_rejected = [] + contents_ok = False + transports_ok = False + + for element in jingle.iterTags('content'): + transport = get_jingle_transport(element.getTag('transport')) + content_type = get_jingle_content(element.getTag('description')) + if content_type: + contents_ok = True + if transport: + content = content_type(self, transport) + self.add_content(element['name'], + content, 'peer') + contents.append((content.media,)) + transports_ok = True + else: + contents_rejected.append((element['name'], 'peer')) + else: + contents_rejected.append((element['name'], 'peer')) + + return (contents_ok, transports_ok, contents, contents_rejected) + + def __dispatch_error(self, error, jingle_error=None, text=None): + if jingle_error: + error = jingle_error + if text: + text = '%s (%s)' % (error, text) + else: + text = error + self.connection.dispatch('JINGLE_ERROR', (self.peerjid, self.sid, text)) + + def __reason_from_stanza(self, stanza): + reason = 'success' + reasons = ['success', 'busy', 'cancel', 'connectivity-error', + 'decline', 'expired', 'failed-application', 'failed-transport', + 'general-error', 'gone', 'incompatible-parameters', 'media-error', + 'security-error', 'timeout', 'unsupported-applications', + 'unsupported-transports'] + tag = stanza.getTag('reason') + if tag: + text = tag.getTagData('text') + for r in reasons: + if tag.getTag(r): + reason = r + break + return (reason, text) + + ''' Methods that make/send proper pieces of XML. They check if the session + is in appropriate state. ''' + def __make_jingle(self, action): + stanza = xmpp.Iq(typ='set', to=xmpp.JID(self.peerjid)) + attrs = {'action': action, + 'sid': self.sid} + if action == 'session-initiate': + attrs['initiator'] = self.initiator + elif action == 'session-accept': + attrs['responder'] = self.responder + jingle = stanza.addChild('jingle', attrs=attrs, namespace=xmpp.NS_JINGLE) + return stanza, jingle + + def __send_error(self, stanza, error, jingle_error=None, text=None): + err = xmpp.Error(stanza, error) + err.setNamespace(xmpp.NS_STANZAS) + if jingle_error: + err.setTag(jingle_error, namespace=xmpp.NS_JINGLE_ERRORS) + if text: + err.setTagData('text', text) + self.connection.connection.send(err) + self.__dispatch_error(error, jingle_error, text) + + def __append_content(self, jingle, content): + ''' Append element to element, + with (full=True) or without (full=False) + children. ''' + jingle.addChild('content', + attrs={'name': content.name, 'creator': content.creator}) + + def __append_contents(self, jingle): + ''' Append all elements to .''' + # TODO: integrate with __appendContent? + # TODO: parameters 'name', 'content'? + for content in self.contents.values(): + self.__append_content(jingle, content) + + def __session_initiate(self): + assert self.state == JingleStates.ended + stanza, jingle = self.__make_jingle('session-initiate') + self.__append_contents(jingle) + self.__broadcastCB(stanza, jingle, None, 'session-initiate-sent') + self.connection.connection.send(stanza) + self.state = JingleStates.pending + + def __session_accept(self): + assert self.state == JingleStates.pending + stanza, jingle = self.__make_jingle('session-accept') + self.__append_contents(jingle) + self.__broadcastCB(stanza, jingle, None, 'session-accept-sent') + self.connection.connection.send(stanza) + self.state = JingleStates.active + + def __session_info(self, payload=None): + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('session-info') + if payload: + jingle.addChild(node=payload) + self.connection.connection.send(stanza) + + def _session_terminate(self, reason=None): + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('session-terminate') + if reason is not None: + jingle.addChild(node=reason) + self.__broadcastAllCB(stanza, jingle, None, 'session-terminate-sent') + self.connection.connection.send(stanza) + reason, text = self.__reason_from_stanza(jingle) + if reason not in ('success', 'cancel', 'decline'): + self.__dispatch_error(reason, reason, text) + if text: + text = '%s (%s)' % (reason, text) + else: + text = reason + self.connection.delete_jingle_session(self.peerjid, self.sid) + self.connection.dispatch('JINGLE_DISCONNECTED', + (self.peerjid, self.sid, None, text)) + + def __content_add(self, content): + #TODO: test + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('content-add') + self.__append_content(jingle, content) + self.__broadcastCB(stanza, jingle, None, 'content-add-sent') + self.connection.connection.send(stanza) + + def __content_accept(self, content): + #TODO: test + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('content-accept') + self.__append_content(jingle, content) + self.__broadcastCB(stanza, jingle, None, 'content-accept-sent') + self.connection.connection.send(stanza) + + def __content_reject(self, content): + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('content-reject') + self.__append_content(jingle, content) + self.connection.connection.send(stanza) + #TODO: this will fail if content is not an RTP content + self.connection.dispatch('JINGLE_DISCONNECTED', + (self.peerjid, self.sid, content.media, 'rejected')) + + def __content_modify(self): + assert self.state != JingleStates.ended + + def __content_remove(self, content): + assert self.state != JingleStates.ended + stanza, jingle = self.__make_jingle('content-remove') + self.__append_content(jingle, content) + self.connection.connection.send(stanza) + #TODO: this will fail if content is not an RTP content + self.connection.dispatch('JINGLE_DISCONNECTED', + (self.peerjid, self.sid, content.media, 'removed')) + + def content_negociated(self, media): + self.connection.dispatch('JINGLE_CONNECTED', (self.peerjid, self.sid, + media)) + diff --git a/src/common/jingle_transport.py b/src/common/jingle_transport.py new file mode 100644 index 000000000..83db8c039 --- /dev/null +++ b/src/common/jingle_transport.py @@ -0,0 +1,139 @@ +## +## Copyright (C) 2006 Gajim Team +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 2 only. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +''' Handles Jingle Transports (currently only ICE-UDP). ''' + +import xmpp + +transports = {} + +def get_jingle_transport(node): + namespace = node.getNamespace() + if namespace in transports: + return transports[namespace]() + else: + return None + + +class TransportType(object): + ''' Possible types of a JingleTransport ''' + datagram = 1 + streaming = 2 + + +class JingleTransport(object): + ''' An abstraction of a transport in Jingle sessions. ''' + def __init__(self, type_): + self.type = type_ + self.candidates = [] + self.remote_candidates = [] + + def _iter_candidates(self): + for candidate in self.candidates: + yield self.make_candidate(candidate) + + def make_candidate(self, candidate): + ''' Build a candidate stanza for the given candidate. ''' + pass + + def make_transport(self, candidates=None): + ''' Build a transport stanza with the given candidates (or self.candidates + if candidates is None). ''' + if not candidates: + candidates = self._iter_candidates() + transport = xmpp.Node('transport', payload=candidates) + return transport + + +import farsight + +class JingleTransportICEUDP(JingleTransport): + def __init__(self): + JingleTransport.__init__(self, TransportType.datagram) + + def make_candidate(self, candidate): + types = {farsight.CANDIDATE_TYPE_HOST: 'host', + farsight.CANDIDATE_TYPE_SRFLX: 'srflx', + farsight.CANDIDATE_TYPE_PRFLX: 'prflx', + farsight.CANDIDATE_TYPE_RELAY: 'relay', + farsight.CANDIDATE_TYPE_MULTICAST: 'multicast'} + attrs = { + 'component': candidate.component_id, + 'foundation': '1', # hack + 'generation': '0', + 'ip': candidate.ip, + 'network': '0', + 'port': candidate.port, + 'priority': int(candidate.priority), # hack + } + if candidate.type in types: + attrs['type'] = types[candidate.type] + if candidate.proto == farsight.NETWORK_PROTOCOL_UDP: + attrs['protocol'] = 'udp' + else: + # we actually don't handle properly different tcp options in jingle + attrs['protocol'] = 'tcp' + return xmpp.Node('candidate', attrs=attrs) + + def make_transport(self, candidates=None): + transport = JingleTransport.make_transport(self, candidates) + transport.setNamespace(xmpp.NS_JINGLE_ICE_UDP) + if self.candidates and self.candidates[0].username and \ + self.candidates[0].password: + transport.setAttr('ufrag', self.candidates[0].username) + transport.setAttr('pwd', self.candidates[0].password) + return transport + + def transportInfoCB(self, transport): + candidates = [] + for candidate in transport.iterTags('candidate'): + cand = farsight.Candidate() + cand.component_id = int(candidate['component']) + cand.ip = str(candidate['ip']) + cand.port = int(candidate['port']) + cand.foundation = str(candidate['foundation']) + #cand.type = farsight.CANDIDATE_TYPE_LOCAL + cand.priority = int(candidate['priority']) + + if candidate['protocol'] == 'udp': + cand.proto = farsight.NETWORK_PROTOCOL_UDP + else: + # we actually don't handle properly different tcp options in jingle + cand.proto = farsight.NETWORK_PROTOCOL_TCP + + cand.username = str(transport['ufrag']) + cand.password = str(transport['pwd']) + + #FIXME: huh? + types = {'host': farsight.CANDIDATE_TYPE_HOST, + 'srflx': farsight.CANDIDATE_TYPE_SRFLX, + 'prflx': farsight.CANDIDATE_TYPE_PRFLX, + 'relay': farsight.CANDIDATE_TYPE_RELAY, + 'multicast': farsight.CANDIDATE_TYPE_MULTICAST} + if 'type' in candidate and candidate['type'] in types: + cand.type = types[candidate['type']] + else: + print 'Unknown type %s', candidate['type'] + candidates.append(cand) + #FIXME: connectivity should not be etablished yet + # Instead, it should be etablished after session-accept! + #FIXME: + #if len(candidates) > 0: + # if self.sent: + # self.p2pstream.set_remote_candidates(candidates) + # else: + self.remote_candidates.extend(candidates) + #self.p2pstream.set_remote_candidates(candidates) + #print self.media, self.creator, self.name, candidates + +transports[xmpp.NS_JINGLE_ICE_UDP] = JingleTransportICEUDP + From b1173a2e87c48bbfb519a2e27e0d9ac7b1a418fd Mon Sep 17 00:00:00 2001 From: Thibaut GIRKA Date: Sun, 15 Nov 2009 21:21:10 +0100 Subject: [PATCH 18/73] Fix a regression introduced by my last patch --- src/common/jingle_content.py | 9 ++++++++- src/common/jingle_rtp.py | 7 +++++++ src/common/jingle_transport.py | 20 ++++++++------------ 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/common/jingle_content.py b/src/common/jingle_content.py index 98b5dfa0a..2feb5d8de 100644 --- a/src/common/jingle_content.py +++ b/src/common/jingle_content.py @@ -70,6 +70,10 @@ class JingleContent(object): def is_ready(self): return (self.accepted and not self.sent) + def add_remote_candidates(self, candidates): + ''' Add a list of candidates to the list of remote candidates. ''' + pass + def stanzaCB(self, stanza, content, error, action): ''' Called when something related to our content was sent by peer. ''' if action in self.callbacks: @@ -78,7 +82,10 @@ class JingleContent(object): def __transportInfoCB(self, stanza, content, error, action): ''' Got a new transport candidate. ''' - self.transport.transportInfoCB(content.getTag('transport')) + candidates = self.transport.parse_transport_stanza( + content.getTag('transport')) + if candidates: + self.add_remote_candidates(candidates) def __content(self, payload=[]): ''' Build a XML content-wrapper for our data. ''' diff --git a/src/common/jingle_rtp.py b/src/common/jingle_rtp.py index 7fb9c2611..d67b0f465 100644 --- a/src/common/jingle_rtp.py +++ b/src/common/jingle_rtp.py @@ -83,6 +83,13 @@ class JingleRTPContent(JingleContent): return (JingleContent.is_ready(self) and self.candidates_ready and self.p2psession.get_property('codecs-ready')) + def add_remote_candidates(self, candidates): + JingleContent.add_remote_candidates(self, candidates) + #FIXME: connectivity should not be etablished yet + # Instead, it should be etablished after session-accept! + if self.sent: + self.p2pstream.set_remote_candidates(candidates) + def batch_dtmf(self, events): if self._dtmf_running: raise Exception #TODO: Proper exception diff --git a/src/common/jingle_transport.py b/src/common/jingle_transport.py index 83db8c039..487c66d70 100644 --- a/src/common/jingle_transport.py +++ b/src/common/jingle_transport.py @@ -46,13 +46,17 @@ class JingleTransport(object): pass def make_transport(self, candidates=None): - ''' Build a transport stanza with the given candidates (or self.candidates - if candidates is None). ''' + ''' Build a transport stanza with the given candidates (or + self.candidates if candidates is None). ''' if not candidates: candidates = self._iter_candidates() transport = xmpp.Node('transport', payload=candidates) return transport + def parse_transport_stanza(self, transport): + ''' Returns the list of transport candidates from a transport stanza. ''' + return [] + import farsight @@ -93,7 +97,7 @@ class JingleTransportICEUDP(JingleTransport): transport.setAttr('pwd', self.candidates[0].password) return transport - def transportInfoCB(self, transport): + def parse_transport_stanza(self, transport): candidates = [] for candidate in transport.iterTags('candidate'): cand = farsight.Candidate() @@ -124,16 +128,8 @@ class JingleTransportICEUDP(JingleTransport): else: print 'Unknown type %s', candidate['type'] candidates.append(cand) - #FIXME: connectivity should not be etablished yet - # Instead, it should be etablished after session-accept! - #FIXME: - #if len(candidates) > 0: - # if self.sent: - # self.p2pstream.set_remote_candidates(candidates) - # else: self.remote_candidates.extend(candidates) - #self.p2pstream.set_remote_candidates(candidates) - #print self.media, self.creator, self.name, candidates + return candidates transports[xmpp.NS_JINGLE_ICE_UDP] = JingleTransportICEUDP From 4c03c1ab85297f2c25aea8bc1521504a63c8bfe9 Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sun, 15 Nov 2009 22:41:17 +0100 Subject: [PATCH 19/73] Remove duplicated Icon determination logic used when drawing accounts. --- src/roster_window.py | 48 +++++++------------------------------------- 1 file changed, 7 insertions(+), 41 deletions(-) diff --git a/src/roster_window.py b/src/roster_window.py index 6d06388f8..e425c9cb3 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1022,55 +1022,21 @@ class RosterWindow: self.model[child_iter][C_NAME] = account_name - if gajim.config.get('show_mood_in_roster') \ - and 'mood' in gajim.connections[account].mood \ - and gajim.connections[account].mood['mood'].strip() in MOODS: - - self.model[child_iter][C_MOOD_PIXBUF] = gtkgui_helpers.load_mood_icon( - gajim.connections[account].mood['mood'].strip()).get_pixbuf() - - elif gajim.config.get('show_mood_in_roster') \ - and 'mood' in gajim.connections[account].mood: - self.model[child_iter][C_MOOD_PIXBUF] = \ - gtkgui_helpers.load_mood_icon('unknown'). \ - get_pixbuf() + pep = gajim.connections[account].pep + if gajim.config.get('show_mood_in_roster') and 'mood' in pep: + self.model[child_iter][C_MOOD_PIXBUF] = pep['mood'].asPixbufIcon() else: self.model[child_iter][C_MOOD_PIXBUF] = None - if gajim.config.get('show_activity_in_roster') \ - and 'activity' in gajim.connections[account].activity \ - and gajim.connections[account].activity['activity'].strip() \ - in ACTIVITIES: - if 'subactivity' in gajim.connections[account].activity \ - and gajim.connections[account].activity['subactivity'].strip() \ - in ACTIVITIES[gajim.connections[account].activity['activity'].strip()]: - self.model[child_iter][C_ACTIVITY_PIXBUF] = \ - gtkgui_helpers.load_activity_icon( - gajim.connections[account].activity['activity'].strip(), - gajim.connections[account].activity['subactivity'].strip()). \ - get_pixbuf() - else: - self.model[child_iter][C_ACTIVITY_PIXBUF] = \ - gtkgui_helpers.load_activity_icon( - gajim.connections[account].activity['activity'].strip()). \ - get_pixbuf() - elif gajim.config.get('show_activity_in_roster') \ - and 'activity' in gajim.connections[account].activity: - self.model[child_iter][C_ACTIVITY_PIXBUF] = \ - gtkgui_helpers.load_activity_icon('unknown'). \ - get_pixbuf() + if gajim.config.get('show_activity_in_roster') and 'activity' in pep: + self.model[child_iter][C_ACTIVITY_PIXBUF] = pep['activity'].asPixbufIcon() else: self.model[child_iter][C_ACTIVITY_PIXBUF] = None - if gajim.config.get('show_tunes_in_roster') \ - and ('artist' in gajim.connections[account].tune \ - or 'title' in gajim.connections[account].tune): - path = os.path.join(gajim.DATA_DIR, 'emoticons', 'static', 'music.png') - self.model[child_iter][C_TUNE_PIXBUF] = \ - gtk.gdk.pixbuf_new_from_file(path) + if gajim.config.get('show_tunes_in_roster') and 'tune' in pep: + self.model[child_iter][C_TUNE_PIXBUF] = pep['tune'].asPixbufIcon() else: self.model[child_iter][C_TUNE_PIXBUF] = None - return False def draw_group(self, group, account): From 338cb11dcce2218978f7e56bb45094c049431d57 Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sun, 15 Nov 2009 22:54:20 +0100 Subject: [PATCH 20/73] Unify update_mood, update_tune, update_activity by using a single update_pep(pep_type) method. --- src/chat_control.py | 52 +++++++++++++++----------------------------- src/common/pep.py | 5 ++--- src/gui_interface.py | 6 ++--- 3 files changed, 22 insertions(+), 41 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index b71033da5..d9607c4b5 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1279,14 +1279,12 @@ class ChatControl(ChatControlBase): self.video_state = self.JINGLE_STATE_NOT_AVAILABLE self.update_toolbar() - - self._mood_image = self.xml.get_widget('mood_image') - self._activity_image = self.xml.get_widget('activity_image') - self._tune_image = self.xml.get_widget('tune_image') - - self.update_mood() - self.update_activity() - self.update_tune() + + self._pep_images = {} + self._pep_images['mood'] = self.xml.get_widget('mood_image') + self._pep_images['activity'] = self.xml.get_widget('activity_image') + self._pep_images['tune'] = self.xml.get_widget('tune_image') + self.update_all_pep_types() # keep timeout id and window obj for possible big avatar # it is on enter-notify and leave-notify so no need to be @@ -1430,38 +1428,22 @@ class ChatControl(ChatControlBase): self._convert_to_gc_button.set_sensitive(True) else: self._convert_to_gc_button.set_sensitive(False) + + def update_all_pep_types(self): + for pep_type in ('tune', 'mood', 'activity'): + self.update_pep(pep_type) - def update_mood(self): + def update_pep(self, pep_type): if isinstance(self.contact, GC_Contact): return pep = self.contact.pep - if 'mood' in pep: - self._mood_image.set_from_pixbuf(pep['mood'].asPixbufIcon()) - self._mood_image.set_tooltip_markup(pep['mood'].asMarkupText()) - self._mood_image.show() + img = self._pep_images[pep_type] + if pep_type in pep: + img.set_from_pixbuf(pep[pep_type].asPixbufIcon()) + img.set_tooltip_markup(pep[pep_type].asMarkupText()) + img.show() else: - self._mood_image.hide() - - def update_activity(self): - if isinstance(self.contact, GC_Contact): - return - pep = self.contact.pep - if 'activity' in pep: - self._activity_image.set_from_pixbuf(pep['activity'].asPixbufIcon()) - self._activity_image.set_tooltip_markup(pep['activity'].asMarkupText()) - self._activity_image.show() - else: - self._activity_image.hide() - - def update_tune(self): - if isinstance(self.contact, GC_Contact): - return - pep = self.contact.pep - if 'tune' in pep: - self._tune_image.set_tooltip_markup(pep['tune'].asMarkupText()) - self._tune_image.show() - else: - self._tune_image.hide() + img.hide() def _update_jingle(self, jingle_type): if jingle_type not in ('audio', 'video'): diff --git a/src/common/pep.py b/src/common/pep.py index 13a774a1b..18a27364a 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -594,8 +594,7 @@ def delete_pep(jid, name): common.gajim.interface.roster.draw_mood(user, name) ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) if ctrl: - ctrl.update_activity() - ctrl.update_tune() - ctrl.update_mood() + ctrl.update_all_pep_types() + # vim: se ts=3: diff --git a/src/gui_interface.py b/src/gui_interface.py index 8aec6f993..ec093b03b 100644 --- a/src/gui_interface.py +++ b/src/gui_interface.py @@ -2001,15 +2001,15 @@ class Interface: if pep_type == 'mood': self.roster.draw_mood(jid, account) if ctrl: - ctrl.update_mood() + ctrl.update_pep(pep_type) elif pep_type == 'tune': self.roster.draw_tune(jid, account) if ctrl: - ctrl.update_tune() + ctrl.update_pep(pep_type) elif pep_type == 'activity': self.roster.draw_activity(jid, account) if ctrl: - ctrl.update_activity() + ctrl.update_pep(pep_type) elif pep_type == 'nickname': self.roster.draw_contact(jid, account) if ctrl: From 28161dc33c2d0f0da693d37baea6980013d6ee2e Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sun, 15 Nov 2009 22:59:43 +0100 Subject: [PATCH 21/73] Apply coding standards. --- src/common/pep.py | 33 ++++++++++++++------------------- src/roster_window.py | 3 +-- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/common/pep.py b/src/common/pep.py index 18a27364a..e0394ce61 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -475,11 +475,12 @@ class ConnectionPEP: self.dispatch('PEP_RECEIVED', (jid, pep.type)) items = event_tag.getTag('items') - if items is None: return + if items is None: + return for item in items.getTags('item'): entry = item.getTag('entry') - if entry is not None: + if 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),)) @@ -493,9 +494,9 @@ def user_send_mood(account, mood, message=''): if not common.gajim.connections[account].pep_supported: return item = xmpp.Node('mood', {'xmlns': xmpp.NS_MOOD}) - if mood != '': + if mood: item.addChild(mood) - if message != '': + if message: i = item.addChild('text') i.addData(message) @@ -506,11 +507,11 @@ def user_send_activity(account, activity, subactivity='', message=''): if not common.gajim.connections[account].pep_supported: return item = xmpp.Node('activity', {'xmlns': xmpp.NS_ACTIVITY}) - if activity != '': + if activity: i = item.addChild(activity) - if subactivity != '': + if subactivity: i.addChild(subactivity) - if message != '': + if message: i = item.addChild('text') i.addData(message) @@ -523,22 +524,22 @@ items=None): common.gajim.connections[account].pep_supported): return item = xmpp.Node('tune', {'xmlns': xmpp.NS_TUNE}) - if artist != '': + if artist: i = item.addChild('artist') i.addData(artist) - if title != '': + if title: i = item.addChild('title') i.addData(title) - if source != '': + if source: i = item.addChild('source') i.addData(source) - if track != 0: + if track: i = item.addChild('track') i.addData(track) - if length != 0: + if length: i = item.addChild('length') i.addData(length) - if items is not None: + if items: item.addChild(payload=items) common.gajim.connections[account].send_pb_publish('', xmpp.NS_TUNE, item, @@ -570,20 +571,14 @@ def delete_pep(jid, name): if jid == common.gajim.get_jid_from_account(name): acc = common.gajim.connections[name] - del acc.activity acc.activity = {} user_send_tune(name) - del acc.tune acc.tune = {} - del acc.mood acc.mood = {} for contact in common.gajim.contacts.get_contacts(name, user): - del contact.activity contact.activity = {} - del contact.tune contact.tune = {} - del contact.mood contact.mood = {} if jid == common.gajim.get_jid_from_account(name): diff --git a/src/roster_window.py b/src/roster_window.py index e425c9cb3..046684b57 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1934,8 +1934,7 @@ class RosterWindow: gajim.connections[account].send_custom_status(status, txt, to) else: if status in ('invisible', 'offline'): - pep.delete_pep(gajim.get_jid_from_account(account), \ - account) + pep.delete_pep(gajim.get_jid_from_account(account), account) was_invisible = gajim.connections[account].connected == \ gajim.SHOW_LIST.index('invisible') gajim.connections[account].change_status(status, txt, auto) From aa53988fd18d1f7f2b8d58bd5dd88c895c40331d Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sun, 15 Nov 2009 23:23:56 +0100 Subject: [PATCH 22/73] Similar to update_pep, unify towards draw_pep of the RosterWindow. --- src/chat_control.py | 4 +++- src/common/pep.py | 4 +--- src/gui_interface.py | 18 +++++------------- src/roster_window.py | 40 +++++++++++++++++++++++++--------------- 4 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index d9607c4b5..9c491b9e1 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1430,12 +1430,14 @@ class ChatControl(ChatControlBase): self._convert_to_gc_button.set_sensitive(False) def update_all_pep_types(self): - for pep_type in ('tune', 'mood', 'activity'): + for pep_type in self._pep_images: self.update_pep(pep_type) def update_pep(self, pep_type): if isinstance(self.contact, GC_Contact): return + if pep_type not in self._pep_images: + return pep = self.contact.pep img = self._pep_images[pep_type] if pep_type in pep: diff --git a/src/common/pep.py b/src/common/pep.py index e0394ce61..85e72d8ea 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -584,9 +584,7 @@ def delete_pep(jid, name): if jid == common.gajim.get_jid_from_account(name): common.gajim.interface.roster.draw_account(name) - common.gajim.interface.roster.draw_activity(user, name) - common.gajim.interface.roster.draw_tune(user, name) - common.gajim.interface.roster.draw_mood(user, name) + common.gajim.interface.roster.draw_all_pep_types(jid, name) ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) if ctrl: ctrl.update_all_pep_types() diff --git a/src/gui_interface.py b/src/gui_interface.py index ec093b03b..f33e769e3 100644 --- a/src/gui_interface.py +++ b/src/gui_interface.py @@ -1998,25 +1998,17 @@ class Interface: if jid == common.gajim.get_jid_from_account(account): self.roster.draw_account(account) - if pep_type == 'mood': - self.roster.draw_mood(jid, account) - if ctrl: - ctrl.update_pep(pep_type) - elif pep_type == 'tune': - self.roster.draw_tune(jid, account) - if ctrl: - ctrl.update_pep(pep_type) - elif pep_type == 'activity': - self.roster.draw_activity(jid, account) - if ctrl: - ctrl.update_pep(pep_type) - elif pep_type == 'nickname': + if pep_type == 'nickname': self.roster.draw_contact(jid, account) if ctrl: ctrl.update_ui() win = ctrl.parent_win win.redraw_tab(ctrl) win.show_title() + else: + self.roster.draw_pep(jid, account, pep_type) + if ctrl: + ctrl.update_pep(pep_type) def register_handler(self, event, handler): if event not in self.handlers: diff --git a/src/roster_window.py b/src/roster_window.py index 046684b57..2745a3365 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1242,19 +1242,27 @@ class RosterWindow: return False - def draw_mood(self, jid, account): - if gajim.config.get('show_mood_in_roster'): - self._draw_pep(jid, account, 'mood', C_MOOD_PIXBUF) - - def draw_activity(self, jid, account): - if gajim.config.get('show_activity_in_roster'): - self._draw_pep(jid, account, 'activity', C_ACTIVITY_PIXBUF) - - def draw_tune(self, jid, account): - if gajim.config.get('show_tunes_in_roster'): - self._draw_pep(jid, account, 'tune', C_TUNE_PIXBUF) + def _is_pep_shown_in_roster(self, pep_type): + if pep_type == 'mood': + return gajim.config.get('show_mood_in_roster') + elif pep_type == 'activity': + return gajim.config.get('show_activity_in_roster') + elif pep_type == 'tune': + return gajim.config.get('show_tunes_in_roster') + else: + return False + + def draw_all_pep_types(self, jid, account): + for pep_type in self._pep_type_to_model_column: + self.draw_pep(jid, account, pep_type) - def _draw_pep(self, jid, account, pep_type, model_column): + def draw_pep(self, jid, account, pep_type): + if pep_type not in self._pep_type_to_model_column: + return + if not self._is_pep_shown_in_roster(pep_type): + return + + model_column = self._pep_type_to_model_column[pep_type] iters = self._get_contact_iter(jid, account, model=self.model) if not iters: return @@ -1285,9 +1293,7 @@ class RosterWindow: def draw_completely(self, jid, account): self.draw_contact(jid, account) - self.draw_mood(jid, account) - self.draw_activity(jid, account) - self.draw_tune(jid, account) + self.draw_all_pep_types(jid, account) self.draw_avatar(jid, account) def adjust_and_draw_contact_context(self, jid, account): @@ -5753,6 +5759,10 @@ class RosterWindow: col.add_attribute(render_pixbuf, 'pixbuf', C_TUNE_PIXBUF) col.set_cell_data_func(render_pixbuf, self._fill_pep_pixbuf_renderer, C_TUNE_PIXBUF) + + self._pep_type_to_model_column = {'mood': C_MOOD_PIXBUF, + 'activity': C_ACTIVITY_PIXBUF, + 'tune': C_ACTIVITY_PIXBUF} if gajim.config.get('avatar_position_in_roster') == 'right': add_avatar_renderer() From a3ea00f4ea349b51b172a9f187842e3a3b6d4f4c Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sun, 15 Nov 2009 23:52:43 +0100 Subject: [PATCH 23/73] Remove different dicts for tune, activity and mood and from now on only use the common 'pep' dict. The pep dict contacts the different UserPEP classes. --- src/common/connection.py | 3 --- src/common/contacts.py | 21 +++++++++--------- src/common/pep.py | 25 ++++++---------------- src/common/zeroconf/connection_zeroconf.py | 4 +--- src/roster_window.py | 9 ++++---- test/lib/gajim_mocks.py | 4 +--- 6 files changed, 22 insertions(+), 44 deletions(-) diff --git a/src/common/connection.py b/src/common/connection.py index 90df73d08..e2b6d2166 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -168,9 +168,6 @@ class Connection(ConnectionHandlers): self.pubsub_supported = False self.pubsub_publish_options_supported = False self.pep_supported = False - self.mood = {} - self.tune = {} - self.activity = {} self.pep = {} # Do we continue connection when we get roster (send presence,get vcard..) self.continue_connect_info = None diff --git a/src/common/contacts.py b/src/common/contacts.py index b8b039288..d012558bf 100644 --- a/src/common/contacts.py +++ b/src/common/contacts.py @@ -94,7 +94,7 @@ class Contact(CommonContact): def __init__(self, jid, account, name='', groups=[], show='', status='', sub='', ask='', resource='', priority=0, keyID='', client_caps=None, our_chatstate=None, chatstate=None, last_status_time=None, msg_id = None, - composing_xep=None, mood={}, tune={}, activity={}): + composing_xep=None): CommonContact.__init__(self, jid, account, resource, show, status, name, our_chatstate, composing_xep, chatstate, client_caps=client_caps) @@ -111,9 +111,6 @@ class Contact(CommonContact): self.last_status_time = last_status_time self.pep = {} - self.mood = mood.copy() - self.tune = tune.copy() - self.activity = activity.copy() def get_full_jid(self): if self.resource: @@ -229,23 +226,25 @@ class Contacts: def create_contact(self, jid, account, name='', groups=[], show='', status='', sub='', ask='', resource='', priority=0, keyID='', client_caps=None, our_chatstate=None, chatstate=None, last_status_time=None, - composing_xep=None, mood={}, tune={}, activity={}): + composing_xep=None): account = self._accounts.get(account, account) # Use Account object if available return Contact(jid=jid, account=account, name=name, groups=groups, show=show, status=status, sub=sub, ask=ask, resource=resource, priority=priority, keyID=keyID, client_caps=client_caps, our_chatstate=our_chatstate, chatstate=chatstate, last_status_time=last_status_time, - composing_xep=composing_xep, mood=mood, tune=tune, activity=activity) + composing_xep=composing_xep) - def create_self_contact(self, jid, account, resource, show, status, priority, keyID=''): + def create_self_contact(self, jid, account, resource, show, status, priority, + name='', keyID=''): conn = common.gajim.connections[account] - nick = common.gajim.nicks[account] + nick = name or common.gajim.nicks[account] account = self._accounts.get(account, account) # Use Account object if available - return self.create_contact(jid=jid, account=account, + self_contact = self.create_contact(jid=jid, account=account, name=nick, groups=['self_contact'], show=show, status=status, sub='both', ask='none', priority=priority, keyID=keyID, - resource=resource, mood=conn.mood, tune=conn.tune, - activity=conn.activity) + resource=resource) + self_contact.pep = conn.pep + return self_contact def create_not_in_roster_contact(self, jid, account, resource='', name='', keyID=''): account = self._accounts.get(account, account) # Use Account object if available diff --git a/src/common/pep.py b/src/common/pep.py index 85e72d8ea..fa4a31a3a 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -237,24 +237,16 @@ class AbstractPEP(object): '''To be implemented by subclasses''' raise NotImplementedError - def _update_contacts(self, jid, account): - dict = {} if self._retracted else self._pep_specific_data - - for contact in common.gajim.contacts.get_contacts(account, jid): - setattr(contact, self.type, dict) - + def _update_contacts(self, jid, account): + for contact in common.gajim.contacts.get_contacts(account, jid): if self._retracted: if self.type in contact.pep: del contact.pep[self.type] else: contact.pep[self.type] = self - def _update_account(self, account): - dict = {} if self._retracted else self._pep_specific_data - - acc = common.gajim.connections[account] - setattr(acc, self.type, dict) - + def _update_account(self, account): + acc = common.gajim.connections[account] if self._retracted: if self.type in acc.pep: del acc.pep[self.type] @@ -571,15 +563,10 @@ def delete_pep(jid, name): if jid == common.gajim.get_jid_from_account(name): acc = common.gajim.connections[name] - acc.activity = {} - user_send_tune(name) - acc.tune = {} - acc.mood = {} + acc.pep = {} for contact in common.gajim.contacts.get_contacts(name, user): - contact.activity = {} - contact.tune = {} - contact.mood = {} + contact.pep = {} if jid == common.gajim.get_jid_from_account(name): common.gajim.interface.roster.draw_account(name) diff --git a/src/common/zeroconf/connection_zeroconf.py b/src/common/zeroconf/connection_zeroconf.py index c5a93b4b8..26725f5f0 100644 --- a/src/common/zeroconf/connection_zeroconf.py +++ b/src/common/zeroconf/connection_zeroconf.py @@ -88,9 +88,7 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): self.no_log_for = False self.pep_supported = False - self.mood = {} - self.tune = {} - self.activity = {} + self.pep = {} # Do we continue connection when we get roster (send presence,get vcard...) self.continue_connect_info = None if gajim.HAVE_GPG: diff --git a/src/roster_window.py b/src/roster_window.py index 2745a3365..17697b3dd 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -2432,11 +2432,10 @@ class RosterWindow: account_name = account if gajim.account_is_connected(account): account_name += ' (%s/%s)' % (repr(nbr_on), repr(nbr_total)) - contact = gajim.contacts.create_contact(jid=jid, account=account, name=account_name, - show=connection.get_status(), sub='', status=connection.status, - resource=connection.server_resource, - priority=connection.priority, mood=connection.mood, - tune=connection.tune, activity=connection.activity) + contact = gajim.contacts.create_self_contact(jid=jid, account=account, + name=account_name, show=connection.get_status(), + status=connection.status, resource=connection.server_resource, + priority=connection.priority) if gajim.connections[account].gpg: contact.keyID = gajim.config.get_per('accounts', connection.name, 'keyid') diff --git a/test/lib/gajim_mocks.py b/test/lib/gajim_mocks.py index d45e488e6..9607a77c9 100644 --- a/test/lib/gajim_mocks.py +++ b/test/lib/gajim_mocks.py @@ -14,9 +14,7 @@ class MockConnection(Mock, ConnectionHandlersBase): self.name = account self.connected = 2 - self.mood = {} - self.activity = {} - self.tune = {} + self.pep = {} self.blocked_contacts = {} self.blocked_groups = {} self.sessions = {} From 1c28dbfae9518ed4c136d4c1406737220729fdcd Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Mon, 16 Nov 2009 16:42:40 +0100 Subject: [PATCH 24/73] properly decode string in history manager. Fixes #5430 --- src/history_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/history_manager.py b/src/history_manager.py index 9ab638364..5cdac0369 100644 --- a/src/history_manager.py +++ b/src/history_manager.py @@ -610,8 +610,8 @@ class HistoryManager: liststore, list_of_paths)) def on_search_db_button_clicked(self, widget): - text = self.search_entry.get_text() - if text == '': + text = self.search_entry.get_text().decode('utf-8') + if not text: return self.welcome_vbox.hide() From 10428555aa87d512def8274bc06abb1de87f2ce7 Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Mon, 16 Nov 2009 19:31:17 +0100 Subject: [PATCH 25/73] Various pep-related cleanups. Most important change is that pep send/retract functions no reside on the ConnectionPEP object. --- src/common/pep.py | 208 +++++++++++++++++++----------------------- src/gui_interface.py | 22 ++--- src/profile_window.py | 3 +- src/roster_window.py | 74 +++++++-------- 4 files changed, 139 insertions(+), 168 deletions(-) diff --git a/src/common/pep.py b/src/common/pep.py index fa4a31a3a..21861c5e2 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -400,17 +400,17 @@ class UserActivityPEP(AbstractPEP): text = pep['text'] if 'text' in pep else None if activity in ACTIVITIES: - # Translate standard activities - if subactivity in ACTIVITIES[activity]: - subactivity = ACTIVITIES[activity][subactivity] - activity = ACTIVITIES[activity]['category'] + # Translate standard activities + if subactivity in ACTIVITIES[activity]: + subactivity = ACTIVITIES[activity][subactivity] + activity = ACTIVITIES[activity]['category'] markuptext = '' + gobject.markup_escape_text(activity) if subactivity: markuptext += ': ' + gobject.markup_escape_text(subactivity) markuptext += '' if text: - markuptext += ' (%s)' + gobject.markup_escape_text(text) + markuptext += ' (%s)' % gobject.markup_escape_text(text) return markuptext @@ -446,18 +446,17 @@ class UserNicknamePEP(AbstractPEP): common.gajim.nicks[account] = self._pep_specific_data -SUPPORTED_PERSONAL_USER_EVENTS = [UserMoodPEP, UserTunePEP, UserActivityPEP, UserNicknamePEP] +SUPPORTED_PERSONAL_USER_EVENTS = [UserMoodPEP, UserTunePEP, UserActivityPEP, + UserNicknamePEP] -class ConnectionPEP: - +class ConnectionPEP(object): + def _pubsubEventCB(self, xmpp_dispatcher, msg): ''' Called when we receive with pubsub event. ''' if msg.getTag('error'): log.warning('PubsubEventCB received error stanza') return - - # TODO: Logging? (actually services where logging would be useful, should - # TODO: allow to access archives remotely...) + jid = helpers.get_full_jid_from_iq(msg) event_tag = msg.getTag('event') @@ -467,114 +466,97 @@ class ConnectionPEP: self.dispatch('PEP_RECEIVED', (jid, pep.type)) items = event_tag.getTag('items') - if items is None: - return - - for item in items.getTags('item'): - entry = item.getTag('entry') - if 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 + if items: + for item in items.getTags('item'): + entry = item.getTag('entry') + if 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),)) raise common.xmpp.NodeProcessed + + def send_activity(self, activity, subactivity=None, message=None): + if not self.pep_supported: + return + item = xmpp.Node('activity', {'xmlns': xmpp.NS_ACTIVITY}) + if activity: + i = item.addChild(activity) + if subactivity: + i.addChild(subactivity) + if message: + i = item.addChild('text') + i.addData(message) + self.send_pb_publish('', xmpp.NS_ACTIVITY, item, '0') + + def retract_activity(self): + if not self.pep_supported: + return + # not all server support retract, so send empty pep first + self.send_activity(None) + self.send_pb_retract('', xmpp.NS_ACTIVITY, '0') + def send_mood(self, mood, message=None): + if not self.pep_supported: + return + item = xmpp.Node('mood', {'xmlns': xmpp.NS_MOOD}) + if mood: + item.addChild(mood) + if message: + i = item.addChild('text') + i.addData(message) + self.send_pb_publish('', xmpp.NS_MOOD, item, '0') + + def retract_mood(self): + if not self.pep_supported: + return + self.send_mood(None) + self.send_pb_retract('', xmpp.NS_MOOD, '0') + + def send_tune(self, artist='', title='', source='', track=0, length=0, + items=None): + if not self.pep_supported: + return + item = xmpp.Node('tune', {'xmlns': xmpp.NS_TUNE}) + if artist: + i = item.addChild('artist') + i.addData(artist) + if title: + i = item.addChild('title') + i.addData(title) + if source: + i = item.addChild('source') + i.addData(source) + if track: + i = item.addChild('track') + i.addData(track) + if length: + i = item.addChild('length') + i.addData(length) + if items: + item.addChild(payload=items) + self.send_pb_publish('', xmpp.NS_TUNE, item, '0') -def user_send_mood(account, mood, message=''): - if not common.gajim.connections[account].pep_supported: - return - item = xmpp.Node('mood', {'xmlns': xmpp.NS_MOOD}) - if mood: - item.addChild(mood) - if message: - i = item.addChild('text') - i.addData(message) + def retract_tune(self): + if not self.pep_supported: + return + # not all server support retract, so send empty pep first + self.send_tune(None) + self.send_pb_retract('', xmpp.NS_TUNE, '0') - common.gajim.connections[account].send_pb_publish('', xmpp.NS_MOOD, item, - '0') + def send_nickname(self, nick): + if not self.pep_supported: + return + item = xmpp.Node('nick', {'xmlns': xmpp.NS_NICK}) + item.addData(nick) + self.send_pb_publish('', xmpp.NS_NICK, item, '0') -def user_send_activity(account, activity, subactivity='', message=''): - if not common.gajim.connections[account].pep_supported: - return - item = xmpp.Node('activity', {'xmlns': xmpp.NS_ACTIVITY}) - if activity: - i = item.addChild(activity) - if subactivity: - i.addChild(subactivity) - if message: - i = item.addChild('text') - i.addData(message) - - common.gajim.connections[account].send_pb_publish('', xmpp.NS_ACTIVITY, item, - '0') - -def user_send_tune(account, artist='', title='', source='', track=0, length=0, -items=None): - if not (common.gajim.config.get_per('accounts', account, 'publish_tune') and\ - common.gajim.connections[account].pep_supported): - return - item = xmpp.Node('tune', {'xmlns': xmpp.NS_TUNE}) - if artist: - i = item.addChild('artist') - i.addData(artist) - if title: - i = item.addChild('title') - i.addData(title) - if source: - i = item.addChild('source') - i.addData(source) - if track: - i = item.addChild('track') - i.addData(track) - if length: - i = item.addChild('length') - i.addData(length) - if items: - item.addChild(payload=items) - - common.gajim.connections[account].send_pb_publish('', xmpp.NS_TUNE, item, - '0') - -def user_send_nickname(account, nick): - if not common.gajim.connections[account].pep_supported: - return - item = xmpp.Node('nick', {'xmlns': xmpp.NS_NICK}) - item.addData(nick) - - common.gajim.connections[account].send_pb_publish('', xmpp.NS_NICK, item, - '0') - -def user_retract_mood(account): - common.gajim.connections[account].send_pb_retract('', xmpp.NS_MOOD, '0') - -def user_retract_activity(account): - common.gajim.connections[account].send_pb_retract('', xmpp.NS_ACTIVITY, '0') - -def user_retract_tune(account): - common.gajim.connections[account].send_pb_retract('', xmpp.NS_TUNE, '0') - -def user_retract_nickname(account): - common.gajim.connections[account].send_pb_retract('', xmpp.NS_NICK, '0') - -def delete_pep(jid, name): - user = common.gajim.get_room_and_nick_from_fjid(jid)[0] - - if jid == common.gajim.get_jid_from_account(name): - acc = common.gajim.connections[name] - acc.pep = {} - - for contact in common.gajim.contacts.get_contacts(name, user): - contact.pep = {} - - if jid == common.gajim.get_jid_from_account(name): - common.gajim.interface.roster.draw_account(name) - - common.gajim.interface.roster.draw_all_pep_types(jid, name) - ctrl = common.gajim.interface.msg_win_mgr.get_control(user, name) - if ctrl: - ctrl.update_all_pep_types() + def retract_nickname(self): + if not self.pep_supported: + return + # not all server support retract, so send empty pep first + self.send_tune(None) + self.send_pb_retract('', xmpp.NS_NICK, '0') # vim: se ts=3: diff --git a/src/gui_interface.py b/src/gui_interface.py index f33e769e3..98c1f3298 100644 --- a/src/gui_interface.py +++ b/src/gui_interface.py @@ -2796,33 +2796,27 @@ class Interface: listener.disconnect(self.music_track_changed_signal) self.music_track_changed_signal = None - def music_track_changed(self, unused_listener, music_track_info, account=''): - if account == '': + def music_track_changed(self, unused_listener, music_track_info, account=None): + if not account: accounts = gajim.connections.keys() else: accounts = [account] - if music_track_info is None: - artist = '' - title = '' - source = '' - elif hasattr(music_track_info, 'paused') and music_track_info.paused == 0: - artist = '' - title = '' - source = '' + + is_paused = hasattr(music_track_info, 'paused') and music_track_info.paused == 0 + if not music_track_info or is_paused: + artist = title = source = '' else: artist = music_track_info.artist title = music_track_info.title source = music_track_info.album for acct in accounts: - if acct not in gajim.connections: - continue if not gajim.account_is_connected(acct): continue - if not gajim.connections[acct].pep_supported: + if not gajim.config.get_per('accounts', acct, 'publish_tune'): continue if gajim.connections[acct].music_track_info == music_track_info: continue - pep.user_send_tune(acct, artist, title, source) + gajim.connections[acct].send_tune(artist, title, source) gajim.connections[acct].music_track_info = music_track_info def get_bg_fg_colors(self): diff --git a/src/profile_window.py b/src/profile_window.py index a417ede08..f3fe1faa4 100644 --- a/src/profile_window.py +++ b/src/profile_window.py @@ -322,8 +322,7 @@ class ProfileWindow: nick = '' if 'NICKNAME' in vcard_: nick = vcard_['NICKNAME'] - from common import pep - pep.user_send_nickname(self.account, nick) + gajim.connections[self.account].send_nickname(self.account, nick) if nick == '': nick = gajim.config.get_per('accounts', self.account, 'name') gajim.nicks[self.account] = nick diff --git a/src/roster_window.py b/src/roster_window.py index 17697b3dd..6cef52f35 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1896,36 +1896,36 @@ class RosterWindow: self.send_status_continue(account, status, txt, auto, to) - def send_pep(self, account, pep_dict=None): - '''Sends pep information (activity, mood)''' - if not pep_dict: - return - # activity - if 'activity' in pep_dict and pep_dict['activity'] in pep.ACTIVITIES: + def send_pep(self, account, pep_dict): + connection = gajim.connections[account] + + if 'activity' in pep_dict: activity = pep_dict['activity'] - if 'subactivity' in pep_dict and \ - pep_dict['subactivity'] in pep.ACTIVITIES[activity]: - subactivity = pep_dict['subactivity'] - else: - subactivity = 'other' - if 'activity_text' in pep_dict: - activity_text = pep_dict['activity_text'] - else: - activity_text = '' - pep.user_send_activity(account, activity, subactivity, activity_text) + subactivity = pep_dict.get('subactivity', None) + activity_text = pep_dict.get('activity_text', None) + connection.send_activity(activity, subactivity, activity_text) else: - pep.user_send_activity(account, '') + connection.retract_activity() - # mood - if 'mood' in pep_dict and pep_dict['mood'] in pep.MOODS: + if 'mood' in pep_dict: mood = pep_dict['mood'] - if 'mood_text' in pep_dict: - mood_text = pep_dict['mood_text'] - else: - mood_text = '' - pep.user_send_mood(account, mood, mood_text) + mood_text = pep_dict.get('mood_text', None) + connection.send_mood(mood, mood_text) else: - pep.user_send_mood(account, '') + connection.retract_mood() + + def delete_pep(self, jid, account): + if jid == gajim.get_jid_from_account(account): + gajim.connections[account].pep = {} + self.draw_account(account) + + for contact in gajim.contacts.get_contacts(account, jid): + contact.pep = {} + + self.draw_all_pep_types(jid, account) + ctrl = gajim.interface.msg_win_mgr.get_control(jid, account) + if ctrl: + ctrl.update_all_pep_types() def send_status_continue(self, account, status, txt, auto, to): if gajim.account_is_connected(account) and not to: @@ -1940,7 +1940,7 @@ class RosterWindow: gajim.connections[account].send_custom_status(status, txt, to) else: if status in ('invisible', 'offline'): - pep.delete_pep(gajim.get_jid_from_account(account), account) + self.delete_pep(gajim.get_jid_from_account(account), account) was_invisible = gajim.connections[account].connected == \ gajim.SHOW_LIST.index('invisible') gajim.connections[account].change_status(status, txt, auto) @@ -2018,7 +2018,7 @@ class RosterWindow: contact_instances) if not keep_pep and contact.jid != gajim.get_jid_from_account(account) \ and not contact.is_groupchat(): - pep.delete_pep(contact.jid, account) + self.delete_pep(contact.jid, account) # Redraw everything and select the sender self.adjust_and_draw_contact_context(contact.jid, account) @@ -3318,23 +3318,19 @@ class RosterWindow: gajim.interface.instances['preferences'] = config.PreferencesWindow() def on_publish_tune_toggled(self, widget, account): - act = widget.get_active() - gajim.config.set_per('accounts', account, 'publish_tune', act) - if act: + active = widget.get_active() + gajim.config.set_per('accounts', account, 'publish_tune', active) + if active: gajim.interface.enable_music_listener() else: - # disable it only if no other account use it - for acct in gajim.connections: - if gajim.config.get_per('accounts', acct, 'publish_tune'): + gajim.connections[account].retract_tune() + # disable music listener only if no other account uses it + for acc in gajim.connections: + if gajim.config.get_per('accounts', acc, 'publish_tune'): break else: gajim.interface.disable_music_listener() - if gajim.connections[account].pep_supported: - # As many implementations don't support retracting items, we send a - # "Stopped" event first - pep.user_send_tune(account, '') - pep.user_retract_tune(account) helpers.update_optional_features(account) def on_pep_services_menuitem_activate(self, widget, account): @@ -5761,7 +5757,7 @@ class RosterWindow: self._pep_type_to_model_column = {'mood': C_MOOD_PIXBUF, 'activity': C_ACTIVITY_PIXBUF, - 'tune': C_ACTIVITY_PIXBUF} + 'tune': C_TUNE_PIXBUF} if gajim.config.get('avatar_position_in_roster') == 'right': add_avatar_renderer() From ff551cd75d54ae71dea842916a12223f7fabb29f Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Mon, 16 Nov 2009 20:56:51 +0100 Subject: [PATCH 26/73] make some strings translatable in RIE dialog --- src/dialogs.py | 72 ++++++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/src/dialogs.py b/src/dialogs.py index 1e4e6266b..4d197a896 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -2843,6 +2843,9 @@ class XMLConsoleWindow: # it's expanded!! self.input_textview.grab_focus() +#Action that can be done with an incoming list of contacts +TRANSLATED_ACTION = {'add': _('add'), 'modify': _('modify'), + 'remove': _('remove')} class RosterItemExchangeWindow: ''' Windows used when someone send you a exchange contact suggestion ''' def __init__(self, account, action, exchange_list, jid_from, @@ -2866,12 +2869,13 @@ class RosterItemExchangeWindow: # Set labels # self.action can be 'add', 'modify' or 'remove' - self.type_label.set_label(\ - _('%s would like you to %s some contacts in your ' - 'roster.') % (jid_from, _(self.action))) + self.type_label.set_label( + _('%(jid)s would like you to %(action)s some contacts ' + 'in your roster.') % {'jid': jid_from, + 'action': TRANSLATED_ACTION[self.action]}) if message_body: - buffer = self.body_textview.get_buffer() - buffer.set_text(self.message_body) + buffer_ = self.body_textview.get_buffer() + buffer_.set_text(self.message_body) else: self.body_scrolledwindow.hide() # Treeview @@ -2924,8 +2928,8 @@ class RosterItemExchangeWindow: groups = groups + group + ', ' if not is_in_roster: show_dialog = True - iter = model.append() - model.set(iter, 0, True, 1, jid, 2, name, 3, groups) + iter_ = model.append() + model.set(iter_, 0, True, 1, jid, 2, name, 3, groups) # Change label for accept_button to action name instead of 'OK'. self.accept_button_label.set_label(_('Add')) @@ -2955,8 +2959,8 @@ class RosterItemExchangeWindow: groups = groups + group + ', ' if not is_right and is_in_roster: show_dialog = True - iter = model.append() - model.set(iter, 0, True, 1, jid, 2, name, 3, groups) + iter_ = model.append() + model.set(iter_, 0, True, 1, jid, 2, name, 3, groups) # Change label for accept_button to action name instead of 'OK'. self.accept_button_label.set_label(_('Modify')) @@ -2979,8 +2983,8 @@ class RosterItemExchangeWindow: groups = groups + group + ', ' if is_in_roster: show_dialog = True - iter = model.append() - model.set(iter, 0, True, 1, jid, 2, name, 3, groups) + iter_ = model.append() + model.set(iter_, 0, True, 1, jid, 2, name, 3, groups) # Change label for accept_button to action name instead of 'OK'. self.accept_button_label.set_label(_('Delete')) @@ -2991,49 +2995,49 @@ class RosterItemExchangeWindow: def toggled_callback(self, cell, path): model = self.items_list_treeview.get_model() - iter = model.get_iter(path) - model[iter][0] = not cell.get_active() + iter_ = model.get_iter(path) + model[iter_][0] = not cell.get_active() def on_accept_button_clicked(self, widget): model = self.items_list_treeview.get_model() - iter = model.get_iter_root() + iter_ = model.get_iter_root() if self.action == 'add': a = 0 - while iter: - if model[iter][0]: + while iter_: + if model[iter_][0]: a+=1 # it is selected - #remote_jid = model[iter][1].decode('utf-8') + #remote_jid = model[iter_][1].decode('utf-8') message = _('%s suggested me to add you in my roster.' % self.jid_from) # keep same groups and same nickname - groups = model[iter][3].split(', ') + groups = model[iter_][3].split(', ') if groups == ['']: groups = [] - jid = model[iter][1].decode('utf-8') + jid = model[iter_][1].decode('utf-8') if gajim.jid_is_transport(self.jid_from): gajim.connections[self.account].automatically_added.append( jid) gajim.interface.roster.req_sub(self, jid, message, - self.account, groups=groups, nickname=model[iter][2], + self.account, groups=groups, nickname=model[iter_][2], auto_auth=True) - iter = model.iter_next(iter) - InformationDialog('Added %s contacts' % str(a)) + iter_ = model.iter_next(iter_) + InformationDialog(_('Added %s contacts') % str(a)) elif self.action == 'modify': a = 0 - while iter: - if model[iter][0]: + while iter_: + if model[iter_][0]: a+=1 # it is selected - jid = model[iter][1].decode('utf-8') + jid = model[iter_][1].decode('utf-8') # keep same groups and same nickname - groups = model[iter][3].split(', ') + groups = model[iter_][3].split(', ') if groups == ['']: groups = [] for u in gajim.contacts.get_contact(self.account, jid): - u.name = model[iter][2] + u.name = model[iter_][2] gajim.connections[self.account].update_contact(jid, - model[iter][2], groups) + model[iter_][2], groups) self.draw_contact(jid, account) # Update opened chat ctrl = gajim.interface.msg_win_mgr.get_control(jid, self.account) @@ -3043,19 +3047,19 @@ class RosterItemExchangeWindow: self.account) win.redraw_tab(ctrl) win.show_title() - iter = model.iter_next(iter) + iter_ = model.iter_next(iter_) elif self.action == 'delete': a = 0 - while iter: - if model[iter][0]: + while iter_: + if model[iter_][0]: a+=1 # it is selected - jid = model[iter][1].decode('utf-8') + jid = model[iter_][1].decode('utf-8') gajim.connections[self.account].unsubscribe(jid) gajim.interface.roster.remove_contact(jid, self.account) gajim.contacts.remove_jid(self.account, jid) - iter = model.iter_next(iter) - InformationDialog('Removed %s contacts' % str(a)) + iter_ = model.iter_next(iter_) + InformationDialog(_('Removed %s contacts') % str(a)) self.window.destroy() def on_cancel_button_clicked(self, widget): From d6e6a5d62d62bc4d3775d60a2525799db5c4e5df Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Tue, 17 Nov 2009 23:17:08 +0100 Subject: [PATCH 27/73] Do not try to send (and fail) sending PEP via Zeroconf. Fixes #5432. --- src/common/pep.py | 2 +- src/gui_interface.py | 2 ++ src/roster_window.py | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/common/pep.py b/src/common/pep.py index 21861c5e2..742325b4c 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -555,7 +555,7 @@ class ConnectionPEP(object): if not self.pep_supported: return # not all server support retract, so send empty pep first - self.send_tune(None) + self.send_nickname(None) self.send_pb_retract('', xmpp.NS_NICK, '0') diff --git a/src/gui_interface.py b/src/gui_interface.py index 98c1f3298..9cb53d646 100644 --- a/src/gui_interface.py +++ b/src/gui_interface.py @@ -2812,6 +2812,8 @@ class Interface: for acct in accounts: if not gajim.account_is_connected(acct): continue + if gajim.connections[acct].is_zeroconf: + continue if not gajim.config.get_per('accounts', acct, 'publish_tune'): continue if gajim.connections[acct].music_track_info == music_track_info: diff --git a/src/roster_window.py b/src/roster_window.py index 6cef52f35..25780f85c 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1898,6 +1898,8 @@ class RosterWindow: def send_pep(self, account, pep_dict): connection = gajim.connections[account] + if connection.is_zeroconf: + return if 'activity' in pep_dict: activity = pep_dict['activity'] @@ -1915,6 +1917,8 @@ class RosterWindow: connection.retract_mood() def delete_pep(self, jid, account): + if gajim.connections[account].is_zeroconf: + return if jid == gajim.get_jid_from_account(account): gajim.connections[account].pep = {} self.draw_account(account) From 960e402cf06bc9310b81e22ef544848f399cdd5c Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 18 Nov 2009 11:04:05 +0100 Subject: [PATCH 28/73] typo in a comment --- src/common/xmpp/client_nb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/xmpp/client_nb.py b/src/common/xmpp/client_nb.py index 489885f76..7d82ae4e3 100644 --- a/src/common/xmpp/client_nb.py +++ b/src/common/xmpp/client_nb.py @@ -112,7 +112,7 @@ class NonBlockingClient: log.debug('calling on_proxy_failure cb') self.on_proxy_failure(reason=message) else: - log.debug('ccalling on_connect_failure cb') + log.debug('calling on_connect_failure cb') self.on_connect_failure() else: # we are connected to XMPP server From 88f3104c4ecadd3e3945d3bd7e693a4e112c2162 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 18 Nov 2009 11:06:09 +0100 Subject: [PATCH 29/73] refactor normal and zeroconf Connection objects with a CommonConnection class --- src/common/connection.py | 958 ++++++++++-------- src/common/xmpp/transports_nb.py | 1 + src/common/zeroconf/client_zeroconf.py | 31 +- .../zeroconf/connection_handlers_zeroconf.py | 4 +- src/common/zeroconf/connection_zeroconf.py | 472 +++------ 5 files changed, 691 insertions(+), 775 deletions(-) diff --git a/src/common/connection.py b/src/common/connection.py index e2b6d2166..3cd3acd43 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -98,17 +98,491 @@ ssl_error = { 32: _("Key usage does not include certificate signing"), 50: _("Application verification failure") } -class Connection(ConnectionHandlers): - '''Connection class''' + +class CommonConnection: + ''' + Common connection class, can be derivated for normal connection or zeroconf + connection + ''' def __init__(self, name): - ConnectionHandlers.__init__(self) self.name = name # self.connected: # 0=>offline, # 1=>connection in progress, - # 2=>authorised + # 2=>online + # 3=>free for chat + # ... self.connected = 0 self.connection = None # xmpppy ClientCommon instance + self.on_purpose = False + self.is_zeroconf = False + self.password = '' + self.server_resource = self._compute_resource() + self.gpg = None + self.USE_GPG = False + if gajim.HAVE_GPG: + self.USE_GPG = True + self.gpg = GnuPG.GnuPG(gajim.config.get('use_gpg_agent')) + self.status = '' + self.old_show = '' + self.priority = gajim.get_priority(name, 'offline') + self.time_to_reconnect = None + self.bookmarks = [] + + self.blocked_list = [] + self.blocked_contacts = [] + self.blocked_groups = [] + self.blocked_all = False + + self.pep_supported = False + self.pep = {} + # Do we continue connection when we get roster (send presence,get vcard..) + self.continue_connect_info = None + + # To know the groupchat jid associated with a sranza ID. Useful to + # request vcard or os info... to a real JID but act as if it comes from + # the fake jid + self.groupchat_jids = {} # {ID : groupchat_jid} + + self.privacy_rules_supported = False + self.vcard_supported = False + self.private_storage_supported = False + + self.muc_jid = {} # jid of muc server for each transport type + + self.get_config_values_or_default() + + def _compute_resource(self): + resource = gajim.config.get_per('accounts', self.name, 'resource') + # All valid resource substitution strings should be added to this hash. + if resource: + resource = Template(resource).safe_substitute({ + 'hostname': socket.gethostname() + }) + + def dispatch(self, event, data): + '''always passes account name as first param''' + gajim.interface.dispatch(event, self.name, data) + + def _reconnect(self): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def quit(self, kill_core): + if kill_core and gajim.account_is_connected(self.name): + self.disconnect(on_purpose=True) + + def test_gpg_passphrase(self, password): + '''Returns 'ok', 'bad_pass' or 'expired' ''' + if not self.gpg: + return False + self.gpg.passphrase = password + keyID = gajim.config.get_per('accounts', self.name, 'keyid') + signed = self.gpg.sign('test', keyID) + self.gpg.password = None + if signed == 'KEYEXPIRED': + return 'expired' + elif signed == 'BAD_PASSPHRASE': + return 'bad_pass' + return 'ok' + + def get_signed_msg(self, msg, callback = None): + '''returns the signed message if possible + or an empty string if gpg is not used + or None if waiting for passphrase. + callback is the function to call when user give the passphrase''' + signed = '' + keyID = gajim.config.get_per('accounts', self.name, 'keyid') + if keyID and self.USE_GPG: + use_gpg_agent = gajim.config.get('use_gpg_agent') + if self.gpg.passphrase is None and not use_gpg_agent: + # We didn't set a passphrase + return None + if self.gpg.passphrase is not None or use_gpg_agent: + signed = self.gpg.sign(msg, keyID) + if signed == 'BAD_PASSPHRASE': + self.USE_GPG = False + signed = '' + self.dispatch('BAD_PASSPHRASE', ()) + return signed + + def _on_disconnected(self): + ''' called when a disconnect request has completed successfully''' + self.disconnect(on_purpose=True) + self.dispatch('STATUS', 'offline') + + def get_status(self): + return gajim.SHOW_LIST[self.connected] + + def check_jid(self, jid): + '''this function must be implemented by derivated classes. + It has to return the valid jid, or raise a helpers.InvalidFormat exception + ''' + raise NotImplementedError + + def _prepare_message(self, jid, msg, keyID, type_='chat', subject='', + chatstate=None, msg_id=None, composing_xep=None, resource=None, + user_nick=None, xhtml=None, session=None, forward_from=None, form_node=None, + original_message=None, delayed=None, callback=None): + if not self.connection or self.connected < 2: + return 1 + try: + jid = self.check_jid(jid) + except helpers.InvalidFormat: + self.dispatch('ERROR', (_('Invalid Jabber ID'), + _('It is not possible to send a message to %s, this JID is not ' + 'valid.') % jid)) + return + + if msg and not xhtml and gajim.config.get( + 'rst_formatting_outgoing_messages'): + from common.rst_xhtml_generator import create_xhtml + xhtml = create_xhtml(msg) + if not msg and chatstate is None and form_node is None: + return + fjid = jid + if resource: + fjid += '/' + resource + msgtxt = msg + msgenc = '' + + if session: + fjid = session.get_to() + + if keyID and self.USE_GPG: + xhtml = None + if keyID == 'UNKNOWN': + error = _('Neither the remote presence is signed, nor a key was ' + 'assigned.') + elif keyID.endswith('MISMATCH'): + error = _('The contact\'s key (%s) does not match the key assigned ' + 'in Gajim.' % keyID[:8]) + else: + def encrypt_thread(msg, keyID, always_trust=False): + # encrypt message. This function returns (msgenc, error) + return self.gpg.encrypt(msg, [keyID], always_trust) + def _on_encrypted(output): + msgenc, error = output + if error == 'NOT_TRUSTED': + def _on_always_trust(answer): + if answer: + gajim.thread_interface(encrypt_thread, [msg, keyID, + True], _on_encrypted, []) + else: + self._message_encrypted_cb(output, type_, msg, msgtxt, + original_message, fjid, resource, jid, xhtml, + subject, chatstate, composing_xep, forward_from, + delayed, session, form_node, user_nick, keyID, + callback) + self.dispatch('GPG_ALWAYS_TRUST', _on_always_trust) + else: + self._message_encrypted_cb(output, type_, msg, msgtxt, + original_message, fjid, resource, jid, xhtml, subject, + chatstate, composing_xep, forward_from, delayed, session, + form_node, user_nick, keyID, callback) + gajim.thread_interface(encrypt_thread, [msg, keyID, False], + _on_encrypted, []) + return + + self._message_encrypted_cb(('', error), type_, msg, msgtxt, + original_message, fjid, resource, jid, xhtml, subject, chatstate, + composing_xep, forward_from, delayed, session, form_node, user_nick, + keyID, callback) + + self._on_continue_message(type_, msg, msgtxt, original_message, fjid, + resource, jid, xhtml, subject, msgenc, keyID, chatstate, composing_xep, + forward_from, delayed, session, form_node, user_nick, callback) + + def _message_encrypted_cb(self, output, type_, msg, msgtxt, original_message, + fjid, resource, jid, xhtml, subject, chatstate, composing_xep, forward_from, + delayed, session, form_node, user_nick, keyID, callback): + msgenc, error = output + + if msgenc and not error: + msgtxt = '[This message is *encrypted* (See :XEP:`27`]' + lang = os.getenv('LANG') + if lang is not None and lang != 'en': # we're not english + # one in locale and one en + msgtxt = _('[This message is *encrypted* (See :XEP:`27`]') + \ + ' (' + msgtxt + ')' + self._on_continue_message(type_, msg, msgtxt, original_message, fjid, + resource, jid, xhtml, subject, msgenc, keyID, chatstate, + composing_xep, forward_from, delayed, session, form_node, user_nick, + callback) + return + # Encryption failed, do not send message + tim = localtime() + self.dispatch('MSGNOTSENT', (jid, error, msgtxt, tim, session)) + + def _on_continue_message(self, type_, msg, msgtxt, original_message, fjid, + resource, jid, xhtml, subject, msgenc, keyID, chatstate, composing_xep, + forward_from, delayed, session, form_node, user_nick, callback): + if type_ == 'chat': + msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ=type_, + xhtml=xhtml) + else: + if subject: + msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ='normal', + subject=subject, xhtml=xhtml) + else: + msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ='normal', + xhtml=xhtml) + if msgenc: + msg_iq.setTag(common.xmpp.NS_ENCRYPTED + ' x').setData(msgenc) + + if form_node: + msg_iq.addChild(node=form_node) + + # XEP-0172: user_nickname + if user_nick: + msg_iq.setTag('nick', namespace = common.xmpp.NS_NICK).setData( + user_nick) + + # TODO: We might want to write a function so we don't need to + # reproduce that ugly if somewhere else. + if resource: + contact = gajim.contacts.get_contact(self.name, jid, resource) + else: + contact = gajim.contacts.get_contact_with_highest_priority(self.name, + jid) + + # chatstates - if peer supports xep85 or xep22, send chatstates + # please note that the only valid tag inside a message containing a + # tag is the active event + if chatstate is not None and contact: + if ((composing_xep == 'XEP-0085' or not composing_xep) \ + and composing_xep != 'asked_once') or \ + contact.supports(common.xmpp.NS_CHATSTATES): + # XEP-0085 + msg_iq.setTag(chatstate, namespace=common.xmpp.NS_CHATSTATES) + if composing_xep in ('XEP-0022', 'asked_once') or \ + not composing_xep: + # XEP-0022 + chatstate_node = msg_iq.setTag('x', namespace=common.xmpp.NS_EVENT) + if chatstate is 'composing' or msgtxt: + chatstate_node.addChild(name='composing') + + if forward_from: + addresses = msg_iq.addChild('addresses', + namespace=common.xmpp.NS_ADDRESS) + addresses.addChild('address', attrs = {'type': 'ofrom', + 'jid': forward_from}) + + # XEP-0203 + if delayed: + our_jid = gajim.get_jid_from_account(self.name) + '/' + \ + self.server_resource + timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(delayed)) + msg_iq.addChild('delay', namespace=common.xmpp.NS_DELAY2, + attrs={'from': our_jid, 'stamp': timestamp}) + + # XEP-0184 + if msgtxt and gajim.config.get_per('accounts', self.name, + 'request_receipt') and contact and contact.supports( + common.xmpp.NS_RECEIPTS): + msg_iq.setTag('request', namespace=common.xmpp.NS_RECEIPTS) + + if session: + # XEP-0201 + session.last_send = time.time() + msg_iq.setThread(session.thread_id) + + # XEP-0200 + if session.enable_encryption: + msg_iq = session.encrypt_stanza(msg_iq) + + if callback: + callback(jid, msg, keyID, forward_from, session, original_message, + subject, type_, msg_iq) + + def log_message(self, jid, msg, forward_from, session, original_message, + subject, type_): + if not forward_from and session and session.is_loggable(): + ji = gajim.get_jid_without_resource(jid) + if gajim.config.should_log(self.name, ji): + log_msg = msg + if original_message is not None: + log_msg = original_message + if subject: + log_msg = _('Subject: %(subject)s\n%(message)s') % \ + {'subject': subject, 'message': log_msg} + if log_msg: + if type_ == 'chat': + kind = 'chat_msg_sent' + else: + kind = 'single_msg_sent' + try: + gajim.logger.write(kind, jid, log_msg) + except exceptions.PysqliteOperationalError, e: + self.dispatch('ERROR', (_('Disk Write Error'), str(e))) + except exceptions.DatabaseMalformed: + pritext = _('Database Error') + sectext = _('The database file (%s) cannot be read. Try to ' + 'repair it (see http://trac.gajim.org/wiki/DatabaseBackup)' + ' or remove it (all history will be lost).') % \ + common.logger.LOG_DB_PATH + + def ack_subscribed(self, jid): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def ack_unsubscribed(self, jid): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def request_subscription(self, jid, msg='', name='', groups=[], + auto_auth=False): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def send_authorization(self, jid): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def refuse_authorization(self, jid): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def unsubscribe(self, jid, remove_auth = True): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def unsubscribe_agent(self, agent): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def update_contact(self, jid, name, groups): + if self.connection: + self.connection.getRoster().setItem(jid=jid, name=name, groups=groups) + + def update_contacts(self, contacts): + '''update multiple roster items''' + if self.connection: + self.connection.getRoster().setItemMulti(contacts) + + def new_account(self, name, config, sync=False): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def _on_new_account(self, con=None, con_type=None): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def account_changed(self, new_name): + self.name = new_name + + def request_last_status_time(self, jid, resource): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def request_os_info(self, jid, resource): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def get_settings(self): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def get_bookmarks(self): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def store_bookmarks(self): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def get_metacontacts(self): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def send_agent_status(self, agent, ptype): + '''To be implemented by derivated classes''' + raise NotImplementedError + + def gpg_passphrase(self, passphrase): + if self.gpg: + use_gpg_agent = gajim.config.get('use_gpg_agent') + if use_gpg_agent: + self.gpg.passphrase = None + else: + self.gpg.passphrase = passphrase + + def ask_gpg_keys(self): + if self.gpg: + keys = self.gpg.get_keys() + return keys + return None + + def ask_gpg_secrete_keys(self): + if self.gpg: + keys = self.gpg.get_secret_keys() + return keys + return None + + def load_roster_from_db(self): + # Do nothing by default + return + + def _event_dispatcher(self, realm, event, data): + if realm == '': + if event == common.xmpp.transports_nb.DATA_RECEIVED: + self.dispatch('STANZA_ARRIVED', unicode(data, errors='ignore')) + elif event == common.xmpp.transports_nb.DATA_SENT: + self.dispatch('STANZA_SENT', unicode(data)) + + def change_status(self, show, msg, auto=False): + if not show in ['offline', 'online', 'chat', 'away', 'xa', 'dnd']: + return -1 + if not msg: + msg = '' + sign_msg = False + if not auto and not show == 'offline': + sign_msg = True + if show != 'invisible': + # We save it only when privacy list is accepted + self.status = msg + if show != 'offline' and self.connected < 1: + # set old_show to requested 'show' in case we need to + # recconect before we auth to server + self.old_show = show + self.on_purpose = False + self.server_resource = self._compute_resource() + if gajim.HAVE_GPG: + self.USE_GPG = True + self.gpg = GnuPG.GnuPG(gajim.config.get('use_gpg_agent')) + self.connect_and_init(show, msg, sign_msg) + + elif show == 'offline': + if self.connection: + p = common.xmpp.Presence(typ = 'unavailable') + p = self.add_sha(p, False) + if msg: + p.setStatus(msg) + + self.connection.RegisterDisconnectHandler(self._on_disconnected) + self.connection.send(p, now=True) + self.connection.start_disconnect() + else: + self._on_disconnected() + + elif show != 'offline' and self.connected > 0: + # dont'try to connect, when we are in state 'connecting' + if self.connected == 1: + return + if show == 'invisible': + self._change_to_invisible(msg) + return + was_invisible = self.connected == gajim.SHOW_LIST.index('invisible') + self.connected = gajim.SHOW_LIST.index(show) + if was_invisible: + self._change_from_invisible() + self._update_status(show, msg) + +class Connection(CommonConnection, ConnectionHandlers): + '''Connection class''' + def __init__(self, name): + CommonConnection.__init__(self, name) + ConnectionHandlers.__init__(self) # this property is used to prevent double connections self.last_connection = None # last ClientCommon instance # If we succeed to connect, remember it so next time we try (after a @@ -117,36 +591,39 @@ class Connection(ConnectionHandlers): self.lang = None if locale.getdefaultlocale()[0]: self.lang = locale.getdefaultlocale()[0].split('_')[0] - self.is_zeroconf = False - self.gpg = None - self.USE_GPG = False - if gajim.HAVE_GPG: - self.USE_GPG = True - self.gpg = GnuPG.GnuPG(gajim.config.get('use_gpg_agent')) - self.status = '' - self.priority = gajim.get_priority(name, 'offline') - self.old_show = '' # increase/decrease default timeout for server responses self.try_connecting_for_foo_secs = 45 # holds the actual hostname to which we are connected self.connected_hostname = None - self.time_to_reconnect = None self.last_time_to_reconnect = None self.new_account_info = None self.new_account_form = None - self.bookmarks = [] self.annotations = {} - self.on_purpose = False self.last_io = gajim.idlequeue.current_time() self.last_sent = [] self.last_history_time = {} self.password = passwords.get_password(name) - self.server_resource = gajim.config.get_per('accounts', name, 'resource') - # All valid resource substitution strings should be added to this hash. - if self.server_resource: - self.server_resource = Template(self.server_resource).safe_substitute({ - 'hostname': socket.gethostname() - }) + + # Used to ask privacy only once at connection + self.music_track_info = 0 + self.pubsub_supported = False + self.pubsub_publish_options_supported = False + # Do we auto accept insecure connection + self.connection_auto_accepted = False + self.pasword_callback = None + + self.on_connect_success = None + self.on_connect_failure = None + self.retrycount = 0 + self.jids_for_auto_auth = [] # list of jid to auto-authorize + self.available_transports = {} # list of available transports on this + # server {'icq': ['icq.server.com', 'icq2.server.com'], } + self.private_storage_supported = True + self.streamError = '' + self.secret_hmac = str(random.random())[2:] + # END __init__ + + def get_config_values_or_default(): if gajim.config.get_per('accounts', self.name, 'keep_alives_enabled'): self.keepalives = gajim.config.get_per('accounts', self.name, 'keep_alive_every_foo_secs') @@ -157,45 +634,9 @@ class Connection(ConnectionHandlers): 'ping_alive_every_foo_secs') else: self.pingalives = 0 - self.privacy_rules_supported = False - # Used to ask privacy only once at connection - self.privacy_rules_requested = False - self.blocked_list = [] - self.blocked_contacts = [] - self.blocked_groups = [] - self.blocked_all = False - self.music_track_info = 0 - self.pubsub_supported = False - self.pubsub_publish_options_supported = False - self.pep_supported = False - self.pep = {} - # Do we continue connection when we get roster (send presence,get vcard..) - self.continue_connect_info = None - # Do we auto accept insecure connection - self.connection_auto_accepted = False - # To know the groupchat jid associated with a sranza ID. Useful to - # request vcard or os info... to a real JID but act as if it comes from - # the fake jid - self.groupchat_jids = {} # {ID : groupchat_jid} - self.pasword_callback = None - - self.on_connect_success = None - self.on_connect_failure = None - self.retrycount = 0 - self.jids_for_auto_auth = [] # list of jid to auto-authorize - self.muc_jid = {} # jid of muc server for each transport type - self.available_transports = {} # list of available transports on this - # server {'icq': ['icq.server.com', 'icq2.server.com'], } - self.vcard_supported = False - self.private_storage_supported = True - self.streamError = '' - self.secret_hmac = str(random.random())[2:] - # END __init__ - - def dispatch(self, event, data): - '''always passes account name as first param''' - gajim.interface.dispatch(event, self.name, data) + def check_jid(self, jid): + return helpers.parse_jid(jid) def _reconnect(self): # Do not try to reco while we are already trying @@ -222,6 +663,8 @@ class Connection(ConnectionHandlers): if self.connection: # make sure previous connection is completely closed gajim.proxy65_manager.disconnect(self.connection) + self.terminate_sessions() + self.remove_all_transfers() self.connection.disconnect() self.last_connection = None self.connection = None @@ -277,6 +720,7 @@ class Connection(ConnectionHandlers): _('Reconnect manually.'))) def _event_dispatcher(self, realm, event, data): + CommonConnection._event_dispatcher(self, realm, event, data) if realm == common.xmpp.NS_REGISTER: if event == common.xmpp.features_nb.REGISTER_DATA_RECEIVED: # data is (agent, DataFrom, is_form, error_msg) @@ -382,11 +826,6 @@ class Connection(ConnectionHandlers): elif event == common.xmpp.features_nb.PRIVACY_LISTS_ACTIVE_DEFAULT: # data is (dict) self.dispatch('PRIVACY_LISTS_ACTIVE_DEFAULT', (data)) - elif realm == '': - if event == common.xmpp.transports_nb.DATA_RECEIVED: - self.dispatch('STANZA_ARRIVED', unicode(data, errors = 'ignore')) - elif event == common.xmpp.transports_nb.DATA_SENT: - self.dispatch('STANZA_SENT', unicode(data)) def _select_next_host(self, hosts): '''Selects the next host according to RFC2782 p.3 based on it's @@ -800,10 +1239,6 @@ class Connection(ConnectionHandlers): self.on_connect_auth = None # END connect - def quit(self, kill_core): - if kill_core and gajim.account_is_connected(self.name): - self.disconnect(on_purpose=True) - def add_lang(self, stanza): if self.lang: stanza.setAttr('xml:lang', self.lang) @@ -983,45 +1418,11 @@ class Connection(ConnectionHandlers): #Inform GUI we just signed in self.dispatch('SIGNED_IN', ()) - def test_gpg_passphrase(self, password): - '''Returns 'ok', 'bad_pass' or 'expired' ''' - if not self.gpg: - return False - self.gpg.passphrase = password - keyID = gajim.config.get_per('accounts', self.name, 'keyid') - signed = self.gpg.sign('test', keyID) - self.gpg.password = None - if signed == 'KEYEXPIRED': - return 'expired' - elif signed == 'BAD_PASSPHRASE': - return 'bad_pass' - return 'ok' - def get_signed_presence(self, msg, callback = None): if gajim.config.get_per('accounts', self.name, 'gpg_sign_presence'): return self.get_signed_msg(msg, callback) return '' - def get_signed_msg(self, msg, callback = None): - '''returns the signed message if possible - or an empty string if gpg is not used - or None if waiting for passphrase. - callback is the function to call when user give the passphrase''' - signed = '' - keyID = gajim.config.get_per('accounts', self.name, 'keyid') - if keyID and self.USE_GPG: - use_gpg_agent = gajim.config.get('use_gpg_agent') - if self.gpg.passphrase is None and not use_gpg_agent: - # We didn't set a passphrase - return None - if self.gpg.passphrase is not None or use_gpg_agent: - signed = self.gpg.sign(msg, keyID) - if signed == 'BAD_PASSPHRASE': - self.USE_GPG = False - signed = '' - self.dispatch('BAD_PASSPHRASE', ()) - return signed - def connect_and_auth(self): self.on_connect_success = self._connect_success self.on_connect_failure = self._connect_failure @@ -1075,92 +1476,30 @@ class Connection(ConnectionHandlers): p.setTag(common.xmpp.NS_SIGNED + ' x').setData(signed) self.connection.send(p) - def change_status(self, show, msg, auto = False): - if not show in gajim.SHOW_LIST: - return -1 - sshow = helpers.get_xmpp_show(show) - if not msg: - msg = '' - sign_msg = False - if not auto and not show == 'offline': - sign_msg = True - if show != 'invisible': - # We save it only when privacy list is accepted - self.status = msg - if show != 'offline' and self.connected < 1: - # set old_show to requested 'show' in case we need to - # recconect before we auth to server - self.old_show = show - self.on_purpose = False - self.server_resource = gajim.config.get_per('accounts', self.name, - 'resource') - # All valid resource substitution strings should be added to this hash. - if self.server_resource: - self.server_resource = Template(self.server_resource).\ - safe_substitute({ - 'hostname': socket.gethostname() - }) - if gajim.HAVE_GPG: - self.USE_GPG = True - self.gpg = GnuPG.GnuPG(gajim.config.get('use_gpg_agent')) - self.connect_and_init(show, msg, sign_msg) + def _change_to_invisible(self, msg): + signed = self.get_signed_presence(msg) + self.send_invisible_presence(msg, signed) - elif show == 'offline': - self.connected = 0 - if self.connection: - self.terminate_sessions() - - self.on_purpose = True - p = common.xmpp.Presence(typ = 'unavailable') - p = self.add_sha(p, False) - if msg: - p.setStatus(msg) - self.remove_all_transfers() - self.time_to_reconnect = None - - self.connection.RegisterDisconnectHandler(self._on_disconnected) - self.connection.send(p, now=True) - self.connection.start_disconnect() - #self.connection.start_disconnect(p, self._on_disconnected) - else: - self.time_to_reconnect = None - self._on_disconnected() - - elif show != 'offline' and self.connected > 0: - # dont'try to connect, when we are in state 'connecting' - if self.connected == 1: - return - if show == 'invisible': - signed = self.get_signed_presence(msg) - self.send_invisible_presence(msg, signed) - return - was_invisible = self.connected == gajim.SHOW_LIST.index('invisible') - self.connected = gajim.SHOW_LIST.index(show) - if was_invisible and self.privacy_rules_supported: - iq = self.build_privacy_rule('visible', 'allow') - self.connection.send(iq) - self.activate_privacy_rule('visible') - priority = unicode(gajim.get_priority(self.name, sshow)) - p = common.xmpp.Presence(typ = None, priority = priority, show = sshow) - p = self.add_sha(p) - if msg: - p.setStatus(msg) - signed = self.get_signed_presence(msg) - if signed: - p.setTag(common.xmpp.NS_SIGNED + ' x').setData(signed) - if self.connection: - self.connection.send(p) - self.priority = priority - self.dispatch('STATUS', show) - - def _on_disconnected(self): - ''' called when a disconnect request has completed successfully''' - self.disconnect(on_purpose=True) - self.dispatch('STATUS', 'offline') - - def get_status(self): - return gajim.SHOW_LIST[self.connected] + def _change_from_invisible(self): + if self.privacy_rules_supported: + iq = self.build_privacy_rule('visible', 'allow') + self.connection.send(iq) + self.activate_privacy_rule('visible') + def _update_status(self, show, msg): + xmpp_show = helpers.get_xmpp_show(show) + priority = unicode(gajim.get_priority(self.name, xmpp_show)) + p = common.xmpp.Presence(typ=None, priority=priority, show=xmpp_show) + p = self.add_sha(p) + if msg: + p.setStatus(msg) + signed = self.get_signed_presence(msg) + if signed: + p.setTag(common.xmpp.NS_SIGNED + ' x').setData(signed) + if self.connection: + self.connection.send(p) + self.priority = priority + self.dispatch('STATUS', show) def send_motd(self, jid, subject = '', msg = '', xhtml = None): if not self.connection: @@ -1174,202 +1513,23 @@ class Connection(ConnectionHandlers): chatstate=None, msg_id=None, composing_xep=None, resource=None, user_nick=None, xhtml=None, session=None, forward_from=None, form_node=None, original_message=None, delayed=None, callback=None, callback_args=[]): - if not self.connection or self.connected < 2: - return 1 - try: + + def cb(jid, msg, keyID, forward_from, session, original_message, subject, + type_, msg_iq): + msg_id = self.connection.send(msg_iq) jid = helpers.parse_jid(jid) - except helpers.InvalidFormat: - self.dispatch('ERROR', (_('Invalid Jabber ID'), - _('It is not possible to send a message to %s, this JID is not ' - 'valid.') % jid)) - return + self.dispatch('MSGSENT', (jid, msg, keyID)) + if callback: + callback(msg_id, *callback_args) - if msg and not xhtml and gajim.config.get( - 'rst_formatting_outgoing_messages'): - from common.rst_xhtml_generator import create_xhtml - xhtml = create_xhtml(msg) - if not msg and chatstate is None and form_node is None: - return - fjid = jid - if resource: - fjid += '/' + resource - msgtxt = msg - msgenc = '' + self.log_message(jid, msg, forward_from, session, original_message, + subject, type_) - if session: - fjid = session.get_to() - - if keyID and self.USE_GPG: - xhtml = None - if keyID == 'UNKNOWN': - error = _('Neither the remote presence is signed, nor a key was assigned.') - elif keyID.endswith('MISMATCH'): - error = _('The contact\'s key (%s) does not match the key assigned in Gajim.' % keyID[:8]) - else: - def encrypt_thread(msg, keyID, always_trust=False): - # encrypt message. This function returns (msgenc, error) - return self.gpg.encrypt(msg, [keyID], always_trust) - def _on_encrypted(output): - msgenc, error = output - if error == 'NOT_TRUSTED': - def _on_always_trust(answer): - if answer: - gajim.thread_interface(encrypt_thread, [msg, keyID, - True], _on_encrypted, []) - else: - self._on_message_encrypted(output, type_, msg, msgtxt, - original_message, fjid, resource, jid, xhtml, - subject, chatstate, composing_xep, forward_from, - delayed, session, form_node, user_nick, keyID, - callback, callback_args) - self.dispatch('GPG_ALWAYS_TRUST', _on_always_trust) - else: - self._on_message_encrypted(output, type_, msg, msgtxt, - original_message, fjid, resource, jid, xhtml, subject, - chatstate, composing_xep, forward_from, delayed, session, - form_node, user_nick, keyID, callback, callback_args) - gajim.thread_interface(encrypt_thread, [msg, keyID, False], - _on_encrypted, []) - return - - self._on_message_encrypted(('', error), type_, msg, msgtxt, - original_message, fjid, resource, jid, xhtml, subject, chatstate, - composing_xep, forward_from, delayed, session, form_node, user_nick, - keyID, callback, callback_args) - - self._on_continue_message(type_, msg, msgtxt, original_message, fjid, - resource, jid, xhtml, subject, msgenc, keyID, chatstate, composing_xep, - forward_from, delayed, session, form_node, user_nick, callback, - callback_args) - - def _on_message_encrypted(self, output, type_, msg, msgtxt, original_message, - fjid, resource, jid, xhtml, subject, chatstate, composing_xep, forward_from, - delayed, session, form_node, user_nick, keyID, callback, callback_args): - msgenc, error = output - - if msgenc and not error: - msgtxt = '[This message is *encrypted* (See :XEP:`27`]' - lang = os.getenv('LANG') - if lang is not None and lang != 'en': # we're not english - # one in locale and one en - msgtxt = _('[This message is *encrypted* (See :XEP:`27`]') + \ - ' (' + msgtxt + ')' - self._on_continue_message(type_, msg, msgtxt, original_message, fjid, - resource, jid, xhtml, subject, msgenc, keyID, chatstate, - composing_xep, forward_from, delayed, session, form_node, user_nick, - callback, callback_args) - return - # Encryption failed, do not send message - tim = localtime() - self.dispatch('MSGNOTSENT', (jid, error, msgtxt, tim, session)) - - def _on_continue_message(self, type_, msg, msgtxt, original_message, fjid, - resource, jid, xhtml, subject, msgenc, keyID, chatstate, composing_xep, - forward_from, delayed, session, form_node, user_nick, callback, - callback_args): - if type_ == 'chat': - msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ=type_, - xhtml=xhtml) - else: - if subject: - msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ='normal', - subject=subject, xhtml=xhtml) - else: - msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ='normal', - xhtml=xhtml) - if msgenc: - msg_iq.setTag(common.xmpp.NS_ENCRYPTED + ' x').setData(msgenc) - - if form_node: - msg_iq.addChild(node=form_node) - - # XEP-0172: user_nickname - if user_nick: - msg_iq.setTag('nick', namespace = common.xmpp.NS_NICK).setData( - user_nick) - - # TODO: We might want to write a function so we don't need to - # reproduce that ugly if somewhere else. - if resource: - contact = gajim.contacts.get_contact(self.name, jid, resource) - else: - contact = gajim.contacts.get_contact_with_highest_priority(self.name, - jid) - - # chatstates - if peer supports xep85 or xep22, send chatstates - # please note that the only valid tag inside a message containing a - # tag is the active event - if chatstate is not None and contact: - if ((composing_xep == 'XEP-0085' or not composing_xep) \ - and composing_xep != 'asked_once') or \ - contact.supports(common.xmpp.NS_CHATSTATES): - # XEP-0085 - msg_iq.setTag(chatstate, namespace=common.xmpp.NS_CHATSTATES) - if composing_xep in ('XEP-0022', 'asked_once') or \ - not composing_xep: - # XEP-0022 - chatstate_node = msg_iq.setTag('x', namespace=common.xmpp.NS_EVENT) - if chatstate is 'composing' or msgtxt: - chatstate_node.addChild(name='composing') - - if forward_from: - addresses = msg_iq.addChild('addresses', - namespace=common.xmpp.NS_ADDRESS) - addresses.addChild('address', attrs = {'type': 'ofrom', - 'jid': forward_from}) - - # XEP-0203 - if delayed: - our_jid = gajim.get_jid_from_account(self.name) + '/' + \ - self.server_resource - timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(delayed)) - msg_iq.addChild('delay', namespace=common.xmpp.NS_DELAY2, - attrs={'from': our_jid, 'stamp': timestamp}) - - # XEP-0184 - if msgtxt and gajim.config.get_per('accounts', self.name, - 'request_receipt') and contact and contact.supports( - common.xmpp.NS_RECEIPTS): - msg_iq.setTag('request', namespace=common.xmpp.NS_RECEIPTS) - - if session: - # XEP-0201 - session.last_send = time.time() - msg_iq.setThread(session.thread_id) - - # XEP-0200 - if session.enable_encryption: - msg_iq = session.encrypt_stanza(msg_iq) - - msg_id = self.connection.send(msg_iq) - if not forward_from and session and session.is_loggable(): - ji = gajim.get_jid_without_resource(jid) - if gajim.config.should_log(self.name, ji): - log_msg = msg - if original_message is not None: - log_msg = original_message - if subject: - log_msg = _('Subject: %(subject)s\n%(message)s') % \ - {'subject': subject, 'message': msg} - if log_msg: - if type_ == 'chat': - kind = 'chat_msg_sent' - else: - kind = 'single_msg_sent' - try: - gajim.logger.write(kind, jid, log_msg) - except exceptions.PysqliteOperationalError, e: - self.dispatch('ERROR', (_('Disk Write Error'), str(e))) - except exceptions.DatabaseMalformed: - pritext = _('Database Error') - sectext = _('The database file (%s) cannot be read. Try to ' - 'repair it (see http://trac.gajim.org/wiki/DatabaseBackup)' - ' or remove it (all history will be lost).') % \ - common.logger.LOG_DB_PATH - self.dispatch('MSGSENT', (jid, msg, keyID)) - - if callback: - callback(msg_id, *callback_args) + self._prepare_message(jid, msg, keyID, type_=type_, subject=subject, + chatstate=chatstate, msg_id=msg_id, composing_xep=composing_xep, + resource=resource, user_nick=user_nick, xhtml=xhtml, session=session, + forward_from=forward_from, form_node=form_node, + original_message=original_message, delayed=delayed, callback=cb) def send_contacts(self, contacts, jid): '''Send contacts with RosterX (Xep-0144)''' @@ -1474,17 +1634,6 @@ class Connection(ConnectionHandlers): self.connection.send(iq) self.connection.getRoster().delItem(agent) - def update_contact(self, jid, name, groups): - '''update roster item on jabber server''' - if self.connection: - self.connection.getRoster().setItem(jid = jid, name = name, - groups = groups) - - def update_contacts(self, contacts): - '''update multiple roster items on jabber server''' - if self.connection: - self.connection.getRoster().setItemMulti(contacts) - def send_new_account_infos(self, form, is_form): if is_form: # Get username and password and put them in new_account_info @@ -1502,7 +1651,7 @@ class Connection(ConnectionHandlers): self.new_account_form = form self.new_account(self.name, self.new_account_info) - def new_account(self, name, config, sync = False): + def new_account(self, name, config, sync=False): # If a connection already exist we cannot create a new account if self.connection: return @@ -1513,7 +1662,7 @@ class Connection(ConnectionHandlers): self.on_connect_failure = self._on_new_account self.connect(config) - def _on_new_account(self, con = None, con_type = None): + def _on_new_account(self, con=None, con_type=None): if not con_type: if len(self._connection_types) or len(self._hosts): # There are still other way to try to connect @@ -1525,9 +1674,6 @@ class Connection(ConnectionHandlers): self.connection = con common.xmpp.features_nb.getRegInfo(con, self._hostname) - def account_changed(self, new_name): - self.name = new_name - def request_last_status_time(self, jid, resource, groupchat_jid=None): '''groupchat_jid is used when we want to send a request to a real jid and act as if the answer comes from the groupchat_jid''' @@ -1918,26 +2064,6 @@ class Connection(ConnectionHandlers): query.addChild(node = form) self.connection.send(iq) - def gpg_passphrase(self, passphrase): - if self.gpg: - use_gpg_agent = gajim.config.get('use_gpg_agent') - if use_gpg_agent: - self.gpg.passphrase = None - else: - self.gpg.passphrase = passphrase - - def ask_gpg_keys(self): - if self.gpg: - keys = self.gpg.get_keys() - return keys - return None - - def ask_gpg_secrete_keys(self): - if self.gpg: - keys = self.gpg.get_secret_keys() - return keys - return None - def change_password(self, password): if not self.connection: return diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index 704cad170..dd60269bf 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -92,6 +92,7 @@ RECV_BUFSIZE = 32768 # 2x maximum size of ssl packet, should be plenty DATA_RECEIVED = 'DATA RECEIVED' DATA_SENT = 'DATA SENT' +DATA_ERROR = 'DATA ERROR' DISCONNECTED = 'DISCONNECTED' DISCONNECTING = 'DISCONNECTING' diff --git a/src/common/zeroconf/client_zeroconf.py b/src/common/zeroconf/client_zeroconf.py index 730eb3ec3..6b50a9fea 100644 --- a/src/common/zeroconf/client_zeroconf.py +++ b/src/common/zeroconf/client_zeroconf.py @@ -23,7 +23,7 @@ from common.xmpp.idlequeue import IdleObject from common.xmpp import dispatcher_nb, simplexml from common.xmpp.plugin import * from common.xmpp.simplexml import ustr -from common.xmpp.transports_nb import DATA_RECEIVED, DATA_SENT +from common.xmpp.transports_nb import DATA_RECEIVED, DATA_SENT, DATA_ERROR from common.zeroconf import zeroconf from common.xmpp.protocol import * @@ -395,6 +395,7 @@ class P2PConnection(IdleObject, PlugIn): False, else send it instantly. If supplied data is unicode string, encode it to utf-8. ''' + print 'ici' if self.state <= 0: return @@ -416,8 +417,11 @@ class P2PConnection(IdleObject, PlugIn): ids = self.client.conn_holder.ids_of_awaiting_messages if self.fd in ids and len(ids[self.fd]) > 0: for (id_, thread_id) in ids[self.fd]: - self._owner.Dispatcher.Event('', DATA_ERROR, (self.client.to, - thread_id)) + if hasattr(self._owner, 'Dispatcher'): + self._owner.Dispatcher.Event('', DATA_ERROR, (self.client.to, + thread_id)) + else: + self._owner.on_not_ok('conenction timeout') ids[self.fd] = [] self.pollend() @@ -578,6 +582,8 @@ class ClientZeroconf: self.hash_to_port = {} self.listener = None self.ids_of_awaiting_messages = {} + self.disconnect_handlers = [] + self.disconnecting = False def connect(self, show, msg): self.port = self.start_listener(self.caller.port) @@ -632,6 +638,9 @@ class ClientZeroconf: self.last_msg = msg def disconnect(self): + # to avoid recursive calls + if self.disconnecting: + return if self.listener: self.listener.disconnect() self.listener = None @@ -642,6 +651,14 @@ class ClientZeroconf: self.roster.zeroconf = None self.roster._data = None self.roster = None + self.disconnecting = True + for i in reversed(self.disconnect_handlers): + log.debug('Calling disconnect handler %s' % i) + i() + self.disconnecting = False + + def start_disconnect(self): + self.disconnect() def kill_all_connections(self): for connection in self.connections.values(): @@ -720,6 +737,14 @@ class ClientZeroconf: P2PClient(None, item['address'], item['port'], self, [(stanza, is_message)], to, on_ok=on_ok, on_not_ok=on_not_ok) + def RegisterDisconnectHandler(self, handler): + ''' Register handler that will be called on disconnect.''' + self.disconnect_handlers.append(handler) + + def UnregisterDisconnectHandler(self, handler): + ''' Unregister handler that is called on disconnect.''' + self.disconnect_handlers.remove(handler) + def SendAndWaitForResponse(self, stanza, timeout=None, func=None, args=None): ''' Send stanza and wait for recipient's response to it. Will call transports diff --git a/src/common/zeroconf/connection_handlers_zeroconf.py b/src/common/zeroconf/connection_handlers_zeroconf.py index 09818be47..b08fba1ce 100644 --- a/src/common/zeroconf/connection_handlers_zeroconf.py +++ b/src/common/zeroconf/connection_handlers_zeroconf.py @@ -57,10 +57,10 @@ from session import ChatControlSession class ConnectionVcard(connection_handlers.ConnectionVcard): def add_sha(self, p, send_caps = True): - pass + return p def add_caps(self, p): - pass + return p def request_vcard(self, jid = None, is_fake_jid = False): pass diff --git a/src/common/zeroconf/connection_zeroconf.py b/src/common/zeroconf/connection_zeroconf.py index 26725f5f0..52876c8d6 100644 --- a/src/common/zeroconf/connection_zeroconf.py +++ b/src/common/zeroconf/connection_zeroconf.py @@ -5,7 +5,7 @@ ## - Nikos Kouremenos ## - Dimitur Kirov ## - Travis Shirk -## - Stefan Bethge +## - Stefan Bethge ## ## Copyright (C) 2003-2004 Yann Leboulanger ## Vincent Hanquez @@ -43,64 +43,29 @@ if os.name != 'nt': import getpass import gobject +from common.connection import CommonConnection from common import gajim from common import GnuPG from common.zeroconf import client_zeroconf from common.zeroconf import zeroconf from connection_handlers_zeroconf import * -class ConnectionZeroconf(ConnectionHandlersZeroconf): +class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf): '''Connection class''' def __init__(self, name): + CommonConnection.__init__(self, name) ConnectionHandlersZeroconf.__init__(self) # system username self.username = None - self.name = name self.server_resource = '' # zeroconf has no resource, fake an empty one - self.connected = 0 # offline - self.connection = None - self.gpg = None - self.USE_GPG = False - if gajim.HAVE_GPG: - self.USE_GPG = True - self.gpg = GnuPG.GnuPG(gajim.config.get('use_gpg_agent')) self.is_zeroconf = True - self.privacy_rules_supported = False - self.blocked_list = [] - self.blocked_contacts = [] - self.blocked_groups = [] - self.blocked_all = False - self.status = '' - self.old_show = '' - self.priority = 0 - self.call_resolve_timeout = False - - self.time_to_reconnect = None - #self.new_account_info = None - self.bookmarks = [] - - #we don't need a password, but must be non-empty + # we don't need a password, but must be non-empty self.password = 'zeroconf' - self.autoconnect = False - self.sync_with_global_status = True - self.no_log_for = False - - self.pep_supported = False - self.pep = {} - # Do we continue connection when we get roster (send presence,get vcard...) - self.continue_connect_info = None - if gajim.HAVE_GPG: - self.USE_GPG = True - self.gpg = GnuPG.GnuPG(gajim.config.get('use_gpg_agent')) self.get_config_values_or_default() - self.muc_jid = {} # jid of muc server for each transport type - self.vcard_supported = False - self.private_storage_supported = False - def get_config_values_or_default(self): ''' get name, host, port from config, or create zeroconf account with default values''' @@ -108,79 +73,61 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): if not gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'name'): gajim.log.debug('Creating zeroconf account') gajim.config.add_per('accounts', gajim.ZEROCONF_ACC_NAME) - gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'autoconnect', True) - gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'no_log_for', '') - gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'password', 'zeroconf') - gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'sync_with_global_status', True) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'autoconnect', True) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'no_log_for', + '') + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'password', + 'zeroconf') + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'sync_with_global_status', True) - gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'custom_port', 5298) - gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'is_zeroconf', True) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'custom_port', 5298) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'is_zeroconf', True) #XXX make sure host is US-ASCII self.host = unicode(socket.gethostname()) - gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'hostname', self.host) - self.port = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'custom_port') - self.autoconnect = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'autoconnect') - self.sync_with_global_status = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'sync_with_global_status') - self.no_log_for = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'no_log_for') - self.first = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_first_name') - self.last = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_last_name') - self.jabber_id = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_jabber_id') - self.email = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_email') + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'hostname', + self.host) + self.port = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'custom_port') + self.autoconnect = gajim.config.get_per('accounts', + gajim.ZEROCONF_ACC_NAME, 'autoconnect') + self.sync_with_global_status = gajim.config.get_per('accounts', + gajim.ZEROCONF_ACC_NAME, 'sync_with_global_status') + self.first = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'zeroconf_first_name') + self.last = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'zeroconf_last_name') + self.jabber_id = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'zeroconf_jabber_id') + self.email = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'zeroconf_email') if not self.username: self.username = unicode(getpass.getuser()) - gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'name', self.username) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'name', + self.username) else: - self.username = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'name') + self.username = gajim.config.get_per('accounts', + gajim.ZEROCONF_ACC_NAME, 'name') # END __init__ - def dispatch(self, event, data): - gajim.interface.dispatch(event, self.name, data) + def check_jid(self, jid): + return jid def _reconnect(self): # Do not try to reco while we are already trying self.time_to_reconnect = None gajim.log.debug('reconnect') -# signed = self.get_signed_msg(self.status) self.disconnect() self.change_status(self.old_show, self.status) - def quit(self, kill_core): - if kill_core and self.connected > 1: - self.disconnect() - def disable_account(self): self.disconnect() - def test_gpg_passphrase(self, password): - self.gpg.passphrase = password - keyID = gajim.config.get_per('accounts', self.name, 'keyid') - signed = self.gpg.sign('test', keyID) - self.gpg.password = None - return signed != 'BAD_PASSPHRASE' - - def get_signed_msg(self, msg): - signed = '' - keyID = gajim.config.get_per('accounts', self.name, 'keyid') - if keyID and self.USE_GPG: - use_gpg_agent = gajim.config.get('use_gpg_agent') - if self.connected < 2 and self.gpg.passphrase is None and \ - not use_gpg_agent: - # We didn't set a passphrase - self.dispatch('ERROR', (_('OpenPGP passphrase was not given'), - #%s is the account name here - _('You will be connected to %s without OpenPGP.') % self.name)) - self.USE_GPG = False - elif self.gpg.passphrase is not None or use_gpg_agent: - signed = self.gpg.sign(msg, keyID) - if signed == 'BAD_PASSPHRASE': - self.USE_GPG = False - signed = '' - if self.connected < 2: - self.dispatch('BAD_PASSPHRASE', ()) - return signed - def _on_resolve_timeout(self): if self.connected: self.connection.resolve_all() @@ -197,8 +144,10 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): # callbacks called from zeroconf def _on_new_service(self, jid): self.roster.setItem(jid) - self.dispatch('ROSTER_INFO', (jid, self.roster.getName(jid), 'both', 'no', self.roster.getGroups(jid))) - self.dispatch('NOTIFY', (jid, self.roster.getStatus(jid), self.roster.getMessage(jid), 'local', 0, None, 0, None)) + self.dispatch('ROSTER_INFO', (jid, self.roster.getName(jid), 'both', 'no', + self.roster.getGroups(jid))) + self.dispatch('NOTIFY', (jid, self.roster.getStatus(jid), + self.roster.getMessage(jid), 'local', 0, None, 0, None)) def _on_remove_service(self, jid): self.roster.delItem(jid) @@ -206,15 +155,6 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): # keyID, timestamp, contact_nickname)) self.dispatch('NOTIFY', (jid, 'offline', '', 'local', 0, None, 0, None)) - def _on_disconnected(self): - self.disconnect() - self.dispatch('STATUS', 'offline') - self.dispatch('CONNECTION_LOST', - (_('Connection with account "%s" has been lost') % self.name, - _('To continue sending and receiving messages, you will need to reconnect.'))) - self.status = 'offline' - self.disconnect() - def _disconnectedReconnCB(self): '''Called when we are disconnected. Comes from network manager for example we don't try to reconnect, network manager will tell us when we can''' @@ -234,9 +174,10 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): self.dispatch('ZC_NAME_CONFLICT', alt_name) def _on_error(self, message): - self.dispatch('ERROR', (_('Avahi error'), _("%s\nLink-local messaging might not work properly.") % message)) + self.dispatch('ERROR', (_('Avahi error'), + _('%s\nLink-local messaging might not work properly.') % message)) - def connect(self, show = 'online', msg = ''): + def connect(self, show='online', msg=''): self.get_config_values_or_default() if not self.connection: self.connection = client_zeroconf.ClientZeroconf(self) @@ -267,10 +208,12 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): self.roster = self.connection.getRoster() self.dispatch('ROSTER', self.roster) - #display contacts already detected and resolved + # display contacts already detected and resolved for jid in self.roster.keys(): - self.dispatch('ROSTER_INFO', (jid, self.roster.getName(jid), 'both', 'no', self.roster.getGroups(jid))) - self.dispatch('NOTIFY', (jid, self.roster.getStatus(jid), self.roster.getMessage(jid), 'local', 0, None, 0, None)) + self.dispatch('ROSTER_INFO', (jid, self.roster.getName(jid), 'both', + 'no', self.roster.getGroups(jid))) + self.dispatch('NOTIFY', (jid, self.roster.getStatus(jid), + self.roster.getMessage(jid), 'local', 0, None, 0, None)) self.connected = STATUS_LIST.index(show) @@ -279,7 +222,7 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): gobject.timeout_add_seconds(5, self._on_resolve_timeout) return True - def disconnect(self, on_purpose = False): + def disconnect(self, on_purpose=False): self.connected = 0 self.time_to_reconnect = None if self.connection: @@ -291,15 +234,20 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): def reannounce(self): if self.connected: txt = {} - txt['1st'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_first_name') - txt['last'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_last_name') - txt['jid'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_jabber_id') - txt['email'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_email') + txt['1st'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'zeroconf_first_name') + txt['last'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'zeroconf_last_name') + txt['jid'] = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'zeroconf_jabber_id') + txt['email'] = gajim.config.get_per('accounts', + gajim.ZEROCONF_ACC_NAME, 'zeroconf_email') self.connection.reannounce(txt) def update_details(self): if self.connection: - port = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'custom_port') + port = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'custom_port') if port != self.port: self.port = port last_msg = self.connection.last_msg @@ -311,41 +259,18 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): else: self.reannounce() - def change_status(self, show, msg, sync = False, auto = False): - if not show in STATUS_LIST: - return -1 - self.status = show - - check = True #to check for errors from zeroconf - # 'connect' - if show != 'offline' and not self.connected: - if not self.connect(show, msg): - return - if show != 'invisible': - check = self.connection.announce() - else: - self.connected = STATUS_LIST.index(show) - self.dispatch('SIGNED_IN', ()) - - # 'disconnect' - elif show == 'offline' and self.connected: - self.disconnect() - self.time_to_reconnect = None - - # update status - elif show != 'offline' and self.connected: - was_invisible = self.connected == STATUS_LIST.index('invisible') + def connect_and_init(self, show, msg, sign_msg): + # to check for errors from zeroconf + check = True + if not self.connect(show, msg): + return + if show != 'invisible': + check = self.connection.announce() + else: self.connected = STATUS_LIST.index(show) - if show == 'invisible': - check = check and self.connection.remove_announce() - elif was_invisible: - if not self.connected: - check = check and self.connect(show, msg) - check = check and self.connection.announce() - if self.connection and not show == 'invisible': - check = check and self.connection.set_show_msg(show, msg) + self.dispatch('SIGNED_IN', ()) - #stay offline when zeroconf does something wrong + # stay offline when zeroconf does something wrong if check: self.dispatch('STATUS', show) else: @@ -356,136 +281,63 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): (_('Could not change status of account "%s"') % self.name, _('Please check if avahi-daemon is running.'))) - def get_status(self): - return STATUS_LIST[self.connected] + def _change_to_invisible(self, msg): + if self.connection.remove_announce(): + self.dispatch('STATUS', 'invisible') + else: + # show notification that avahi or system bus is down + self.dispatch('STATUS', 'offline') + self.status = 'offline' + self.dispatch('CONNECTION_LOST', + (_('Could not change status of account "%s"') % self.name, + _('Please check if avahi-daemon is running.'))) + + def _change_from_invisible(self): + self.connection.announce() + + def _update_status(self, show, msg): + if self.connection.set_show_msg(show, msg): + self.dispatch('STATUS', show) + else: + # show notification that avahi or system bus is down + self.dispatch('STATUS', 'offline') + self.status = 'offline' + self.dispatch('CONNECTION_LOST', + (_('Could not change status of account "%s"') % self.name, + _('Please check if avahi-daemon is running.'))) def send_message(self, jid, msg, keyID, type_='chat', subject='', chatstate=None, msg_id=None, composing_xep=None, resource=None, user_nick=None, xhtml=None, session=None, forward_from=None, form_node=None, original_message=None, delayed=None, callback=None, callback_args=[]): - fjid = jid - - if msg and not xhtml and gajim.config.get( - 'rst_formatting_outgoing_messages'): - from common.rst_xhtml_generator import create_xhtml - xhtml = create_xhtml(msg) - if not self.connection: - return - if not msg and chatstate is None: - return - - if self.status in ('invisible', 'offline'): - self.dispatch('MSGERROR', [unicode(jid), -1, - _('You are not connected or not visible to others. Your message ' - 'could not be sent.'), None, None, session]) - return - - msgtxt = msg - msgenc = '' - if keyID and self.USE_GPG: - if keyID == 'UNKNOWN': - error = _('Neither the remote presence is signed, nor a key was assigned.') - elif keyID.endswith('MISMATCH'): - error = _('The contact\'s key (%s) does not match the key assigned in Gajim.' % keyID[:8]) - else: - # encrypt - msgenc, error = self.gpg.encrypt(msg, [keyID]) - if msgenc and not error: - msgtxt = '[This message is encrypted]' - lang = os.getenv('LANG') - if lang is not None or lang != 'en': # we're not english - msgtxt = _('[This message is encrypted]') +\ - ' ([This message is encrypted])' # one in locale and one en - else: - # Encryption failed, do not send message - tim = time.localtime() - self.dispatch('MSGNOTSENT', (jid, error, msgtxt, tim, session)) - return - - if type_ == 'chat': - msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ=type_, - xhtml=xhtml) - - else: - if subject: - msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ='normal', - subject=subject, xhtml=xhtml) - else: - msg_iq = common.xmpp.Message(to=fjid, body=msgtxt, typ='normal', - xhtml=xhtml) - - if msgenc: - msg_iq.setTag(common.xmpp.NS_ENCRYPTED + ' x').setData(msgenc) - - # chatstates - if peer supports jep85 or jep22, send chatstates - # please note that the only valid tag inside a message containing a - # tag is the active event - if chatstate is not None: - if composing_xep == 'XEP-0085' or not composing_xep: - # JEP-0085 - msg_iq.setTag(chatstate, namespace = common.xmpp.NS_CHATSTATES) - if composing_xep == 'XEP-0022' or not composing_xep: - # JEP-0022 - chatstate_node = msg_iq.setTag('x', namespace = common.xmpp.NS_EVENT) - if not msgtxt: # when no , add - if not msg_id: # avoid putting 'None' in tag - msg_id = '' - chatstate_node.setTagData('id', msg_id) - # when msgtxt, requests JEP-0022 composing notification - if chatstate is 'composing' or msgtxt: - chatstate_node.addChild(name = 'composing') - - if forward_from: - addresses = msg_iq.addChild('addresses', - namespace=common.xmpp.NS_ADDRESS) - addresses.addChild('address', attrs = {'type': 'ofrom', - 'jid': forward_from}) - - # XEP-0203 - if delayed: - our_jid = gajim.get_jid_from_account(self.name) + '/' + \ - self.server_resource - timestamp = time.strftime('%Y-%m-%dT%TZ', time.gmtime(delayed)) - msg_iq.addChild('delay', namespace=common.xmpp.NS_DELAY2, - attrs={'from': our_jid, 'stamp': timestamp}) - - if session: - session.last_send = time.time() - msg_iq.setThread(session.thread_id) - - if session.enable_encryption: - msg_iq = session.encrypt_stanza(msg_iq) - - def on_send_ok(id): - no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for') - ji = gajim.get_jid_without_resource(jid) - if session.is_loggable() and self.name not in no_log_for and\ - ji not in no_log_for: - log_msg = msg - if subject: - log_msg = _('Subject: %(subject)s\n%(message)s') % \ - {'subject': subject, 'message': msg} - if log_msg: - if type_ == 'chat': - kind = 'chat_msg_sent' - else: - kind = 'single_msg_sent' - gajim.logger.write(kind, jid, log_msg) + def on_send_ok(msg_id): self.dispatch('MSGSENT', (jid, msg, keyID)) - if callback: - callback(id, *callback_args) + callback(msg_id, *callback_args) + + self.log_message(jid, msg, forward_from, session, original_message, + subject, type_) def on_send_not_ok(reason): reason += ' ' + _('Your message could not be sent.') self.dispatch('MSGERROR', [jid, -1, reason, None, None, session]) - ret = self.connection.send(msg_iq, msg is not None, on_ok=on_send_ok, - on_not_ok=on_send_not_ok) - if ret == -1: - # Contact Offline - self.dispatch('MSGERROR', [jid, -1, _('Contact is offline. Your message could not be sent.'), None, None, session]) + def cb(jid, msg, keyID, forward_from, session, original_message, subject, + type_, msg_iq): + ret = self.connection.send(msg_iq, msg is not None, on_ok=on_send_ok, + on_not_ok=on_send_not_ok) + + if ret == -1: + # Contact Offline + self.dispatch('MSGERROR', [jid, -1, _('Contact is offline. Your ' + 'message could not be sent.'), None, None, session]) + + self._prepare_message(jid, msg, keyID, type_=type_, subject=subject, + chatstate=chatstate, msg_id=msg_id, composing_xep=composing_xep, + resource=resource, user_nick=user_nick, xhtml=xhtml, session=session, + forward_from=forward_from, form_node=form_node, + original_message=original_message, delayed=delayed, callback=cb) def send_stanza(self, stanza): # send a stanza untouched @@ -495,95 +347,10 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): stanza = common.xmpp.Protocol(node=stanza) self.connection.send(stanza) - def ack_subscribed(self, jid): - gajim.log.debug('This should not happen (ack_subscribed)') - - def ack_unsubscribed(self, jid): - gajim.log.debug('This should not happen (ack_unsubscribed)') - - def request_subscription(self, jid, msg = '', name = '', groups = [], - auto_auth = False): - gajim.log.debug('This should not happen (request_subscription)') - - def send_authorization(self, jid): - gajim.log.debug('This should not happen (send_authorization)') - - def refuse_authorization(self, jid): - gajim.log.debug('This should not happen (refuse_authorization)') - - def unsubscribe(self, jid, remove_auth = True): - gajim.log.debug('This should not happen (unsubscribe)') - - def unsubscribe_agent(self, agent): - gajim.log.debug('This should not happen (unsubscribe_agent)') - - def update_contact(self, jid, name, groups): - if self.connection: - self.connection.getRoster().setItem(jid = jid, name = name, - groups = groups) - - def update_contacts(self, contacts): - '''update multiple roster items''' - if self.connection: - self.connection.getRoster().setItemMulti(contacts) - - def new_account(self, name, config, sync = False): - gajim.log.debug('This should not happen (new_account)') - - def _on_new_account(self, con = None, con_type = None): - gajim.log.debug('This should not happen (_on_new_account)') - - def account_changed(self, new_name): - self.name = new_name - - def request_last_status_time(self, jid, resource): - gajim.log.debug('This should not happen (request_last_status_time)') - - def request_os_info(self, jid, resource): - gajim.log.debug('This should not happen (request_os_info)') - - def get_settings(self): - gajim.log.debug('This should not happen (get_settings)') - - def get_bookmarks(self): - gajim.log.debug('This should not happen (get_bookmarks)') - - def store_bookmarks(self): - gajim.log.debug('This should not happen (store_bookmarks)') - - def get_metacontacts(self): - gajim.log.debug('This should not happen (get_metacontacts)') - - def send_agent_status(self, agent, ptype): - gajim.log.debug('This should not happen (send_agent_status)') - - def gpg_passphrase(self, passphrase): - if self.gpg: - use_gpg_agent = gajim.config.get('use_gpg_agent') - if use_gpg_agent: - self.gpg.passphrase = None - else: - self.gpg.passphrase = passphrase - - def ask_gpg_keys(self): - if self.gpg: - keys = self.gpg.get_keys() - return keys - return None - - def ask_gpg_secrete_keys(self): - if self.gpg: - keys = self.gpg.get_secret_keys() - return keys - return None - def _event_dispatcher(self, realm, event, data): + CommonConnection._event_dispatcher(self, realm, event, data) if realm == '': - if event == common.xmpp.transports_nb.DATA_RECEIVED: - self.dispatch('STANZA_ARRIVED', unicode(data, errors = 'ignore')) - elif event == common.xmpp.transports_nb.DATA_SENT: - self.dispatch('STANZA_SENT', unicode(data)) - elif event == common.xmpp.transports.DATA_ERROR: + if event == common.xmpp.transports_nb.DATA_ERROR: thread_id = data[1] frm = unicode(data[0]) session = self.get_or_create_session(frm, thread_id) @@ -591,9 +358,6 @@ class ConnectionZeroconf(ConnectionHandlersZeroconf): _('Connection to host could not be established: Timeout while ' 'sending data.'), None, None, session]) - def load_roster_from_db(self): - return - # END ConnectionZeroconf # vim: se ts=3: From 86b39a72c6efc9ea7cab56e174af7d962509083b Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 18 Nov 2009 11:12:06 +0100 Subject: [PATCH 30/73] fix http message parsing, it may contain \n\n! --- src/common/xmpp/transports_nb.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index dd60269bf..1aac5b52d 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -651,7 +651,7 @@ class NonBlockingHTTP(NonBlockingTCP): self.recvbuff = '%s%s' % (self.recvbuff or '', data) statusline, headers, httpbody, buffer_rest = self.parse_http_message( self.recvbuff) - + if not (statusline and headers and httpbody): log.debug('Received incomplete HTTP response') return @@ -663,12 +663,12 @@ class NonBlockingHTTP(NonBlockingTCP): self.expected_length = int(headers['Content-Length']) if 'Connection' in headers and headers['Connection'].strip()=='close': self.close_current_connection = True - + if self.expected_length > len(httpbody): # If we haven't received the whole HTTP mess yet, let's end the thread. # It will be finnished from one of following recvs on plugged socket. - log.info('not enough bytes in HTTP response - %d expected, %d got' % - (self.expected_length, len(self.recvbuff))) + log.info('not enough bytes in HTTP response - %d expected, got %d' % + (self.expected_length, len(httpbody))) else: # First part of buffer has been extraced and is going to be handled, # remove it from buffer @@ -720,6 +720,7 @@ class NonBlockingHTTP(NonBlockingTCP): http_rest - what is left in the message after a full HTTP header + body ''' message = message.replace('\r','') + message = message.lstrip('\n') splitted = message.split('\n\n') if len(splitted) < 2: # no complete http message. Keep filling the buffer until we find one @@ -727,9 +728,6 @@ class NonBlockingHTTP(NonBlockingTCP): return ('', '', '', buffer_rest) else: (header, httpbody) = splitted[:2] - if httpbody.endswith('\n'): - httpbody = httpbody[:-1] - buffer_rest = "\n\n".join(splitted[2:]) header = header.split('\n') statusline = header[0].split(' ', 2) header = header[1:] @@ -737,6 +735,12 @@ class NonBlockingHTTP(NonBlockingTCP): for dummy in header: row = dummy.split(' ', 1) headers[row[0][:-1]] = row[1] + body_size = headers['Content-Length'] + rest_splitted = splitted[2:] + while (len(httpbody) < body_size) and rest_splitted: + # Complete httpbody until it has the announced size + httpbody = '\n\n'.join([httpbody, rest_splitted.pop(0)]) + buffer_rest = "\n\n".join(rest_splitted) return (statusline, headers, httpbody, buffer_rest) From 4671f62d2fd7d54e9501ff5307a07847e976772d Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 18 Nov 2009 11:16:15 +0100 Subject: [PATCH 31/73] fix ConnectionZeroconf initialization --- src/common/zeroconf/connection_zeroconf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common/zeroconf/connection_zeroconf.py b/src/common/zeroconf/connection_zeroconf.py index 52876c8d6..e1cc30971 100644 --- a/src/common/zeroconf/connection_zeroconf.py +++ b/src/common/zeroconf/connection_zeroconf.py @@ -53,7 +53,6 @@ from connection_handlers_zeroconf import * class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf): '''Connection class''' def __init__(self, name): - CommonConnection.__init__(self, name) ConnectionHandlersZeroconf.__init__(self) # system username self.username = None @@ -64,7 +63,7 @@ class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf): self.password = 'zeroconf' self.autoconnect = False - self.get_config_values_or_default() + CommonConnection.__init__(self, name) def get_config_values_or_default(self): ''' get name, host, port from config, or From 1b22a33239e99592b4e2b27df68315605d28dbc4 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 18 Nov 2009 11:22:48 +0100 Subject: [PATCH 32/73] add a HTML message parsser test --- test/integration/test_xmpp_transports_nb.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/integration/test_xmpp_transports_nb.py b/test/integration/test_xmpp_transports_nb.py index 6e56ddf94..ef9908903 100644 --- a/test/integration/test_xmpp_transports_nb.py +++ b/test/integration/test_xmpp_transports_nb.py @@ -254,12 +254,12 @@ class TestNonBlockingHTTP(AbstractTransportTest): def test_receive_http_message_in_chunks(self): ''' Let _on_receive handle some chunked http messages ''' transport = self._get_transport(self.bosh_http_dict) - - header = ("HTTP/1.1 200 OK\r\nContent-Type: text/xml; charset=utf-8\r\n" + - "Content-Length: 88\r\n\r\n") - payload = "Please don't fail!" + + payload = "Please don't fail!\n\n" body = "%s" \ % payload + header = "HTTP/1.1 200 OK\r\nContent-Type: text/xml; charset=utf-8\r\n" +\ + "Content-Length: %i\r\n\r\n" % len(body) message = "%s%s" % (header, body) chunk1, chunk2, chunk3, chunk4 = message[:20], message[20:73], \ From c9c5f72ff9abb0c889edef20c0f5e7a20bf60e0a Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 18 Nov 2009 21:07:11 +0100 Subject: [PATCH 33/73] fix traceback on startup. Fixes #5435 --- src/common/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/connection.py b/src/common/connection.py index 3cd3acd43..ee79c1ddf 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -623,7 +623,7 @@ class Connection(CommonConnection, ConnectionHandlers): self.secret_hmac = str(random.random())[2:] # END __init__ - def get_config_values_or_default(): + def get_config_values_or_default(self): if gajim.config.get_per('accounts', self.name, 'keep_alives_enabled'): self.keepalives = gajim.config.get_per('accounts', self.name, 'keep_alive_every_foo_secs') From 750fbc844dafd82a403a0694b4bfa9f342948361 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 18 Nov 2009 21:32:10 +0100 Subject: [PATCH 34/73] [Urcher] ability to copy emoticons when they are selected. Fixes #2570 --- src/conversation_textview.py | 6 ++++-- src/htmltextview.py | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/conversation_textview.py b/src/conversation_textview.py index 837e2a0ce..fa5dcd25e 100644 --- a/src/conversation_textview.py +++ b/src/conversation_textview.py @@ -67,13 +67,15 @@ def has_focus(widget): class TextViewImage(gtk.Image): - def __init__(self, anchor): + def __init__(self, anchor, text): super(TextViewImage, self).__init__() self.anchor = anchor self._selected = False self._disconnect_funcs = [] self.connect('parent-set', self.on_parent_set) self.connect('expose-event', self.on_expose) + self.set_tooltip_text(text) + self.anchor.set_data('plaintext', text) def _get_selected(self): parent = self.get_parent() @@ -1043,7 +1045,7 @@ class ConversationTextview(gobject.GObject): emot_ascii = possible_emot_ascii_caps end_iter = buffer_.get_end_iter() anchor = buffer_.create_child_anchor(end_iter) - img = TextViewImage(anchor) + img = TextViewImage(anchor, special_text) animations = gajim.interface.emoticons_animations if not emot_ascii in animations: animations[emot_ascii] = gtk.gdk.PixbufAnimation( diff --git a/src/htmltextview.py b/src/htmltextview.py index 36f3ac131..5c12a049c 100644 --- a/src/htmltextview.py +++ b/src/htmltextview.py @@ -804,6 +804,10 @@ class HtmlTextView(gtk.TextView): self.connect('motion-notify-event', self.__motion_notify_event) self.connect('leave-notify-event', self.__leave_event) self.connect('enter-notify-event', self.__motion_notify_event) + self.connect('realize', self.on_html_text_view_realized) + self.connect('unrealize', self.on_html_text_view_unrealized) + self.connect('copy-clipboard', self.on_html_text_view_copy_clipboard) + self.get_buffer().connect_after('mark-set', self.on_text_buffer_mark_set) self.get_buffer().create_tag('eol', scale = pango.SCALE_XX_SMALL) self.tooltip = tooltips.BaseTooltip() self.config = gajim.config @@ -873,7 +877,43 @@ class HtmlTextView(gtk.TextView): #if not eob.starts_line(): # buffer_.insert(eob, '\n') + def on_html_text_view_copy_clipboard(self, unused_data): + clipboard = self.get_clipboard(gtk.gdk.SELECTION_CLIPBOARD) + clipboard.set_text(self.get_selected_text()) + self.emit_stop_by_name('copy-clipboard') + def on_html_text_view_realized(self, unused_data): + self.get_buffer().remove_selection_clipboard(self.get_clipboard(gtk.gdk.SELECTION_PRIMARY)) + + def on_html_text_view_unrealized(self, unused_data): + self.get_buffer().add_selection_clipboard(self.get_clipboard(gtk.gdk.SELECTION_PRIMARY)) + + def on_text_buffer_mark_set(self, location, mark, unused_data): + bounds = self.get_buffer().get_selection_bounds() + if bounds: + clipboard = self.get_clipboard(gtk.gdk.SELECTION_PRIMARY) + clipboard.set_text(self.get_selected_text()) + + def get_selected_text(self): + bounds = self.get_buffer().get_selection_bounds() + selection = '' + if bounds: + (search_iter, end) = bounds + + while (search_iter.compare(end)): + character = search_iter.get_char() + if character == u'\ufffc': + anchor = search_iter.get_child_anchor() + if anchor: + text = anchor.get_data('plaintext') + if text: + selection+=text + else: + selection+=character + else: + selection+=character + search_iter.forward_char() + return selection change_cursor = None From 909ef8da53e25b956cca6161645781997b317cca Mon Sep 17 00:00:00 2001 From: red-agent Date: Thu, 19 Nov 2009 07:39:04 +0200 Subject: [PATCH 35/73] Added /grep command. Fixes #5438 --- src/command_system/implementation/standard.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/command_system/implementation/standard.py b/src/command_system/implementation/standard.py index 8afa044e5..3b46a30db 100644 --- a/src/command_system/implementation/standard.py +++ b/src/command_system/implementation/standard.py @@ -17,10 +17,14 @@ Provides an actual implementation for the standard commands. """ +from time import localtime, strftime +from datetime import date + import dialogs from common import gajim from common import helpers from common.exceptions import GajimGeneralException +from common.logger import Constants from ..errors import CommandError from ..framework import CommandContainer, command, documentation @@ -28,6 +32,10 @@ from ..mapping import generate_usage from hosts import ChatCommands, PrivateChatCommands, GroupChatCommands +# This holds constants fron the logger, which we'll be using in some of our +# commands. +lc = Constants() + class StandardCommonCommands(CommandContainer): """ This command container contains standard commands which are common to all - @@ -84,6 +92,42 @@ class StandardCommonCommands(CommandContainer): def me(self, action): self.send("/me %s" % action) + @command('lastlog', overlap=True) + @documentation(_("Show logged messages which mention given text")) + def grep(self, text, limit=None): + results = gajim.logger.get_search_results_for_query(self.contact.jid, + text, self.account) + + if not results: + raise CommandError(_("%s: Nothing found") % text) + + if limit: + try: + results = results[len(results) - int(limit):] + except ValueError: + raise CommandError(_("Limit must be an integer")) + + for row in results: + contact, time, kind, show, message, subject = row + + if not contact: + if kind == lc.KIND_CHAT_MSG_SENT: + contact = gajim.nicks[self.account] + else: + contact = self.contact.name + + time_obj = localtime(time) + date_obj = date.fromtimestamp(time) + date_ = strftime('%Y-%m-%d', time_obj) + time_ = strftime('%H:%M:%S', time_obj) + + if date_obj == date.today(): + formatted = "[%s] %s: %s" % (time_, contact, message) + else: + formatted = "[%s, %s] %s: %s" % (date_, time_, contact, message) + + self.echo(formatted) + class StandardChatCommands(CommandContainer): """ This command container contains standard command which are unique to a chat. From d664daad1a715bb5fe8e1eff7aac7089a5520411 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Thu, 19 Nov 2009 20:36:40 +0100 Subject: [PATCH 36/73] we can now send pep thing to a zeroconf connection objec, it will just send nothing. so GUI doesn't have to know it's a zeroconf connection or not. fixes #5432 --- src/common/zeroconf/connection_handlers_zeroconf.py | 3 ++- src/gui_interface.py | 2 -- src/roster_window.py | 6 +----- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/common/zeroconf/connection_handlers_zeroconf.py b/src/common/zeroconf/connection_handlers_zeroconf.py index b08fba1ce..4ea0e65bb 100644 --- a/src/common/zeroconf/connection_handlers_zeroconf.py +++ b/src/common/zeroconf/connection_handlers_zeroconf.py @@ -35,6 +35,7 @@ from common import helpers from common import gajim from common.zeroconf import zeroconf from common.commands import ConnectionCommands +from common.pep import ConnectionPEP import logging log = logging.getLogger('gajim.c.z.connection_handlers_zeroconf') @@ -376,7 +377,7 @@ class ConnectionBytestream(connection_handlers.ConnectionBytestream): raise common.xmpp.NodeProcessed class ConnectionHandlersZeroconf(ConnectionVcard, ConnectionBytestream, -ConnectionCommands, connection_handlers.ConnectionHandlersBase): +ConnectionCommands, ConnectionPEP, connection_handlers.ConnectionHandlersBase): def __init__(self): ConnectionVcard.__init__(self) ConnectionBytestream.__init__(self) diff --git a/src/gui_interface.py b/src/gui_interface.py index 9cb53d646..98c1f3298 100644 --- a/src/gui_interface.py +++ b/src/gui_interface.py @@ -2812,8 +2812,6 @@ class Interface: for acct in accounts: if not gajim.account_is_connected(acct): continue - if gajim.connections[acct].is_zeroconf: - continue if not gajim.config.get_per('accounts', acct, 'publish_tune'): continue if gajim.connections[acct].music_track_info == music_track_info: diff --git a/src/roster_window.py b/src/roster_window.py index 25780f85c..6e3bcdc2d 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1898,9 +1898,7 @@ class RosterWindow: def send_pep(self, account, pep_dict): connection = gajim.connections[account] - if connection.is_zeroconf: - return - + if 'activity' in pep_dict: activity = pep_dict['activity'] subactivity = pep_dict.get('subactivity', None) @@ -1917,8 +1915,6 @@ class RosterWindow: connection.retract_mood() def delete_pep(self, jid, account): - if gajim.connections[account].is_zeroconf: - return if jid == gajim.get_jid_from_account(account): gajim.connections[account].pep = {} self.draw_account(account) From 8720eb221d1e767cab60b63336949c8fb89f3406 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Thu, 19 Nov 2009 22:13:16 +0100 Subject: [PATCH 37/73] don't propose to add contacts we already have in our roster when we get a RIE request. --- src/common/connection_handlers.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index cb9197d5e..15a979415 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1860,9 +1860,23 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, log.warn('Invalid JID: %s, ignoring it' % item.getAttr('jid')) continue name = item.getAttr('name') - groups=[] + contact = gajim.contact.get_contact(self.name, jid) + groups = [] + same_groups = True for group in item.getTags('group'): groups.append(group.getData()) + # check that all suggested groups are in the groups we have for this + # contact + if not contact or group not in contact.groups: + same_groups = False + if contact: + # check that all groups we have for this contact are in the + # suggested groups + for group in contact.groups: + if group not in groups: + same_groups = False + if contact.subscription in ('both', 'to') and same_groups: + continue exchange_items_list[jid] = [] exchange_items_list[jid].append(name) exchange_items_list[jid].append(groups) From 094941f89e8bc57354e2a6019d10908d341094f0 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Thu, 19 Nov 2009 22:42:35 +0100 Subject: [PATCH 38/73] don't try to send thing after we are disconnected. Fixes #5437 --- src/common/connection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/connection.py b/src/common/connection.py index ee79c1ddf..3c8cf537d 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -553,6 +553,7 @@ class CommonConnection: self.connect_and_init(show, msg, sign_msg) elif show == 'offline': + self.connected = 0 if self.connection: p = common.xmpp.Presence(typ = 'unavailable') p = self.add_sha(p, False) From bafca7579fb94c84f094a02eaf7b46681cd69899 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Fri, 20 Nov 2009 13:49:56 +0100 Subject: [PATCH 39/73] [Dmitry Korzhevin] new tango emoticon set. Fixes #5421 --- THANKS.artists | 2 +- data/emoticons/tango/alien.png | Bin 0 -> 1452 bytes data/emoticons/tango/angel.png | Bin 0 -> 1587 bytes data/emoticons/tango/arrogant.png | Bin 0 -> 1307 bytes data/emoticons/tango/beer.png | Bin 0 -> 1382 bytes data/emoticons/tango/bomb.png | Bin 0 -> 1110 bytes data/emoticons/tango/clap.png | Bin 0 -> 1334 bytes data/emoticons/tango/confused.png | Bin 0 -> 1280 bytes data/emoticons/tango/cowboy.png | Bin 0 -> 1490 bytes data/emoticons/tango/crying.png | Bin 0 -> 1319 bytes data/emoticons/tango/curl-lip.png | Bin 0 -> 1269 bytes data/emoticons/tango/cute.png | Bin 0 -> 1275 bytes data/emoticons/tango/dance.png | Bin 0 -> 1359 bytes data/emoticons/tango/devil.png | Bin 0 -> 1457 bytes data/emoticons/tango/disapointed.png | Bin 0 -> 1225 bytes data/emoticons/tango/doh.png | Bin 0 -> 1284 bytes data/emoticons/tango/dont-know.png | Bin 0 -> 1265 bytes data/emoticons/tango/embarrassed.png | Bin 0 -> 1284 bytes data/emoticons/tango/emoticons.py | 52 +++++++++++++++++++++++ data/emoticons/tango/fingers-crossed.png | Bin 0 -> 1373 bytes data/emoticons/tango/freaked-out.png | Bin 0 -> 1236 bytes data/emoticons/tango/giggle.png | Bin 0 -> 1305 bytes data/emoticons/tango/glasses-cool.png | Bin 0 -> 1302 bytes data/emoticons/tango/go-away.png | Bin 0 -> 1324 bytes data/emoticons/tango/good.png | Bin 0 -> 1011 bytes data/emoticons/tango/handshake.png | Bin 0 -> 1390 bytes data/emoticons/tango/hypnotized.png | Bin 0 -> 1315 bytes data/emoticons/tango/kiss.png | Bin 0 -> 1345 bytes data/emoticons/tango/laugh.png | Bin 0 -> 1250 bytes data/emoticons/tango/love.png | Bin 0 -> 1069 bytes data/emoticons/tango/mail.png | Bin 0 -> 881 bytes data/emoticons/tango/musical-note.png | Bin 0 -> 941 bytes data/emoticons/tango/party.png | Bin 0 -> 1488 bytes data/emoticons/tango/pissed-off.png | Bin 0 -> 1342 bytes data/emoticons/tango/question.png | Bin 0 -> 1491 bytes data/emoticons/tango/rose.png | Bin 0 -> 1062 bytes data/emoticons/tango/rotfl.png | Bin 0 -> 1318 bytes data/emoticons/tango/sad.png | Bin 0 -> 1283 bytes data/emoticons/tango/sarcastic.png | Bin 0 -> 1274 bytes data/emoticons/tango/shout.png | Bin 0 -> 1288 bytes data/emoticons/tango/shut-mouth.png | Bin 0 -> 1306 bytes data/emoticons/tango/sick.png | Bin 0 -> 1309 bytes data/emoticons/tango/silly.png | Bin 0 -> 1266 bytes data/emoticons/tango/sleepy.png | Bin 0 -> 1400 bytes data/emoticons/tango/smirk.png | Bin 0 -> 1256 bytes data/emoticons/tango/terror.png | Bin 0 -> 1259 bytes data/emoticons/tango/thinking.png | Bin 0 -> 1282 bytes data/emoticons/tango/tongue.png | Bin 0 -> 1260 bytes data/emoticons/tango/tremble.png | Bin 0 -> 1298 bytes data/emoticons/tango/victory.png | Bin 0 -> 1387 bytes data/emoticons/tango/wink.png | Bin 0 -> 1276 bytes 51 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 data/emoticons/tango/alien.png create mode 100644 data/emoticons/tango/angel.png create mode 100644 data/emoticons/tango/arrogant.png create mode 100644 data/emoticons/tango/beer.png create mode 100644 data/emoticons/tango/bomb.png create mode 100644 data/emoticons/tango/clap.png create mode 100644 data/emoticons/tango/confused.png create mode 100644 data/emoticons/tango/cowboy.png create mode 100644 data/emoticons/tango/crying.png create mode 100644 data/emoticons/tango/curl-lip.png create mode 100644 data/emoticons/tango/cute.png create mode 100644 data/emoticons/tango/dance.png create mode 100644 data/emoticons/tango/devil.png create mode 100644 data/emoticons/tango/disapointed.png create mode 100644 data/emoticons/tango/doh.png create mode 100644 data/emoticons/tango/dont-know.png create mode 100644 data/emoticons/tango/embarrassed.png create mode 100644 data/emoticons/tango/emoticons.py create mode 100644 data/emoticons/tango/fingers-crossed.png create mode 100644 data/emoticons/tango/freaked-out.png create mode 100644 data/emoticons/tango/giggle.png create mode 100644 data/emoticons/tango/glasses-cool.png create mode 100644 data/emoticons/tango/go-away.png create mode 100644 data/emoticons/tango/good.png create mode 100644 data/emoticons/tango/handshake.png create mode 100644 data/emoticons/tango/hypnotized.png create mode 100644 data/emoticons/tango/kiss.png create mode 100644 data/emoticons/tango/laugh.png create mode 100644 data/emoticons/tango/love.png create mode 100644 data/emoticons/tango/mail.png create mode 100644 data/emoticons/tango/musical-note.png create mode 100644 data/emoticons/tango/party.png create mode 100644 data/emoticons/tango/pissed-off.png create mode 100644 data/emoticons/tango/question.png create mode 100644 data/emoticons/tango/rose.png create mode 100644 data/emoticons/tango/rotfl.png create mode 100644 data/emoticons/tango/sad.png create mode 100644 data/emoticons/tango/sarcastic.png create mode 100644 data/emoticons/tango/shout.png create mode 100644 data/emoticons/tango/shut-mouth.png create mode 100644 data/emoticons/tango/sick.png create mode 100644 data/emoticons/tango/silly.png create mode 100644 data/emoticons/tango/sleepy.png create mode 100644 data/emoticons/tango/smirk.png create mode 100644 data/emoticons/tango/terror.png create mode 100644 data/emoticons/tango/thinking.png create mode 100644 data/emoticons/tango/tongue.png create mode 100644 data/emoticons/tango/tremble.png create mode 100644 data/emoticons/tango/victory.png create mode 100644 data/emoticons/tango/wink.png diff --git a/THANKS.artists b/THANKS.artists index 5cdff9134..7f8f24c99 100644 --- a/THANKS.artists +++ b/THANKS.artists @@ -1,10 +1,10 @@ Anders Ström Christophe Got Dennis Craven +Dmitry Korzhevin Guillaume Morin Gvorcek Spajreh Josef Vybíral Membris Khan Rederick Asher Jakub Szypulka - diff --git a/data/emoticons/tango/alien.png b/data/emoticons/tango/alien.png new file mode 100644 index 0000000000000000000000000000000000000000..ca83124f833b1e8ad9e76cfcdcd2b950a375d6c3 GIT binary patch literal 1452 zcmV;d1ylNoP)41R~3f8wa+>CX~&LVX?krZvJ>~=M95T5o3@ca1tl#}jUrJ6iAe<>BbXtWFesBE zF{*@Oh{{7kA{D7ykw(x`wITt>PNGs*b!glQJ9gsQv7PICeeQYey%^l2q_$JBq&-<% z>;LxFzxFx@Rb}IP_C%(zRsM_CvnMj%R(ZpQICe6B3aB1Cng7kky`hn;42@)Ei<;j9 z$+469lN7P5NlFelRTA9X2~geYBS$MzD>AO4YdrScZ=!6vPZ zCLhY@3gH6{Pq7+ZCrM*Gas*N&kRmy=jl5|jXBx>!k&G14)KH0*d7$Ab^0`6?9NH2v zG?F#OGcO%Ea6G?mS8&c@@bC%n$PkbtbLLfoKc6Ne4Fu%zC5ul6QIDD}bL7DByz$IS zLnB$U2>=`~we*y_+V^lPTErNG5ud^vXUU#<6;A@>!rzebxA4j1zwrv0GoygPh)0;L z(cRHUsimg`9Nz>mo*eDl-CroDHvoro4#klhA0sn<4yO*d{eZBT3vZJdKZnSjHWw=C z5`DY-3&xY9n@kpwfsSpRgtiREqH0kr)Mc>}VGn!-K}fZVht~6Iv6|vEC3Yzt+d2`E zfzJVkMzSJ8PisRbv&jS=0iGnJ8dDt{zz~q?I_{aHpdLa6Gx!*mlvHCJF%C;=YjiX` zgtM+^Xe2A6!;#+Ars7aVCBdpA)eMFjD^EXJH&iR68sLeCa~Avb*GZ|7YDh|iCB~70 zYF!Kcp9iDEks41X7b|lFk|))Olp0sYZxFjGk*gBv8Zm2xdW(oPA}y0>l@BN8NHiqT z5Z?<}tju9NnH(LC^m8U#>)u^hnQ=Lo?V)LVRm7ooFFdAUQug@at_BpkvSgghry*9f1L4I}Sg<6z^kB<&Vm$!P!j1EVa z>vcK=^&=r5_!Rb$NZzsAN^^)As(fIG2w?_R&y-rnFM*JRKJC*+JF z*tJ{$0000B;sx}fO55=Zc5UD6ue6T_v zEEG{HDr&%&q|ss`MD#&ulhmlwrfHbOTxM*frp;W>WzL+l_dffwJ|AW#sTnJNu-NNm z{eSEG|JS!gRavfyD}TIWZszrUBKB_IFR!zH*P!*g`m8Oj@@%PKZP6Ix<1}?y+KIC) zYG-LMpQb@IO@k>T-nqdw_njz~hR6H*1{MHBRXKKKj}QDfm+SfB>fu6ZV6aHO&`U1Y zkM;ZU>}re*fb^qU1R_*xxU5Q=R!QO-ov2Q|7Eqt-w3DR$ViZn4@yPyD&K^6mM}S`q zt!eDvdGB?ScrEqXM$*J5j!TGiV@x;JcH`%|$rlEQJ5|yoA=^Nz~qX{48_MdvnF6Dc0 zW~#UKrxU{mRuAWUR}VP~g@9bXj`bbZ5Ae)9Vv2x=a}MVsvb05-G)dw%ohaq%-v_A8 zbvMFr<`AUcyczOx%{|lm;b3^fYvFyL&!tI&j4qH>ASFcdARb0MKoN1E5omxA;QM6? zuOE+#rvFm|2VQ!0S@Ra)QSsVa7?Vez-r@$jC5~pn8wN9onm)v=LMZ?iBN?h8xCL-^ zaI^Ti5~kKr{q-}K%a_~^exn(HStbP|?3E8}-(c2ml1_URAp|<8c6j;pTbw*IjtGdc zoc`-LC(n$7c2JFgh$Os(S+icuom)2qBkWz)y$L|LM@F_6lJE_14b&}wTR>>>@}DO- zedY>i0ItpHzg*<>nJWk(LL0OVS(Pk2kBn?DAl$PoYq_<1K!z*ECFe1c;oLmN6j7IB ze!hY4=OC+tc+~4PvMfbiW6`2)1|`BJv)JK^QOg4>0Ftsp`YL4gKY$8iV$?MOVdqxx ztcSQs9F`rMeX=Y=vq^AGL?c8K(sY`9-(DnT$I1?Fde*_gqPH$U#PZm_UXT)+O<|13 zfzR{+pDnF-}S6T#=pVQ0QX+4{rcs?lfe1$^a0niZ9 zI_m1+YD=rW7`SUw=>6=X_=)EO?X;eKulR}%0QjDm>CpRbSGo8N@8N?*S+`lz;1n*t zMi##XfS3CqCU+OMv(ogIB%x*njK`R~5d%wM{;P`dcjtLtA0oT;hN zhB2l#HagnS_lT~vXsIfditQR4EW3{DF=*Kd1ta1+amJ`|3NhjWfu?n?34~|Qp3QF8 ld0)VPD-n_9z5bsE{|({E33t-kYh?fc002ovPDHLkV1h!+3a|hG literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/arrogant.png b/data/emoticons/tango/arrogant.png new file mode 100644 index 0000000000000000000000000000000000000000..42b85ff14c9468acfb701b4a1201041c204db0c7 GIT binary patch literal 1307 zcmV+$1?2jPP)WiB(v?QK647O`P>S71X(7-oqzj>TQTjo# z6fAVnQba8D!`Mv~ieNyrjZxb)hUl1Rg2_x`=FOXV@7>cyw0^{h8!sHVaJl~<|8wp= zM^u$(xy&hzVs&t+%*~-|>a#0zNUuB{;6pK`kWPsux^#O-irfkbgGsCk zNSv9hR&WFycOK*iR8Dq#8u$m(E{8av_CdK>H7JF2l56T>@4j@Dt&?hHoJf=edkT~Uv%h~fq zdM|q%{`LZ5YPrxi&87Y_$9}y{|J9I43h4HZlq5LDZgFM-yjFIQZEhmhBvpR|5d-3) zI3yEso^P1Xm*1SFv9SRq&6?Fq`02MiR|f8o%_a$|laO03WSg73R(9Oez5h&w&C=4G z464@wi>d{+h_Q$$E1K*0>XSV{67Y%F2p{aEDg_E`=j zWWCleLH$cW95GeY`e4dHj3eLuOV9b+v~OL;dvC9$z80?Z-{SKl1AKa*5zzn}c~IZR zy$|`Yga3c(FlYpc8sW`74WI=y54mN0G-CWdFUW;qBs&Dh8mQ(I@E zU>H${nLn;wE<8;wM;Cv0rGj}=FqiNKPP58II5BQ?2?n+o@3f@VvE2|&g9(| z;BJVWaiLKGp$N8!aqDo+D&%Chr#`zLY~K&{qRb(^qJe)Ad##*30qf!04Yp-ux zmhoTAW-9CJoG=G49>s=lOvNt`-bwWx{wH_Jvt~k#7L73zy*)ieeL!^1k-2KsDmT4g zLB>|>a|X>=Az?&fR;)2;b;(t|eTT71+ Ry4e5#002ovPDHLkV1hKzV>JK( literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/beer.png b/data/emoticons/tango/beer.png new file mode 100644 index 0000000000000000000000000000000000000000..e955770a388b2a40e5426c96f07a337b9a4527b9 GIT binary patch literal 1382 zcmV-s1)2JZP)M z*zfnPsI08yUT+>`MFBtn160+8!ongntZLwa(m;jF?Y@2X{FycYY+1nRbEj8xS^m!D zQdEz}4M7fsAdnCsA_zeM1ONd7Ls_5VUidlb$;T^1k+ua^e`a$|iHBs+!R*eDXoBrMwo2p}SuW)|T{1b6P- zfDs$RvrTK^_xeDBRy+wHKLU_PQB~aP{Sj+7>_$5HJHn|560tZY3=2XKd^(3u&7z@{ z;I7<)fg3*~zoG$-aJ=p3PiHun2OY=y?BF}^Us#|3f?QnU$RuO9{?!W*lf4LNNz|<@ zz?Kagv2EifG}Hu8=5-(?2GMhB7eWtigE5Aeo_`TLwzj&~G&D6zl63au`IB1~D4^M- zRMl-pMvo));5y>{9gE6lYCcM;pM$4<2LRw|cb6zBEKXNHRpY6zt<&$^yZbqS4LJbD zm>>Zd@i6i$TOe1hL#q1&XpM(K)-=emz_cQmw@yHBIRXV5!tp8i-NkSmfr(J)xM^93 zo9Y_<1k^0Fh(KV3RD2SW9R+2h01+UvV2U4-Rtz>9cae!f%1$8>9e?Dog5&Xc+@-lR z2qFA!ZEbR{MGOQY7>Ni(o;>$*gc$>qlz9db$c#cTj#S*9Gr$8+Dg#3X2mq+Oyqx7) zG?PSFnFK6@pl~*`SvCe@xh;##C@hD;O3yuDmbQ~`y7fFGok{y-Nt!tD-a#t|00uz> zWX%+^h6!1n1(-2_&Fd&c5Sa)}6Udr60DyDu=$iYBhShbxNGwWBlKyzO{E-4;1V-Sp z3(?2~v_d}XcQh9O0Wu>9hHS`M9ROhG_8lC6FE|;(-NAvBBk1Tt0ApF(b`Yp;#=tLq zDDy}&mz<3ur&m4zAN~%=K_q8FpOjz3=Hl(Kk^EmM$4;jqpEHjhzLXX|4P}m z{rzH_&sab2!|C{@z0;rXe{D^DlddZ2V(aJ1c5L+D?l*&>$?mT^FYf8NeA!qM(AwH6 zd5ekyyLN8>Qq#0IR#aB7veGiet-Ik;T_6H7nKX>FflzqT9vK^Prl%9vE?&IyNqjmU zWD1MsYg)9ct1CxIPQV|Gj%M52zx}wjwz^~8y5@I>yni=wE|p3W%L4$%5r$>kk!U13 z(A#t4hq3YTVPaDDVFXG_N|v?=0M@Tx&oip3amUfgW*(B@R%Mn)#9X$pEk-;8Aq+?& o17q-U-X<)U2*H(mjD0&07*qoM6N<$g8A!tg#Z8m literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/bomb.png b/data/emoticons/tango/bomb.png new file mode 100644 index 0000000000000000000000000000000000000000..9f28ff03b898bae02cea32cb874bfed608cb1106 GIT binary patch literal 1110 zcmV-c1gZOpP);i0bnz*2z;sK_G&ZC0 zo*B_TIM4gpgc@)ycU1Z72Aus$4x!kx=PllazmwfVvg6NK*=DomBf04<$&D%g3m`c~gJ?$g_J2 zxj2sST$g&ij_bM@hQWq>p7!<*emXnuUSC)^cXDL>g@JBHfC0b(e$5?Kiz@=2+*SDS z!A<=~p4&T`3&Rl8Fp1*`*KJU(RVkMp!Z4(>vy;KWA;x|gcduPvIDG!M@lRTcZ3c`K z3PYNvPrms4{+upCS9dqrtVI}xRH_w9#S*1*iCV1&Ku3EATOQiVw?F*orfD*~d?t57 zYbjsJ!R>?FtTc(qSQ&D;EctvM+qN-{42EH#>-rsewN|B8tFnD?n}w2t&2}vSLH2cZ zb?AW~5GM(~AL2L_!Z09CB9b&AP11XEoN}42t`1$0ea(I?ziEov*_N|$eHXo9pb2=M zN1Q~sjRwB&6Gq`ZIiByaA)hBj?OYQ85XUiY!$m1Y6ou%zL7F7^zE8bg$M^kv^hzn> zI9|!B6`&zgCC7P2B{6~H<9Qysu8|}uVHmcU1DK{s$#Kw-sWkx#HC8G)yZU?chEs6} zg0NZA5?01y;bt*bsIk_9n?uxzE7umHBvqKExu&^knk1>>%GK*pqh39|CSY=6Vj*z7 zPiHR9dA4n17^^>5hGAgaHZ!xAeYf5?K07__h)CQ0cL3JR)cLQgwc5q$nM=NiPS(m| z=msK!h+yajSu2Z(&h*TspjxfX%})RR4PepK_LiG0BJDsP=pA_E(RUu`Xn(V(yVGoI zvo+hY0Puo&%9Xh4R8Gy$UN{L9fhFLoQYu~*une)e^~QJg^z}WqA>aE_#>x(B zy0#UNG)b-mzCTsGx%ho)Y3Tw`;nrMUQcZ^U9ho9x0Bt}Hu$NmPK*X&X-~lxtXg2;w cfd3-?0_$hgRTXiI5l=FZH$=X23EEldD6p5)6(zLUJ~ zdEax+dqh?FKZo_dq65xdSJ#T2uB8>B5r-!6SO%aDr}5}OIUYI3LH4VECkVNdMH6jO zFCh+4kxacwf8Z_F|o8d}tpSr9mZr8v58^OC`r99ds5ZbQIEfM0? z;^oIMDj-27UCm+(*v=dnxS(?QP+Ifuj;KRB@2Hs#S8xU?k2d0o?QGf{^(#sxmmS8} zFe+sjVi@knFdxs9kjN#LL>|lLN$OdqiAzWL_!>3t=I(w=~`N;NG6CUBY09!w-C%o%3)AcT`*~qRSxJl3o9b1e*??1NmTlv z84{H~YEnQ3a1qVl4mU%f4yPHp{3OLu^5diY*mwrbD+ja3$ex)47v>=%NX<0@OKx)hm{L_YhX-C3oNxmS3@BH6 z`y%H!IE_1VP4}y>E^FlQp|ow)V+R&c+vxJ(D~oYndyC-eGWr%=pug%of#>JryuOv% zjdAK*^H@$Pj#YykK9p9MHM8UUmar>s*S|FH?$ASDrHVqUu&+k2RyN@U|HgjmY3g3E zsA;vZo_Yo^coA9oG|tr`f}!WogI}dI@9wxgwhPiU>d-#XbLl;^XJ=ZjO~BkzeD`-K zDMPLsWW*qI%O-oJWysJ3{+?Z#tLLwLw63FJ_w9HE1Pxy{1)e+d=*OS_S-Rec<`^5r z4lRcp!%#jC?vJ5k!;`(yVSM$oAD$|Ie(yFB7;>A4x)_vWulJ{QZkKX~lihU)v=_5I^TA8kEZxAR8E83i(3 sU0sELv#Q>+QrobnHX_qC-Md%)3)v@5_Fi%IwEzGB07*qoM6N<$f^o%kc>n+a literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/confused.png b/data/emoticons/tango/confused.png new file mode 100644 index 0000000000000000000000000000000000000000..e1a76fc38a4cca82781578d4a5a208b7c74c55ac GIT binary patch literal 1280 zcmV+b1^@bqP)Xkap9~E<+~XNP-bwNf3<~A`wzT1lEN? zNwc--X#$JDNttNI5ypFWZ;{Tw_T51Q(vT$&}&C@Sl*)MI%R z%LaT~OR1E{Ha7?d564Ni^yhr~9++UcPdZIJ@*@Wi$IJc%fyNNC4oodb0x?m*Me)EE zK$pNSAS@7e8sXDZIh!l)T#f5qf?jbX!UgS4#R6uq)*|A9gi#_OaWJ*0sYg}}ktj$6 z)ewQqq1~yNM7Th&I8_Gu4ti*6Z6Vnr<>E9V2E;{iIP=AIPM;e_iGiu(lh1zU^ts`c zGzP@MD=$EDtI*WilJB6WI)SiD+FGN2`6ggdwV)OeOZwZJq%V%b3YzpcBcv~mA_5{S zh!p~F8fj~dBJ8>=d#?{%cGK8wtUryB0INPmd{n(kHGo|LDF7BwR7;>fsE6WX0}mRT z4KBN@48&)<)^9?KzXD;zlu=s*Qy{gc5hR4@9M-y|UfhDJhuR$2BBCWkJ#3JN`cCdP zo)&Q7wYaBjR#s|9X%Z8q8yi!=e4nKpAui)f`X&iimpumAhx4F2iJ}o+A-Q z{RJ>JfI$PmSVYPoHE19eWLgGF81krH2AjjUQOHhJw5k<xMqaSyNkQ0&faYXNA0i z^()U2m)SYX$De2U>1u%;PuB3-enUfD0W^o0Mwpql_*|}jMaqzljTfBo;|L|Ni>L`*AaiN z+7otTx%IDZ%{;yDv6+)Ko0CnZZEKC$&@8JLj=eLj!Jp$^aWZq~%Gx%;jEv+!?V{Y6w%mwGDz0000fEGiNS)FCJ!`hPn=H zY3oQwYkmLPYyCR=psL(yKRI$po*X$8`h@r1|H@TWL{-^n4UY~D8e?7r?gP@Q`W%3W zJgurN#+biaYkxhK96z%WW1Yb8=ul`ZIbIna9XgmuB$5w(>7j=9_IA?gMczLDP643r z3!jfQH@7f%b575lJ6Bv@UOqUM9RJ(!=#Uvpj=N737#&KT5ERzcX^)o+9O|m zWUuiIi;D|1C7RgO)&{`h!UDOK9BrH0C|4?a>eQ*fSZf~#mOam#verH`mK=W*z|`S= z4Gj&i?AfztS0a(90^3w|uzmAp78X)C=g2N)nVx>1>FM{$E@iRSGM}2KqoY$)^--WR z7K{0VgI^wqMx!T(M~C_Vd;nw2(+>;`M7y_jbLC%GTrQW}1DNaozRu+2+Z)aZAl{f* zD|E0a(Nw*+rw3Jyo;ma8kAM*$KvfTPbaa{^2TuiK@gE! z65ZY1D2TI`bUMSO%a;OGePJv)?$%Clv^4v!|H0xve|oyo;#IBURH-oqR6xMu#t0sPVYX)P<@lx@Kq}+qMt?S$7m5Wyp(+s8syk-s^ehU!( z^wMD<%E-5W2RKgrVIS(!;FiE<2u0g5=dWAykBL;nue5N>MF1|!0ol?Ow&fcbv8YxN zse)Fh%|h`_9^YFAT&-DJ0MG$)eW2`KCtyI-2!;ZVFscE#0wO+#pK1k( zp>_#lJPtqA36w!sP|X8HAV4%gMG!@9Xb-7xfHH+Fb zXck-+G{?mGMR2R&@)$1+nb{ia;{sGUt*JR@{ASQqL|3U)ioBT2)ZlIjwHa`k`g4ij zAD^RKUabje9xx&gTb@Bo6eWU4sO|}=Tht;_MO_IK+Ds(65B_*8@AAPw{HRW?ZxIkbs#97Cp4F2l z^5N*?gyUZUmq#>Ytpv~B4qtZy0Dd86zUckDF&278 z9^2o*)_cST7je~_X!XNdlD`?_ci=^L!pseICr<{c75BmqChq^ut7qQL-cexZ&b<*o zmT&z|&rJWweW~Ze_iXJj_x6NMd#BK{r52Q##m&#Dy>hJ*+?bhv>C%>;C1>t_$B1OA zg~s&7i*J-R6c7>FIWTY+&=ObM^25(u-`5onKGzX1?P~U_TLIEGG4Z7i_zkU05zpQkn^3( s0maG5N&9im9serU1>bfF=>M7cAA_I!lq&6X(*OVf07*qoM6N<$f*eP^s{jB1 literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/crying.png b/data/emoticons/tango/crying.png new file mode 100644 index 0000000000000000000000000000000000000000..016b82eb5cc418e18c6122cecf42d1eb59d830e9 GIT binary patch literal 1319 zcmV+?1=#wDP)!F(7h<#s=4cf*rgPJs^gNsMygtwK|8=qHmUH5be|X@5KmYHS@AvzA z5mn_u9`iua0r#G_?cx+$*oCkemo^Dl2cRx}1e`^=#)sjOLy!{ka<`Fe*&ul z5@X8M3XXu|E`Z?+DyO=O>X#2DT@GDZsF1{Vo6ftZD0YC%$nsR3LR zA8ZM966^%R1Tm)$J~>jbh4RMPcFibg7f0e8(;Yh#AvvtIh`1mzlsHHdOf72C$TT5R z0}@9yLMR@(V`oC*9HU*FIRwQPc2S>gBGV+*@+cw(#6@u!^jMa1&%w~Oh$;KH;uy%&^szKJ6O zBGVQtg#IYfoUK9Fd<%A_4_w-)YcMt##Yl+N03!jaK3B&b(y4IDcPWuhMHw3h)e2|; z>Z1hM(1*GPgG<{S1ro51bR!yisI4MYIn%#{eTN^zHLz{{1fm`qdTd)?z%{V%gDhvx zKLlDq)W?Q5Al=BVhP-Q;8md5RCiF+xlhf2=cXOltcHzLMk*V>;DljQA>qICZ-Bm3Bi zNeVZMy#DeSsuhe$qT#=|3!X*?KQD>Wb35DHo!l-?p_M*%Ki6uPm84qu86<&7g3!jO z_z`@6s_vr^ODq~<#0M$>L{m><#;o$)7X!%w*4>)0Ga(<3td{9hY@9#fbnU_FBWX;Bn zwLm(hVfxiaFKu6$DzC3kRn{$Xf(*d*FfwxaM(mHl$wdE=zcOcw*5uXbxG^T**V8ku zvq0xvnX6Z?c9Tn&rfkJ7G-%2SaU&A3VvSK_6{?6W3fyq5y#bVZdwb{X=6MP3c0@#O d{p)=`{13Z9M?xF>hWh{j002ovPDHLkV1is~Yg7OL literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/curl-lip.png b/data/emoticons/tango/curl-lip.png new file mode 100644 index 0000000000000000000000000000000000000000..bf12d34c8d15a08494342fd8a84ddcddb90aeeaf GIT binary patch literal 1269 zcmV~JFn$la&yz;nNwH8A(5FxU1&FM-UJeg7X^Y|2tfqFh!GMYIV~plG`mNfTsylEw&Ai}JT?QU%P<}TD1(u!^zp6w%R$I;7A^EhhlDsqb8AA{ zTEjH980uG7lIba!PFWot%X4#VTnoGwJF1r-jo^JTS&tqGgnG5BJ3@TDc;z{)3P_NO zt3@0E$9)2l|EQep&uO`EBOwB(KBYXmVXz|?}o5EBMmlrq>n zXa?*Y!W==T5k5bbwb{bfrFPv{utyvTaZ0ajFP%?d+ntH9wjiUjs-4^=C20;lREYpahyhHOtw78Qy$D5H0^7?mmIK z43nOEiC7f%=D^ed2K51B5h;PxpuSWr(-Kg`kmJTg39N^4!;qe;U{xvb7+^Z}qN%Mj zet8N}XVG~D>s8tj7ZE{K5j<2&To}sGQ0IYW5z`3MDT~KIbr&gvIy#njg3lln!OmBP zNaqlpMePitX|OXGWB~A}<0L0L0!=SS zDSHv{!FpV}Tqcvt^V}vwBx0D$Sbq8izBui0aBr41O|!V+Es#tqH^;^!SLwUgJjbYcM$SVb_j^MR|PYMW-?lXrJAfM zBYt57>)k}XTa_Yr6~=AG33WnpQf+^KVLD?!d26u!qccDMG4oJ@&dwb{H1&Q*ts@Vc(yrK++62)aezC1U}|zc_}6GAGIH!n{Cv)uv>Kf?#-xV_ z2WNE&=&~ns0(r39&hWdDq%`AU`xTRCSw|C3sMX fh}?baBNqM(Ob|z0ykkOl00000NkvXXu0mjfe=tgN literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/cute.png b/data/emoticons/tango/cute.png new file mode 100644 index 0000000000000000000000000000000000000000..ca6b4018a85cf223c1904a89261aabb14d11dc1e GIT binary patch literal 1275 zcmVqV6Bkv55P zminfQv^2%2YcQl5mo{^gFg;~;bgaa^v2pcyBY9eHJ{{mAF$JF<2}OIgvnx(|hxpZb ztO`hkrBur}0*<=@vUgOzA1G?o>r1$t=9Jpia%*Z(@aZ9)>81NbBB-yGN_mDrW2kJx zlEU&FmM!?UhH|-xZP+YypGc6-^cH-2p6DRoBW)%Ty+ZeiM8zK`&^Tf?f~f&XA|?j7 zC{?f}&_%HG2=hdoI{5NT!4|xOtK(X6a7-MDa#lOL;sMiGYY}llA}CRi1ehAsq>yDG z5(9~%8X}Ncw6iNNQO@s%c4I&s zs+D<2Hw*PmnPMwP*E$dmOG{JCuiOPJsut8TG(1Dso8NKe>I5uDbLHwNUB|y-=-MC_<`@hd<isVo&bkfp%20+cZnQJV)_z_>BUO)hDz zse#WWrl+iNYeDmf=0WqIi;RxVfSp5w9M~Khi-B#h9C#Yl*6Ge$25?a2`QVmoCM10hiF`=zQqDTIxRRGD;dQ+=j zpAMWWgrlzzPIiDTAUccMS+Kd~XBKo84HgL}4iSkSLHo}YwCeRe+O{9tt{;?~Q;B%! zL+R{_k=`bOH-h!=q5l1)L+)0LTaOcMhwOyffdOxN(SG*+VC%={f4)BZL_mA{%MmwT z*z|65_J!j+r#}sEOV^v0=CIk)Agc$CeK4iLpJP>TBKzQrTaCS6XX~ySk(^&l%#4g& zEUya?5oteoum(saHAsE1=kAg1NpF9Bvb?X>@zVe^L1=PfA@cj^VtnMxAL)xlYjSFI z&KQ#$9vqz0Rif*T%w4;7xrxo2leTO(8Z>Ezs1XTSvBs#e3Khf_1r}Uu7l6{x(9oLM lye{BzOGM=1yFTT?e*h9;T1x+Fw1WTu002ovPDHLkV1mIWM34Xg literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/dance.png b/data/emoticons/tango/dance.png new file mode 100644 index 0000000000000000000000000000000000000000..709701d9b4b22d7bdacdf14963ba9717749fbe08 GIT binary patch literal 1359 zcmV-V1+e;wP)-0f{h0HHRbMxJA(SyUL-qwEEdM<1Tx6P3@eBGQ~Vi&5gTd?fRI; zz>Kwn4v^0cqf!JCM=6*#2~e9O$W7o?mW#9XnK*5q?n?DtAP#R|E$~m0)#Z8CZH)Qu zcLc%rxWyY$Kq&+%0x8Bgr5L9aaZ11xgDJ)jTyo(M(z-b=UUb*wi_$z>Kt!y?!w6tO zEs_JG5s(N<6eJF&1T|%dDFI1<#6Y5`dW4chH?s!9 zxTJ}$Q^T}A`Z*`N`vD(phLhbrv_AX+T|W&WGF>DJp+AT;*2fT5&q^{bcgbycNIo$D z8epXq@PqBkvwX%JodmmYOBj=X)Z%FiM1||KiEKRmB+Frpf-y$a1pP)1Al!BG;HGU z4><$Tm5V%(MKz1sG_{o>=mhmuAsUWiNK#iBB05f8WeA$W{h=FMt+?Ii|Ltx@nVw=~ z08AXU8PGK7Bwrr6OfoqFb`&&;hQpvq#zz0-NJl@031oDVpaoFCY!xUIWN*2L08gmUuj2IBG|8?X4xW#+R~0`#_<+{9h>5AlQ6x-j(|;~VG>~!>?mP#IeNSu z*I$k6uY{xRIehlO9PK*ws=Q21-7aa}91rao{K>CD1jR#e0GTElf( zCXyM1aeVd&ykGd1W@pcX*Tr1UIIqPEy%%ItYmAy|3A3lM!6h{KW9rYi3o&j5PP7I3 z`qj3zWrrs0yU%tm-u=b#pOaU+nfOwD_;p>O5Gi-z7QsII9njSU56L8V+h zaO~wl4bS)b+5W!E@Bdb{>*K!p-9}_2NXLgypFWzqrhte@%c@l+Kv_b=vgdBTxUn{o zT~U$9EiZL~8i3)@8|WX8{Ms{7c>2AwHAmCdjHuC3W6a2@&dyQ2%ITaV6QHT7$&Htn zCv48%V9H?6EOcoUtq6;rek-F-(5-eZ? z6?LtBIWCF;NqdnuvVL{*yJ4AJs$-?Yt9J z-Pz0mp+F)cKeMXe0-C^AU1MGVK9gEI45(wwec;2y+Mffz0bGHX5&4d)W`Nx6w(!RQ zfJ??S&X0@?EN^ah*V^q=AShR>#bYN=7E6^1g~NwIgz%$}Xx8g2-ntdGmX{lV^yPA; zk#f0sYkht7`7}MUqv`HW(TLoN()6?EM@GCxl8`-ju2>!)$Hp<5$zVMX977?Sp;)Of za^XV0ar<^Yc<((bp2za$Ccr;;YwiIg*8bvNqj9D>F;P4;JxvLkr0sxmwaUSW8CR!y2c3b;E{luh&UpzPR+~|Y$w1v4!92FvS==c=JQZ0p`|kP z_oIaZcpl_(*u000Bc^6%3XU;9sEL?;0N`uoYISJv=ur~CjTiyrpvLgZAO6Vwb^tjK z$|Y8UkXL?t85{={0b@v$gu#gk4pgfZ;OqMUM&#L}laqaRV*|FE1ht4-=2uqu{_o#} zBtg4fUY+|j^GizzAZ%5k0@`XZIyqTzjd^yT$wuT{|IiQ`1Q^%BCJBiXcuQNgz$Jy@>uw1z>6qpe7;$6NO3zyRZPc9K`XKK(EKl*eJLz z;3zHg7?e-uHd1q}MZMRXs z4J{x17TRn=s|BqV(r$C*{wmt(pq&nm2!6eeW6brPQSq_KskLu6>vb#n0)$&POTv(! z-nk3D55A9XHlf+vg7`i^xw$~x2_V=yPc}=lUbm^WZ||!jg?HAQwkGV+;fVgdt{dkow|cN8#Na-QCkP>xg`K z;koC=4`;KIEG&TMA^AMl5NctUg*XP_L^jK8UxBBLLE;#CU7XP|*1}M)zVXIls(NZx zHQEE%hD^Dh_t!7K^pa<;UBf)oW= z{oJhT`;Y2mW>w#}z20l{b913RdlvW9DF`}3?Dm|9T*xi{X;_l`1LxQ^}1cX zb0=P3S!uj;=8A&PgZS07|EH#a=*#-ERjzSVKv2WoWF7_;%g{QRcw65R*biOI=Hw>UW1XM476 z&^{}8E@Z7(15TC7PQAcYpjmLiRf4KYz*N9u}}xHPzC;ewShG3lZUVgd)GA7}X_@|!n$FPa{CP9;-Z?I&h00K5!%bdtZ*K1Qo%4O~oFit& zt6c7tq67X5Z_kKV8Q?U+34DenWKHNwS9}Y^OcEHDIfL6ga zK-Ur0iF;k}^_8-ggJav}dYZu*@g%`D8yQVTtfH!j_#knN1V{>82j()!CLz)elE5rR zBn50_G${$LaYnorg317=>F(<#+bi{884(BKV|a*oAZ-{)aGe+_khaZi9EeAwz7E+# zLU&(pWq{MI83-q&zpp*4KLivr1ye*65o?~eq#*7F2LtF*afmFdluqtM?&3ywSM56|D^*DTnTL>iNsAUdd!LL9Z zadk`sa8)1%CI}5w{bm4K1uKIFh}95lpiu=fgKQP#e=~TZSsleN3qUJPqY=;uBfvQi ziz0+NSOv2(Pz3_S8qdN#^}u{)$S;9QVOj;NfR(VQNN#D3u?sU`1uR-)?7~khE(Ayxt_fo(8xd4)UoN?4Q!&2#5okxQ2!V$nJp ztv3m*jJRE}x~zoUZY40LZEn8m#ot1xffk!ZWb3T_6O!o=#PXm8G9ALoY6GzXSaI{K z;KdJPbMsY<=@t)?511@_^9$q_2V?t=OTGL95P^n_o_wtti`Fq5qbHjYSyTmTm{o}- z--TSxSeai)K49|6&$DBz2yPP9bBIe|#1V;Y#v@H|ix3Ght>I$(i6>6|PpbgZ6L#Mk z!9|<8QEp3oKwEkkw2W8*(*jqo3s5BlQ+uN2rfR`wx8RsA632c zsbuUk85wOSdr+ca77bUh@bBg$e?QLe#!C!AZqamVDp=jnFF%i{xoi!z_S{qWQq>ENC2bnR%T7iIz0qS(@6G5*`!MsoJbAK6YO<>s3TetDEg{JpnCW2 n-Im$BBf;~Qh{)F7Uh?6;`=ndM)=_Dn00000NkvXXu0mjfcR?#@ literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/doh.png b/data/emoticons/tango/doh.png new file mode 100644 index 0000000000000000000000000000000000000000..d5cd84cc84a7345895f3992f31431aaa410e1c95 GIT binary patch literal 1284 zcmV+f1^fDmP)ZlOK1xeTBwv_(MnS_@`y2NQAt~HqLOx~4v2ANOwh)3KpYT-I5EaJ zB2h;|5M!d0PNXOd8cB$?QZ%8qlos2Bd;7Z2bIx84Zh_)$i4%6RlAY}A|Nqxod;d#R zl?S=Z14Rei`<`}+likKXgx$EbicbrGx(wpek1`Pdi!+?mU?m7ysYDCi(j-w%QB%KJ z8tP+I*BYuemJU-BFgb8pS&00#pV5Fd$7Q;-Guxd@Kc|q$R}kPWVf-8n)=PuZIb1R4u6GtCN4z^;RF} zE{wopY5Ff*q3hrYzC1CEgdBqHk5PQBouF$!@_37j@ML)pTw1BFHP)ZRNPtxzBR+3* z)^c#|RWy7V-!_2cc**^hy`8goFFpr$2Gm3Gv4IEGwFZ~gG6L~w(W;GT{tqC6m?CQP zU~+gbw1G(?n#Nj}VCS<~n**Bxn@6;OsD}-*P}Rik4_Wq#N;NBDQB?C_vx{{VumMUQ zV;nRH5Q?B#R5L&h$Rq0g7w#^Bx(t$~R4kVWkR z*bK&vL29CeRhEHIKhu*>n~F*kcoU$eq#On7gG71nqZ9zgKBz|UQ7xjHMKlYVK}C-Nvl0kQL7XAzx6bOyC)P!H6@kOrMWGz~hp7+IXiR&-=Ehca-d zw|Bg#Q=Ci_gY18$$*#{yF>?_lj!3-ZK@y_$iw%MW0@Qkl6i}NZ6n_e5Dy01Q-Ed-< zmfH?{H-nQJXr2Rdwm0jTCY;a)u(P1)lKRCEn*y6ggEYbn8q8qaN?g;3obAnOo&!tm z`yR7P9@Sy>@<+7ybS4~qnQ*cdYzDbQl3I*(i8z=ioY+Mq+J>Gvol!4;BzaVamtrn! z*Hby?a3UT$B<&qB>NZJ`AHw=KQU9OPC3geHt-*;l!}txgy}kMAdHdNr1KU3S;g{dj z_a$g<-V<@-nKcKujPK}rc>2@urn(x_uqABP*UHj?W2YuHxHjtLZ;VeJzq+yKoAK() zMkM8D6Sszj&J|W95D{tKxw8VON@`H`?)K|#4<++Y)FcZnm5yHra4QH++?b188JUj{ zeeq}AxvVuQH9BLANevDR%;*x(6-Q=cW22i`w=QW5cC|s1R)`vrkQHl;8mmx5Y*t{- uwRR53UAlCsY&Ne*u-p<6xxLr_dGTK^M^mQ^hUE7E0000#g##BZ=ll4+ zd(U@8Re74rJXLhSeeCIgI9?|Q5cc5GDM}yeHI(7h-(2FnM(aVydKTRulrBkd zf#$YWX>Uu?)M9AdRz1v5!R)lv@re?5CvK_Fq0C7gdoqHL#1umYC6*Y{zWx;1T@nWK zSQU^sRaeV60*?C(O#Y*CIp=BMpGdo$OV4 zER9&6#j=Ia)>AHf*p|)0kz;AHtwY6-!N)4NKPX)$o%n?#$I_MX7Lle9vk^=^NCq)U zz(onbmOvN5&LhkdcbedfQ$<_!cdxcYR4u4Q#FG2*Hn}S|p=M3)hf#7@ zZXyCAHH#IZU>a#}OCs!9f?b&dmtLA$j18wT5@9vONQi1s?FO)mASJ*8ifS1&1PxF^ zY!pCKi@~LL4S|I8XyZ22{{x64rh-}@Oo{$|O&~Eu3s~#Y|Jn{z1JoA5`iPbh4X}|1 zja@7aA}&V{Zvsi7(HzD&96j6uR6rL|EdnLLM>KdCZiPTyM#xXS zN+ykl^I+-#gGPX{h*Uu8&`7G5X$2@_@KAdnY!TxoAwN~cTB9K38ne@TO?`ujf+<9u zn)3=a1WC~M?p-!+aH!uP9Nd@Utv6z*dWd?UMZ`40?6f81+S*s74D0wr$%#LYPzJkD z9U`4abPlxzzCE4d^N$m}zE?PVegV+}=t8YLocJzue4>OhynMIIlRO|!v5OH7zTIWF zl%!Jp86<^Bs+vI(W9i)s7Kl(A0AnF6TV;IkY;y9|*h5NgdO`KcTsc#k$0s z)Ma|X7LjF=e60#q;%Je&^ee;@`_W4miyHVR9**rt#`R3eIi60%K9s)xB-!l}`J>qI zE*kzH78SYyReS2_CgXM3(mY bgpL0L;mb?*CgEqS00000NkvXXu0mjfLA^`X literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/embarrassed.png b/data/emoticons/tango/embarrassed.png new file mode 100644 index 0000000000000000000000000000000000000000..93c1c2624ce3121de6cfc5fa413d266d9f265cd4 GIT binary patch literal 1284 zcmV+f1^fDmP)9$t0-_4Gl%mwu+mmizY>PQYo$q3U(!ekVO{7MZqpb zp$l6Cr6N+gDv1kgTSREED8$s-4@1()M>Eiwe9dIu%zO8^NZO`OQ@iPf3l|RO|G)qL zJ@*_@Ri5QC&lDYSo)|qOkx~~25%%NID*<}|)M1o>ODH4mO@{bE!;K(hBa4;?q+1f4 zCDoph&h|Q*TMUh@tHJy<rmN( zr4h^XShf<_dVIfxZD|sY9Q8n&In{MO!TIU2oSz1&1Ue3C?O?zZ;UnT8oGS5=TjZcwp*L(}=7Q zB6T1MRHKAaK>PY#NpO}!5~(35b#ahXdxmsID&-kO42Xjg;q-SmICW+e#Rb#ASKt25 zsWYQ%Y79t(YGnb^9YU%-Q|jVitpZ`cbhg(8l{+rHfkQ9NEye~j7zwc&U?f1bx|$7Omq1nk3n;2SXaHJ8 z39w-mnp+GGy)^_9utytP(egEdoDgJvY;hj5sh+oAsV8tfLADprkxfJ@|NUN!X%#(2{tdSa+VK`gKU)<SkJ#7ZQ-3&97w$mJ^~^7#cs=TTcgG!M3bp#VBhKA-3El_k(c z&=QgO4s?8C1!bg0fqRpoVq{{H>{NGj>prOze`Qw(eECfile1NdrI6*)YCR`i#dDRm zRLF;imrz?I>b?ZotTH<>>E2}MLGf(ZD$3^xD_O)OP~wP0*X|>0B^n|Upw`Dkw-HYq z_@7n*Bv0sdt(K4L@VR0v@fNXUFW4fY1=JS6=GS%sw19?7#JqjP69>?tb49I|kN>xA zKek=ZtwfG_ZuArB>#rldUBdDhHkd_&yQ@jgHjI-ZlIVf#l-l9pa&F0f@zF@vXXk(U zv+zWMo}Sm^j$7RFK}YuGcVEbT9^0Nyna+-w+1euO2acVa(eR&%YI!O', ']:->', '>:-)', '>:)', '(6)'], + 'musical-note.png': ['[:-}'], + 'sick.png': [':-!'], + 'giggle.png': ['*JOKINGLY*'], + 'cute.png': ['*KISSED*'], + 'sleepy.png': ['*TIRED*'], + 'terror.png': ['*STOP*'], + 'handshake.png': ['*KISSING*'], + 'rose.png': ['@}->--', '(F)'], + 'good.png': ['(Y)', '*THUMBS UP*'], + 'beer.png': ['(B)', '*DRINK*'], + 'love.png': ['<3', '(L)', '*IN LOVE*'], + 'bomb.png': ['@='], + 'question.png': ['*HELP*'], + 'cowboy.png': ['\m/'], + 'fingers-crossed.png': ['*OK*'], + 'alien.png': ['*WASSUP*'], + 'disapointed.png': ['*SORRY*'], + 'clap.png': ['*BRAVO*'], + 'rotfl.png': ['*ROFL*'], + 'dont-know.png': ['*PARDON*'], + 'confused.png': ['*NO*'], + 'silly.png': ['*CRAZY*'], + 'doh.png': ['*DONT_KNOW*'], + 'party.png': ['*DANCE*'], + 'dance.png': ['*YAHOO*'], + 'victory.png': ['*HI*'], + 'go-away.png': ['*BYE*'], + 'smirk.png': ['*YES*'], + 'pissed-off.png': ['*WALL*'], + 'mail.png': ['*WRITE*', '(E)'], + 'tremble.png': ['*SCRATCH*'], +} \ No newline at end of file diff --git a/data/emoticons/tango/fingers-crossed.png b/data/emoticons/tango/fingers-crossed.png new file mode 100644 index 0000000000000000000000000000000000000000..6494707c5d6ea127bb15f740b473de6503de026e GIT binary patch literal 1373 zcmV-j1)}_D*)z zf93!G@3oeg8NcUpzbiW6|LSYMc;#-^BlO_YD-kOI%x8p%e_;%#kFbZ=EWQ$iTuGvH z1F}St?54G&T{=5b&x5?W>WeY1$W_-4@#q?6R%RDGW43KZ%%!ZAbxV2eo z)w9Hw!sKd{7L;pH77*z?s?{>uHecAVF+;w6b17orR~eiekR>jY{E`hDGqvahv853= z7u-CMEaFmtj}d~-g3W+VAxx3*a`57gl9no~E|u#dgMRTO$!=TMmyS7usv_cpBruX7 z8F2G3*Mc+>A}Nq0W&yDjv2}fENwS-M@h(eH?q)r$9qr`XrB<0h#DVx29xuFdm|cS- z7-?|L?0oqEy9P%ZY#fM3Set_UVxhI8z1+?E%NYng(%F%UYGZ(6reKPQGW5Y1L+>Aj zhBQO(9c1YJqlkb=Lt=wCoIpA|QV2cgVK3H!PcOMPr_lsXV$>p>M3{y3XaJo7nFSOu z%&K4!Scnm!aR|9KhfnX%5|sZTJIz?b@iCeXen#AR4c2oDrWJ6r^xcyK2@ory>eF}c zBFsWeOQ03Rssx{Yg*SE@U9uWc)|9u%v+1q&n3x%*zspp#lsW#l5Z?rD)T7$J7Gw_M z&1o2#1`>$#FpYs4SQ*n2!Zb~TZxMIJ_=h068G=I;(%bCnx(6N~yHgM?1IvKsl3-;% zKQM*G)0j?!PJ$J&_$*ivv#TMaKgcRI7p7W$1EC=*IEp4NY4+)z)az*Sk^U1s}Vg9c$~e9;&o!r}@z z2PPo(c!>dF_F|#MJZ1a#Rs*|>z}CXPcai0H(3I^3Eg@FKv|(?48Je;^$g(x) zzP$vTkAJTsG5@^K?8=)YZH^~!`~@J481WN{wbHjfMgBU8D+kf&M=Uy3C+si8`K@@# zWiWoybkCkjr5b*b^7UqMgid)n<+Fn=R{2>sEpHNe;N9C3|CPAqkBi4wK77NOXPU0d zx4O>7O>RM(Tsm;{^n}I#84W8Z$4|fT{k5B49nXE0GAquV4$i)F~00000NkvXXu0mjfZXAgR literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/freaked-out.png b/data/emoticons/tango/freaked-out.png new file mode 100644 index 0000000000000000000000000000000000000000..3bb6d58e5ec87252a81521c70db0bc51c1fdac6d GIT binary patch literal 1236 zcmV;_1S|WAP)Ln7o`PlBZ3ecT1=~LvuOToCYhvZlF7`S$=rKB7fI>QB&C~vs|)9x_u+ld zx!)(M%9AYfMAZTJk*hu8d|hm#3B*Zh}jIL8YGFBsx?3fYyg@C zyNKu<6&gcM_2+E9xaa=3?!BTOaio%ddth%OaF)kQkzOu-2uq=LJ+tsLg>bAX-GUM5F<+ z+Gn`?L$3Qw4Mh||wE#Ach)dh?T_6cGTEG~Gw&O1XE1+3ab3gzT5M6m#?jAs0hMCX2 zEY)#r6o9GV`gjYMu5Bk@C_4v0q7ruRoM8LbDX=*l$H07MRjbTF$N-tym!!@!gmZ{G z3|?vD^@DHX`~JFs>FG3Q&;7#o#>=2NkowgY5;9N@PzD)G2TtcJUR1mRR;byWCo?zA zt?9{i0Wmj5U3DIG2{gco?Sir21C+rsKw^Y5IcI8|iJ5lSZ`+a{fee?F0 zM+Q2+eps)}K}W}HaW|2xduQ9^?jz4-KCke%_@<>HCR-jHI65Kt!Zt&z@?aHmOnV`#UENZcP@R_mjoW8YgT5xE;lA{kIhV z_hvS6^{X3A7xLE3tI-8x%>3}+;DX*Ky5Y!dZEbbE`ue0T+RX+{TA|X2#H?6j)L4ZT y#O4K-Tx*wrU}$KlY&LHQSg%Ax?tbfI9{e8-=p2twE#M3Q0000B&TA^B7q;0w=CekjXqLg-HOBdpz6bdQ0kS>I} zDU>c;l|olV#6>GcZBf(GMKDF`PyNA$iTP^+nK+3vb7$t>^L3Gk(J|u20~Zb)?)&h* z-#OnWs>-um=9#Jk?h{Y=vguronO&vRp*>`lM4L z9H+50Ep4q)8k!8r=7q!TI80Ai9nKV)%#5hV?!*zj{&aw^#N>VYBosNQJ-snf>%_0* zuqq&77E&$Y2srL@FnU|%bbmoB-r=~*5e}(cDpys5yiXsg^g;IRj|Yu)QZCI9XcU!t zEJ-ZSV_A)FYbliq*rrv&zWs4h>4SNnz9%}k*C(AO9=XWA{qeFtLZC6ktOQdFl0Zxp za8W8?i=gvha|k)YP6PaKG;j0X)}?VhaIi-liEvzddSe08SZfh+LBc2zkT{rH)FhEb zAQA=ZwrYS&Hw{h3`dN$wsQQTb{5J3(2lj2??D?Bu=RvcaJ@+>UKH1E# z0~t^sw1VPegL}}>WN_(zAba0{&n8VaquwWNJwDu3Ps^GlU7blpXR+4hqxaX+ z(i~#rdQj^jT0*pf4GNI#r`QK^@K%osI9s{o-F>=Rg9)_*}VB!^l{H?U8B1IWW6E#-QlH8L%1bhB&B)6JCc7 z|6CMhu<91YZgGmq%m|}n8j8J&>OANi*c@-XGSA6#9;KlX%v(FCnBS?GKan@z!M!nw z+8l1Q3r0tk$;?RX7N@GO=`jt|W4D~{FF2+XC-ef?9Ox{`6;0boI#g%1l@ zj9Z6m+L6=!1@+jyw0%F+>=H-xx>mfy+J7Qn6M2`KL^s$xqO+)-#qS8A1O0^jZG`+S zFmMJot|uJXfu1^%*NS)e(YF2Ac0E;e4#i`kFQlh8N@|S+-c_tWiIygDZkKUxRu&w* z{2HuSsrKwyZ+hN-_t{{_*C+qDGW$e8SJ&HNHU_bPtQx&*jLBXe9K5SbM3)_z?d|PueATLi zE!mX@O;{mfL_$`qF>0(r8LUP)Xd_f=_&6#BUIB~@Z=j0GuC0)_!CDv61~fg|FBFfu-?>3}#O3KL@- zFaYAf)DT0!1fV&8*teoKFt8?(udDkl%D7xoaQSHmV%I_B%11wwG!qS6}2@| zUmKyk(oj}49ZU|w@Q~Fj{TXibU)3xx#`o)m|3~nln3PY4gu-Dxdk_r+ZHvsdjxxJ4eMd^$+fbh2wtET|}zd~Sq5Bd9FLQikP0 zEGzMC3AtPvTe(8mwI@cRrZeT!anB5<<$N}y51ECW*l5=Tq~a8U|i zGoX`T#}UTyoO1a5V9KVl>ley3pTSOXB+M~wYmWvDW35HR1@TbAATcl{s3}8c2$2X# z7}XGgjG}GrQ3-R5o#HGaNH?;birN|yHImN`A!0yW6o(^UUgU6BA4(KVDTluJnZsRu zGuaprheCcF5_LjFZB4q7?X$S^$5hxL^|g^g{u=LnlwmTJ2N?kzF7>AX;o{{nz~Nlq zRRDT_xd!S30mU)PCwo2I`q~JC{TqOjw@c)*h4M;c{Ui;m1<(Z(K-t_{&E}2ObMNSU ztbVnN4W^*H(%`aXkwoItOj#8gjPm?eAJ+xc0+waPAxcX;Hg7CtQ)?NeB_7L)i-764 z>%xw$E;g8evMOdTnglxrb_@-YV3RY?F%ESN zkP7E`2<8KCs{^97? zH+lX28+f5Co7Sa4CqUCUUITiiKZDXU=k450-YHIMEqy%u!dkmBBl*<#AW=l3^j(b7 zbu!GKe@*|-nmTxSN1n$T^FR(XLnzvUb1S5LbK*km0?o4(cIi@)Jw=coKuj3LLnJg~ z+4K=s4G{5Bo5O@w;f1&TN2vhf`}Kkrvir3Ac&aG;97XXhU{i>WqIMK)a%PUs7(Pi+ z>`A=vHuUuIloqo4?kwAPE!Vd)&fZuw^p3Q(M@UplkUfv}Z=n9o=}qn`j9Y;dZi0d9 zYP-9$!;|*2H+mXBKK0$NqxU3eYI??Vqp9Vu)eWqB>7n6Ii>eb9roOJotgMuU4aeRZ z(%^D`A$xt`){)<v-Wp6%uZQJU2_VJ2% zuDR6l696MYXz==k_uG}p==p;e6KB%aB-QAcF(%p9(=(H6mzfBQnccY8{>3H6>?uXPrBD=HB}|JZ4p7XiC z^ZWjOM^u$-Y37<%2i%!xrzE^8PvWpm1l5o|$trJpaW){V!=+C`RsyKYQ9_QO3?_#; z#D^MP4Imd<Ab$vXsXP1t3(fW%P}ASp0&$$WK!%vV{EG)NP*?&IUTVnz-+N{PRC!oMtcy6bBIpq@lJNWV)vfpch3L zR40jdE)v?h7J5@Z8bayod)l+~))q+~YSVR=779pVjt`$HCSVW+I!+cO5 z6rx}px4Wy6vFtkP04{yBwi_FcV;N00JRmwcwh}%9$f(%f|gLNu;q~sum#i> zuu%Z5?FN^=dKHDNq-6mb6;NA6sG`;fErXUor}%#8I2siYoj`37wWaBADu@Qys0b|! zxNsmZ0bG1g1$;!kLb!l*Xo__Ir-uq~nvD5!h_9i*{>m+aNFVUEAAwO2Db-9WNIl|fT z^=59fiGndi9cpJ>!G<6S6qm;yjI*kziT6I9N}iqa&Y|MH#FEB!@Xv za{?4^Hexu#=!Q2{P?s^EFvU1%wH(6BxG{*DOH}XFT=UJp1~-L`CV{dRzm4mu3!@ z8WQ)=knRIpLS`WOX)n~k(G(4-wZsz}(L)DI8u+^|zqX}rWbPDmrtIuUC1Wqhrv64c zJ0$XtVZ+mC_;)SIy%FQK;Usz?H>x(1@y|}#*Pa_(`ts-B{!;i)KyUAzaW`4I{;5T| zH4iU7`)Wf+XPfC>)L`bf%j|(;&x~vIS2plRb7%JbzF_+Yxz?YINIvvZ=Z+m4s9Y5w zBGP;N?Q?;av_>t@-+F4}O=*91Te`Bc*$F!V&PB1Y(PI3!k*Vady?=BLc-G|A=!7vQ ze{^tgLT44NZ_%_ZS+c}U&6}6D6?>gQ(^g0rk(d>0j2f#@MXV=KbgeA{<)NXWy4ieH iz?Doyj literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/good.png b/data/emoticons/tango/good.png new file mode 100644 index 0000000000000000000000000000000000000000..127d07313db268573d39468940611007f2caddb0 GIT binary patch literal 1011 zcmVw}^~U%U|zW23Zz5H~hPb7|5h&2=}K+1=ThbN)W0rJJ}}Qf)nOn1M6=erLXq zGegXbTRFrnrT-rSwcSGT1U&{JPcvvnQ}}4|bsqaS0;<-5`yw4wPIAaP@^sYXE^;t* zX36-bBk>k5+^C>>L;}sIYb;YlLGErNb@2YQh^lY!JVkDbfHX#VdD>zl!0S^`ZCy&X z95_&@mtWvDx}n~xKJ1+2c|f1i;{zp1>;bQ4VIAs;Qo5zPP?qn2i2joR00L?H^yE3s zECG_>^}k@>`*@sE4Nnz&)dlZ3J+u=+)si<>h>E@9W7*pIh-&YQa(*0$H-VfxU}lC8 zjt#o(dkYNyh(q(a^9GOXSm5>2Q2}`x_04Ik<3UG671*5yNCL_MIW2&lgvnolo*xf* zW+X8F+GW=IIj4`mt0^e-%3g}4PFS4}$3?ms>|O#2a{!4A0ZBkefE3710agOAk}!)C z2!8$2=W=;({7t@mjkmdj7bpfHU$q~2CQh~iU-<>_X0PpKW75?t01iEHX$kDjLS$MY zcD49MVSgoZv*2wmf_0&=oWs=j{Q#RSfo|&Px)coPGN``M7N(W}07?00ZekMYy{*@& zM4N#m{`p3%LCJWOkTH;o19l1!5$MGUgyVyr zUcKbyeDU(T=YscM>F2Pfpu4+UEr((9v%}|~Y-_MiTK64`in`rkzW^@g!QKkOxpCjj zU7)o!^6W~C2!jw&J$-lzB+YyOdR_0{o@KI6B<)0u(g;g zs@VzG8u@T?-w(g%67!)jF0|5+VO37mh}wI4B9UCqk;cSrt)2auayq*d(V9pxGYKsW zh{`0iZ-Al!3ZZR#V*>*&Zvk%qu>;W6)n%1Rr3g|f8)0ZmArt^;V?u{0SRo1`u3O4x hv%yyIjs!Pj{0)Pnn!GlKRP_J=002ovPDHLkV1m)p+Km7J literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/handshake.png b/data/emoticons/tango/handshake.png new file mode 100644 index 0000000000000000000000000000000000000000..5c6d6a53198adecf978de2a448bfb6ae355686e6 GIT binary patch literal 1390 zcmV-!1(EuRP)mT}F7vc(NAA#dT zmV*NULj!{nvD{y)KmAMi)#|dD#+F7}n$pZ}G?HlmRU|+7v{o`z!WeMe>2UafvZH&P z^294*dCgy|_B>!m9bUpquO^%IWp1lP;Rvb*B?$Ib(yLi|uV0Q15k(Q08jxvVYN>J!rVeB#m9qvUg_tB(J*>&Y(yXs8 zo5$CPa~9lFm(66mQZYxdsvsUB$w~Vfs$7HAfXT3K!yeXe><1jyy*J4EO(%%T1>}+z zp`p16xZ|t~WVz(h1}<)tsPrf6HjT33;}fVlyt&~Q-u>XmN}5atgGQ(Zbbqmr?w!9P zqICBhqq`@MVu{K_(Au0<;l`=l8``zCDIJ#%Ap)QE6xrUBN5ryy=K$Mx?x)Ii(E_65 zfCUt^1l8`=2pf;$%xW-rbWFwX(IFQ$h_#0iIa6S&QT2-|*G2QFb;;f~7c>Gbl3knu zNY-ZH7Qhxj%IB)k$%sRQ306Ij0+R$g#;S+w5lK_!8qqx1Q;6nKJA%fA3S7tN-8+t$ zM5W6ezMmqy?9=?87{_PVw-M+#=oo58!JbC#2vx4J@i1zKvC%N-2-p#@qpW|wNE8Mb zGrb}qA-O3BAvrxcq~g>O1SQ`3ChCcw>Ja^CFXSmR9Fo(ap!V3t{;&&=%ass zhK<|m*gxPQB3#j`thif|Q$IH=$Yg0npdnDgcnx?5=D@%Jvgr0o=(*3!6vu?F`#gqE zpe@0o|WyB7G08e=FEAY&^-CWE#OB2*z!|P2WH=^C;$MTJ@bb0vpD~%Iz#v z6+5<5|I&6H@$Z8{{Pr7xb7Gn%XFh@F-;D9*Qsufye^sTXrv}caL3CvB$fg6cpKYrv zc3zYF{3&no5)YI(w^eN2p3#z=nEm{Mft8of9J^mZb|0J3W38C$=bY{kQSCojyY;mnT0R__ zNF2K$T^e0?-Otz0@kE|HP)~rbGrCEf^diIt zbzQC0*OgLLW2l%r8B7eo@SxS6-V8&%eVXOv=uSQV{|G)5lk#YjK&V~UHiwBV5U((b zRRIYy>1qx~z;VkUep%)F&a@V?J0dPS*`{_XTu}~E9&N;G+i7Wy_|@f-&)vY+QdDMQ zslYM|%WOP5gIq3+t*I1RS|h}2+fyEG_f#;^CJiPM`i+*>NZ#w?>r?YM)m=_vqJ(d@ z3XT&7T$BRX4Cpx6QG`)~P8ICmm$Iqs(y4acRT`jR%$!7=I zad18@t4G*Ibo=)^4Y&dIsR)NZdx;Rqrmek0_^!tcw%9JGd-oC;1>pw`npnt z<;4UptEj3m)=OZ-r>Sm??w%lTtRLg(Pd@cE3U(Yc!HX*cyuNFKD?>_kd4X&m6c6i9 zKvj*wWz}uiTLL_qv|=ur{SyfC{!176?05rP_s`;ynB{|(ETT!Qb!mOU=jo;#1H*Z~ z=m_)Lv)2$UVEr^yG;pgR|3z?HQXY$9B*U8#s0f(0@~{yw(YXLtJusu=>|B@mA*L%n_BOLYA6d8Ta@ z*<<+mIATI5K|}&K-!qv5RUZ)#wK+^+4#Cix|7aCJbf=!zLUxCC9!Zsi)>9H)1vZ6f z617RNiJLnKnne9^N+Qb$hSs1RM^akI?zppU-?d#|&p6v6;lO*cwz-tpJn^$E9d`sCP;eSP zs$cQ$qgU5F6wN+f9nCeBJ6;Unh94N{9}AxA84sV`cQJM%ZB0Urju>MS-CbQHIt6sv zk-2c;LN`)b8MQh4ph2Tn2pN%p6>E$dtB^-*T42nzb_~dzK7G1qHcv}%w#YCt Z;@?ZGT^{=1yjuVO002ovPDHLkV1hIlVF>^L literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/kiss.png b/data/emoticons/tango/kiss.png new file mode 100644 index 0000000000000000000000000000000000000000..8d0d2e8b13cebb0e625674ce0d03dc2e3efb1020 GIT binary patch literal 1345 zcmV-H1-|-;P)=Ob=Dw-}?mc(6i<4c=R)kHsY!Q!j0P50cu>6J z6jlW!#7$RcaReOq0Z9F&a=bIE#r&bD%V7?vohw(8LB^wvM6#V-d!l~4LJG4J_*#a_ zA}p0y7Gqh8XUmzL&0?#U2)p(~NhI4d9&Pum;6|G?m}vMkyY@s2-XOk45VH_WIYRo0;&QrYnyouJINfsvB?6{` zQ)jQz^6clF{%sP)fH)KjQ;?_^;&sXFYPObEAZ(Hqb!A@RGGI})pq6ic9A)qBMoxVH z7u>Su+pa$L?r!2lR|=6^7AyG0G_s_%2qC6`+B~)KAiwVZ zl;Vs>`TiEv2{QK9H(b6t&4y35p%z4E5iMf{}U;It2 z6Q48{q-=qQYi-p&%4^Xxl#4y#G(c0>_V1>DK;%ob&M0Q7`uk_0NY` zu%?Ey`_k0M>#1D3ma>`}oLCH-%W<{0mrLin+4$K?Vo3{{#R)x#_J5TVrMm1Ef;Q$iQIa z5*?)>A5~xTw94_$tYaE*0?WWov2q{89uG2BW%x4yf4VTT#38gP#LAb0V6S7`3S6@a zIo_GoJgsxvcS$$RydnEZB-^EBThzA~Dc<-FL{I{V8dL=_g5rZhF!m^pX+S%^%rG=~ zDEg+pJy-9p?RqTd9Ee5&uSs)D8HwfM=lihU2_58FbY)Xk^tG7uAKDn4lr1F{aTj zjSE-qO;fv21EkhaS`&lCD1};WmGXC5CSjlyhWYc}y)GyOI?%A`w|aRe_ndpa``vTJ z7{ik+^+eeL`>|-7SebpaA~a)jR2-TBjLmf%E@SBT-=d3i#@&oUHfzyBhs4Fpd8%S{ zQXlhC5mAJzm%;QDq>|c3~o$eszn zcA=@n(L2cHGU!OTaPm}uXkF)`L&u|h>jV}$BrYCrrzDOA^3E8ALZ06S9TM%=xHgzY zSs|1aVqhpsqP>r6LkN4(Vs~>(???7ml z`k2qjj{_QGG#E`R>O;g3jcfx7pqGe6w*#8GnleNl4v7J`kVNWZK7{5X2iP2?BBI=- zDSGzzVGPK!@WTxVBC@iXAP$H?YIu zM~7m=a_8pgJGdXC79f0pl?%DPMXNvZ1nNu3-M+mga)8{OJNRQ>#1x9KRR_lAI_asy z1OseqEMd6yRm!)RgEInUZ&siz52UA-xr!V(Tqc#&$_^1ZZZlshY#K0Y$^iGhikjmA zM3q4*sd2bmEMVw2Bcn@J>C=?OTz0?aBV8gJP_D083qmjW*%migrF$_Wqe~e2SMb#q z`ESv+XpN3BF&Xz%J}(XLU*(sd4AD?Ik?<}yl1k6db7w9|^G7~xUn5LR819aa`ESux z?BJYnO_meV)19$Y9A8C*!{3)OUYTZSdYt)%1@eUgxqO}nnG8e8aR%ic&EIV!7%RtC z4MJ+c2)Dm#otPzvn|~N%)A?h?^ZoJ9jb8z2!yrZj@bxA) zm}?4IP849q^-N99m;N>Kz(4%;KhfTdR%xTmoKh-%y}y6XtRda>$!ut7umk1gL7mgv z6(*5tz5No(Gl&1_p|*&ZYtzm8_;6m@O{+AINv))F}prssI20 M07*qoM6N<$g4aA$@c;k- literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/love.png b/data/emoticons/tango/love.png new file mode 100644 index 0000000000000000000000000000000000000000..5171deed6713928ef6b1837673212c0191262bed GIT binary patch literal 1069 zcmV+|1k(G7P)Lj6l4?k;NmFezR%+&+nb~XkAQLmqaT>%dII!XTYkh0| zd$ZXjB3R2%Yg)Y+V35euO;o%AA_5T=mE3Jn57D!xQXgoMkOFdwXe!72Zl}nJyr!4v zX-lb(aw3lafFk-)Gk1523={=?Z5Rg~zyIaO+S^OZfo;8iJw)xkb?f@K@7w1yOcTgYE`oF_ zg`q=-5;~K4%Czktws&>;yk2jChI2WbID9yfh{c|HL1ZWoSn}91&F)P*b`%!>+h;hA zgHUU$4Pd0Pwbk-E&I*k(O>Ek^v&3TAx_4D*56>FIas+S*`R z7B~`FefQ<%$ap-sHZeg294r9m7cYtYAtLXeK6WfY_4Odf0kYY9f3^+P(16ne14(H8 z-d>S;R{#L))%rjz68UCgcsOZoUZ{4zBhQpW`9!|w0kt5G*{psD373VF={Mv6* zQ&+B@Kc6*QT7Z%g_u@Vu%&l8-_0px>uTxV$EiiYv$yVG&0Pq!2xuev#8yXrOZE9=t z@`VeK+qeHos;Gc!ZpP)4C)3v_CucLvPrfMPx@oQe0QlJNcY?`ePoT20qhClS&hiWay+Qm4ry#!yM0p3IIezOdkfrt&I3;+PL z22lo}#pM<$qIg|ZRjjYCPh0`Z?W)_h&GO`O4rj?W4Q&f68zP1n!o*4oC`$%sG*Q~G n)Z*yqXzrd9{NJ(WuZq6_OQXw4$XpyE00000NkvXXu0mjf)I#sZ literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/mail.png b/data/emoticons/tango/mail.png new file mode 100644 index 0000000000000000000000000000000000000000..027bdb0900cf2cba554fd8fcc74438f94a5ddbda GIT binary patch literal 881 zcmV-%1CIQOP)K zl}~RQMHt3^GZQENx0XoKl3Zd6-+-*MR3N0JX-Zll5m4?Ghujh3Lm(mX1vphgjhZHP z1(gYP2+%|-ibL8H2TTz{!in++$KACZ@9s(e? zilG;WPnGS?&NfTgMUzUt&^I?X=R=D`v$M0I)>`Dg%CWb~Y6{(17MlnDZ# z-Mw9M%Q=iOrc^4ar&YCPV2mM?vB}wy)O1$yDlsuRiIucEWb5c6 zAq0s;oC_BwsMb6dZ{0*mg_X3>TBEhbN?J%MS<2p|Uav7RF-{_$K#0z!>`DNjlp-@W zMmjyh{Eh3B%ZCgM3^F({NV!~Q{>D7%m(q;dHcF|#&vwM`l&Bkw*5sD&F#4(uz~+wy zwE3%=W9dCRK^TT?7B+D`kC~bGsMe~it=&Ti!PL|gu~?jMR#w>G+sC$TRF48Wjk7}{ zV+@{GC7)j*8jW&!`Z9jdAkaR}c?ajbgCF`d{047LPZN(P$ltw-=X!KA=8g)sOC|2# z{{d%gjB_u)LZwnB)FF|c_N`&)Yoh6@N!FkQ$xc#@C5<;-CQQ*?0X;LW* z*R6mQDEZG<<+_gv} z5z|IQ1@r+CjA=G~2>1q$q%d`DOwEMK>(O<0YisN2*mosp;@tRnPwJr2XZrkJ6Utr* z5u>zH#^^xGFd~FcLp6dx9`*Lt8ihjP_+Xq&@NeM1|71P^Q5J>D9ZOYN00000NkvXX Hu0mjfr%IqN literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/musical-note.png b/data/emoticons/tango/musical-note.png new file mode 100644 index 0000000000000000000000000000000000000000..5990ae544fa3e436d53fc1e2ccff3cd41d622540 GIT binary patch literal 941 zcmV;e15*5nP)V0eb^V7%g3DD{G4A2G^fO$lgMPvoldEjUm*78Og`!tTiG>z&>9P!Pi zpYE-KPPZ2VeQQmbCb4TYqIwd?QJO@=ah)^{@v6Z%ugLO(tZ+Oy+nl)P?m1c90A zy(hpkCt8%Q!j+EFRkzR1QIGsY}i%)Odf2jW#Kx8uZ&oh`}>U4XLi^$u+ zNmYGRM0Wnlf9PG*>GobR)_k%!*NSJF4Vxq(qjAyN8;o()I|@X%}%=RAO{KKp#z8wu=@ z7tUP#LEbyD*pB8~Gel9HEHBx;HPnlnS4SwPCjhECYmLGADOrtk1RY@L81?mZ_p9x! zc;cDXzcSxV8TE+S;e)!i5Wd{|sKg^!?U{ON;GvzIB8ss*zA3<5-?uc|6{2ifWN zUKEitik?Ji0%L>QstvEa9}X{HLr76cL8Ck4&JZ1<3`L~>C&2gB+WvRv&mRQb)!N#c z*=w#G3rCw?yiW`^QAj{TRO=XH0r0*;qyWhUMxJV<#*B=}V5!~ipF4L>0VV+Zo}(u%zn`~9o9@5}CY#Bz~*wJ!)0 P00000NkvXXu0mjf3?Ij0 literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/party.png b/data/emoticons/tango/party.png new file mode 100644 index 0000000000000000000000000000000000000000..bcc7a4ffee8ba070364f79f9a83e284b82f0fc14 GIT binary patch literal 1488 zcmV;>1uy!EP)b#*bzQ0`m-+f)z{er5pf9Q#Ts`CFBvVZ7_08p_kD{mP|D_nM6?}Dmc29m2I_1rr@ ztMfldRF%he9LhcZjeX6&uN3knuI2K2Emv@|A1>FgE@T=C%X#y?UB8-$2TKD08sI!G zXHmw&7a8R@>fH(u05Ha6jIo-%l0#U6#JW0U>(*X7wyDW}^Tju44gOJE9(m{ifFP+-v5f1~SzcJ?!{k+JWrxMt3>PM+_`<;! zaay7{RB@=`#ACPvcNiYp8{xr+2Ta61S2`v~I44# zv3tKmAk>ITD?V|2HsaHc>o?;#W&Ex-;lQB?iQc0nm%}#&xUTn)!nLArM?!QDcC(?a zi5H$d#qHa(+`Fq0WCKVHW3^zc7Q|`>69W^Y?gntY3^KAOB0>9T@vsc60|0Z&*}T)# z*+PD~$S+SmPkT?AJ&$=HAru=V0;U-?al|x(L_r!sY*d4Il0%0_!ea9RUlr?yCIhL= zm5lY;Z(n2T!X>`=@FMq*2uK{oLc{`TKna7{fcgax2Q+|U5V5G&vV;xpMF+&`Q}#0T{mKflDtqc3xAVh*T-U*X)uG$Z?d!T5U_M0CYL z!K=?Bw{2-exOZKNp8EWA6C-0#6>egVSfGz%PJiIsN8)Jo)wQ z{Qj*Zs0&(0aq+z(Z0a%u*!2-WR9ALTIVO2M_?->a>S^4J;HHB3r4UzZVG=w0nJwD% z^=!n~0JC#BdNv1{`D>E)N)9dLFcF7_Z5H9Soyf@_6&g=yY;6zLbiIa`sZ+ZoIPW{e z@9MzIEwN=w2kI?jj74vk2U?+13OJV*aV8hA?+g*x6oIxI{jLKvz?fF?;-dW9eb&}PEDz<=S2}6&5l2C6HvgsACj{|TyOJ;tDX>KtM8`IbWHlP0S zi-hmK`{PO8pvvU?y!-et_U(!UVvw2laXGslpp5BsvSNK|cf_r{OX6!bZ|@$ar>g7g zxGE2rwT1p8?Vx3>&?nI8mnx!+tpkKFGFq~dGo1%ET~5}kbE(Cj|9$h(U#B)r7?G@7 zjx0@1o^fso5D^(18fpgOQT5{Ax^wn{o@jM@XVkf?#c~q>OI|QNmk(W-E`%qao=Ti4 z`zEVKmyI#m^J8Pny2f;UiB{F$-yevywMBi$Z!&1q7i=RE^u;$ujjvEc{IWnk;QM)? qGCn@O?r**&;AWERR~Yoa4Ez^E3AQNb$!XF60000P)fHx#M5M0Jg`e?PO$|KrBQGgtl3fO0BBQGJ-`G*(HnYBP9`wC<_Ex zMp@(!h@vi_t`sR#RZ)YREUKYcl?ZK$)f((EV@QCxGoCwl?yqyYFeZR8%7$-sj;`+e z9KHI!_Y-R^PqWlhRR^3WeLN?k6Q}^kwARN{V-5mnr7ovwx&YK1sGhVo-AO}svglKh z{e4QEFHB5Kjg`x8VSHQ-l}Z4#*VpkH4RdpOIq>TBmB^SAuUq@wPXc@(qGpwP$H`_- z9j;XJBeSz&;uxFBU~)Oo3KaxMoS+6sk?pNj0yx$ZxH z_tsne((Pgf2Vr0U1_z-(54kMjIM^hCpa;GWoerw$VU^;`Pd@Q`L2&dzyY6?Ohe6z_idS8jBC$mU>ph&4ap-Os-Ot+67Y6lsz$Ix{1MiHWIu zo%b*RI5t);yXMXvuoi1ASc6zYZEcM+fBp+33AWec>>vL`?fP{D5H>BgfN8ZDE0-#82W0C|(Q*4}2bY0e4E%1FL?9pg8DdzLoD1tbJB!M)=gb{`D zaiz6Bz73EXb8x6s!YnO8HVbjQDG-LdGBpW~gKV@hqN&VIW8(xH1Q10?6hWL|dOe25 z$JrXlhXD86Dvm(FhFRzah}OJWeim{$Y-sdDg*b+87lHr@Lx`fy?s}wtv`DGT z?e+DSclGs=_Cjz1{xh*RFwVaOA(s)DRXHzIrUc72(Lr8q(=NuZJEP;pW$02S9Bbz=5h)udm!% zS=qn0T$W^Mi5LELmAU0r;%pYlIQRNqA=W^eqM~>+GQu8ua*A8muUoHPUvZ$irF(x4 zPFkBr#++EJR=a7bgc=@(P6syHIG%_0S~y;djOXEaEwtA{ZEQfhjmo*0kr5WF)ox_W zi3j8Rp@s9hwcjOSc>3b}yl)R3!Z~mN{4R1&$hKP0YC*fbdG~#sav3`}$Hn=1KMBLr zkH+?6<9aHSc|Y$sZy&By+_9-CF{`WCW)tIio3APqkX^e_lat(BS+R@NYVWSKAAdGI zefFDgzimDdFgN#`tdsY5zg({$`Ni$qAMJVOXQfj4xy-Id7EZI#FpcHqW02eWZJ0nqE~yKy^{ z`S)!%e`VpBi3^cZcdfFXQtIyITFtW$i0-&DXJ%%c{=It#OlXD`cEAWZC6X~>l(NcL zh!E2iXgkKVf#A}mOWStyj)2FNh{)F2{=^^u4=Si_B2s&!2mk;807*qoM6N<$g2e)l Ao&W#< literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/question.png b/data/emoticons/tango/question.png new file mode 100644 index 0000000000000000000000000000000000000000..fc657b46ad623ef1db0a7d71fe9408a75bc150bf GIT binary patch literal 1491 zcmV;^1uXiBP)*33(vG`wZ{P0w?{^;mXwjB}G1-%x z0H0^_L%BPb!mTe5%O$rlqW15v5Ra>o4@2 zJ(SxgXYjE_T-E`okGF9-h0+=7qk~t~yJZENpKhNx`O+f`upV7~qkQcrf9A-GpJ8Fh z@cvkdXMWJGukHA)Qd@6`-Zq_;z5=OXtCt7zj0_BFf!4@Dy?8T#@vNTi9~no*5DNHa zu;6(vHIV>Ls8Y&nW2RUWp-6!uC5q81-1u69dSgL|jGDe*Y~fUdUP)~Id%+_sNWwi_6E=I>gL3W_jskf zA0!N>npfH{^7HmfAQ6yCkRYmlJjtLBwuB_e@7N{I8~{Mb>W#)CDc0xLB_YweYZJdpoabjJ(tKrG zAA25K0#XG!iBRT=$72}ruyz783$}o05z#W%%Rw~G^#?fz@R!m?|LfbE!Is(c(8u`F zfxn{mABgw>A)77nw{ugR?MXA5F7m_g#zAwaW`R6VKy;m_$c^7mF^}yosjr}7mjt2PnhJ|dtD+)FP6fxvbI|Vk2@l`_l%Cwc-2>3$f?++dA zU+~i_z^!X@?4-r9i60D1A3Dq2;InXR(Y9TdjaXB>)Q17Hp z?Fqg1^q$8)9WIU?1%jvzBgO@as0I*mKn+F|RS(hPOrvw41)RX`=;c4;Md_Re2=&pC zbp{4W4#oXT@0L>bO~3=|f)o((gU;N0Ea0KGJOj_;4{d;CQW+T-4E51*{oMbW)2r%f zfmS(|$T=pC<6j1L95jR4%(Uh~^p%?3ppzizANxo$mUmZ@c8InV46Ny4<(*F3_&ZZZSoPHt*(bW3LXY_ufFcFqFLdi+69^dnj4^cO#N^ zbKz^fy(f#e1c-<uMag0pObFzcMrtxNvzg)cf+K zhLbsK(rR?f7?XaxvvW*m9Xem4nTW+=zVMPI5nHqi4H~gR(1`f0SYy;!g%V1ljwEP)MHI*XXXd-_C0RmPHbTJ$YJ#a+O`ys z3^6nQPZKK*7$SP(KLdP9^z0B(-(BJ_F>v@K(eAs}@+XO2X{14MCHjo$h$ecOnfm~w zP1pURXUi7v{OQw`$oKmJ1g7hrp&4QG2nB@0phaiRvp;I*4Kdp{mKHur+onU$YF%DGL44mBA<07QUk{_W5jh$4U)uq+Jk*%Q{7 zTXr*-{;6R!R0qJ!jNE$ZQrwB%WqoIlusJ z*XQTkj8X|)E-%hLm4big4kYjg8q_)#K+He^<AD}S-L}nX z?d%kGb2I8QGnl$^B^n(W@uPZu$BWD({~7=QpAfCGqdn7PShG{&L~#n&sBbY%bd-jo6`E((c)V*AFD0GJySx zc|8GU*JEpNxi|sC_tKn*_L`>s8Z)b~Ufy;1K;_J`Pq1J|2FhQ(J5YW+O;v{(aP1a= z86u5|_WRfG@7wrl2H+-UEcu=+`@L9ugWePp{oEJ%;po%0JhnTNCmk1>-UPaxHvawe{kWa&a>;*ndy$+-rD%N zb3rViySv*CfRsySQX47(O$mTKLBb^x5!g1s6n*n$$Mej>*UU8mTE#|KaqF`3Pptz3k9#Un45&25&!@I07*qoM6N<$f}8{P<^TWy literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/rotfl.png b/data/emoticons/tango/rotfl.png new file mode 100644 index 0000000000000000000000000000000000000000..9f985a2e65780c4c0c264c3a3fa87a1b337dd986 GIT binary patch literal 1318 zcmV+>1=;$EP)%= zFfl4YjK&Q<7N)u|(GXLWuuwyy3zsf5uF=Hc0}7GYf+V(qL=amo*tDH?N~?^ekLh&g z`|fek2jBoF2Y}DPP?j&#LAD}n z#GzR{ngG<{JRa>RouOWia7g`I3CIGXbgQ%wU-=2m+k*hwek{x2r4&^)QK_pbWJQHV zN#)$G)G&-CjrR4=Fw#Gu8FohwYR~Nj-j_jMY>8#9p8yD$E$G)@z}^JmbvIVj+0xnnqwVPwCuf)A5Z>+Wfx=F#T@VW zo0gXv`SH|Ma-PGiKZfkw6&AOkb!wk9+!PUSOL#iSB|?Q|*n+ouC4NH_{i6}$u}Tsn z5U-A*1Q;OQA#X&qIFY5G%pp6NxFhEP-sl)^k%MS1Z*4IHbvRFIcoUH@;aZ38&5u%i zhXPYTpdo~1yO0{5^Ocu@M>}IlV>!iCJR4xVP*`|p9oMBW5+NLo5-uttQdEQ+2wW2g z3Z>h^h!usgq`{*-F9VcL?dzYhA9ylc`tVr-l{UdEQOcJu$8oOxcD?z&&*a1e7gM9G z-&24cycg~Jc1D!WybMCU9ATt?fW(lxp@(RA*QIxMoOC90OEhW>-IG@++ZH5TxfT)$ zWu$)~)XR~4M2FPZ47;W6Xxg?KD1D@gb)UO*O$_nJ$PoWdPNJ%)Dl?f3W8>p=UQV#& z8H=Wm>u{`kr0r;0Gwi!C?^Pq~ zC5`1#ENcjCHKkGk+fXa)-Jc}W)awQG+}FW;k8Cu_*w5_UpDYKXgqlFiDlpX`Da6D9 z7o`HW2$}~wi!e*nse><%de-wdEsg7rgWcjtjN`h!I}tL4wH6T1VnAFJhwbwK*P?uK^ZS3u+Ou^nLmZXU~tpVl-#ZU*+(T z9}oeN#fTNc$^_Ed97ou4OZLt?aOtA1!PsB|BOz7;j0C7wn3(cN*Tlf)L5ien;!IA> zp;`hBKr1K#HmpEhgTbY189)L$HQk8%zXMUklu_%0Dbl^Q4kUu;G}gLwzqlUN3Ti#D zKB6T=E7-6A>5bfakpFdXJF_wtMb!seK*VL=p4A`;G@QX0hkbh*fHG(vRSzfvKBASo z*j!y`o}`Zz)f>>Ov#7ATi}jvZ3=yd1D@O`gWOdEVG*Id-Uua>eJ& z**xeRXaOg>0UaJGq6{qqB(BiU1KHjkC$mmM{~|V+M1$)KhupOow;m_f0ogIN0|Wk4-hTGpQ2XJNXMUT$ zFQB7iYt&76tKVtKKJ(gRQ%9=SW$I09OO;vEAWIvLy)mKTKO+@?EPLaNKO1|$&DQ;D zL~=nPdGq4M)1?&wA|f4|HdO=Zl!oc|pS-qheae5TK2_?hae@rM%`h@PHW&S4IG?z9 z^m68O!J3>JoiWDbE({IL=n~NtTV`8Zo13hyP1%xNWzdurVn!rl#Tui=DwGjh5SVkV todb&J&YfGC6bUq=mK&EyXrA)@l?)V`3^BD1JpmjEOFYx?m+FCS8c6mKQ_k15Q7OS#RLP!xTqqAQZ0(sLTRy$oqo>Dn|bfuLa`P>8#-Cs)U)2=4J|2C_>cOc=?&VIG?wA_Jl-YZBwn%u((A##mO|# z#pBO3*owILg$$S#U`kObLCkW*S%wlt$fMyT=or{BuxTO@FoWaDH(&N7dT5$0{9Fdv zdbUwfQ%$N`3f>?h2E=908|T^l>{smBcXB!#Q^JRzp5>#@x&Rl&fH?StaY(HbDr%~; z^=w;ck+4Z>Yl?%yWx%3pK`jU0Y9bMLx%kfr;DgQ4ak86^?=B(&A~Oysg#I8>TT_g% zX;$`HAGkDAUTJJFh>;Mh0Y(B;eO8sl00-e&?0FZzt zEv-Vm-+&^-6j1Ad$+6|pGB8m@N3qtWW%F88ebi>adWhx`^|4_VN*kDM$m=th^Q?eH zQT4!P5pmi1)N+tG8jfL%!z(XT0tL`XR5L&h@DTNHl)DB{moCx+kC05D!8n*GV9*dS z7Lfu-6b)tCa4i6N3|Z7pfz4ptVn`26Yc0qi;55U751SPwCiDjobh1`q(Sn0IZ-<2YP)Eot;$Iu^CL(ckvK>ZNQBw{Ua&xjS|7-R=7_`}z!{1x-Dn)q zP(60YiH@ve8gL@3!H$EDqIMK!j@UHVQ8XMynC@ibmf)H?Fbs2W}kOi;nA|oU=a>kGw8h zTZ&1okd1e_oHi46{dDw)U2wM`3uJm4QlvzpYL7iANu%0RomzN<-Zz{bdXJqba$W1 zF9{G4Y23JR1yGvQuypU;m$$4{_th{)`>-r~dm0oln@S!yCm&Hw-a07*qoM6N<$f)h7W*#H0l literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/shout.png b/data/emoticons/tango/shout.png new file mode 100644 index 0000000000000000000000000000000000000000..c7aa7a9d703477caabc0bd2953a007b93c64da7c GIT binary patch literal 1288 zcmV+j1^4=iP)C(NiLX@H}BF~7b>iiLal8bPHNO9IPtST^I?Y6^uMwqcWSv@b@oX&~#-|5OJn{nBn? z;a@r07b|)b_!>pbMljVNal}Lb2c-ly54sFCgODK819UPW`gc;QC zo~Tb6Yb_!UNQnMVerIaNf~iJLg6UbI|KrOb5s)ydL3~+2yL+M%W{|@Yr~u^JI7EGO z6Uinix+z2qh=UT~^0hQ4zxWv?3Z{lnKO5olwbZH`0}`NA%s_IRP~Y5?YvWL*17V-E zG)KJRUBIGhK`kPdL`|5P^fIibW@au!q9%d}h^(eq!7rtdmgWe;zWcHd`@o@#x&~vt z6h?fkdKmFgEzz^T0i+7-GDx1D1KUAMs1`uI@*WTCm!Ph};Lue85RaXjXhhvVfDmGe zsCB{QdH3x`FmXf|u-4(&J3Fv8kJ>C)7tsQuC9I!=L_7Bz@=*s5a#qBmsJdWtH}Z%sfG(|mIRc^W z=-7B3WwZhiz0D9a;}c9yX)wAM)#Y-a&9J+}s{rio@KJve>>_G2IFSyRoK$AUC!)6* zs&sHpeRb)T3&Xj9X(tfe0yYD>z=6(%3c!KR3_=-ToEjX{id-1ZsY~zL@%_lKi=Wb4 zT5?b5@cC?2_;sq{U0}1kx+_KBky+|$i`3N?dGAP?&Rw&pzf4tZFQM>3bm)9mOYVus z$MzG)^;|x1JQfXpDBV2~l3T@hZ(_X})GPl<;%vb<^#sBlFgc}mc-T!Z+b`cAZ9Dn> zr5g)R1$1=m4>{3n?Xhi>ue`M*{dv{aWW8zGR%JFf$l3?T&ZX49GhTA1Cg;BTt8w6) z$+~MsWZuig?%lk3v9K;cM5JTSo@yWwS3mK=i+2w`A9r7>j~8~<1iU1`JwG@-wG{eu zY&m-K%-_k2Icw(C=%O)Z{`%v|^1>V-<>s y%?T_y)-D10D_5>myv^$Zp0q?n?(g+~2L1;MlSb#GzD}h80000VW&i*UjSOma_q2H7;w#X9a+|bmDUnr6bzSSx#y&lZ4C^QKn6rB*H0b z8|tO8p@Nz^L-nG`$MhW-9<(eHoNV~3YiIP|Z zV|fnC0(@IZp^(GY%@#t1t#@YUh0&3db*$Mwj;W^p9KDP7+Z4H(8+i--#nMu~vLz*M598kqtj z6(A8*Lj>{wUEdOw2&dRAP8lG#oDI}A)RU-}qBn?$0dY|rj(vNRqsKc@qF}1{@|)i| zdc1SWjRA2e6~`d4M5t}3&n;&|xdUOfG&WTD#eTq|YC$a`mgGmi2mfYP#G32b$r-Ln%(2>8DgkgFZlm5lY1ozB_Z9eTTX+On}XR&0?Gor0+~>l{MMt zBEy5$xK*GTL^Gfnc5M~z4Q1K3=Qfuv5Af@i0k-YF!QFcqc5V%0gRx0~W)U+Rh6gP^ z7t143I<%)Z@4WO@%+LP@b{r&NZp>$AYlNS#I2`=^J_zi2H^S?y6m)E=(K%2LC%hEx z`93d7M;Rd6&DpHe+ed1kDYW2KDP}JL0psHVU!PVkcZS%#-2%cF$8%h}Zh2>;$DFu_ zYMxMZC8Scy-QK=vH)qQuI;nwrY?5=yoMW1BLJPr;aqvi(syR819T;NG$~0?Mra5-t zK2>w_d~(Eq9mTj+xMmr0E}2u0P1DDB*)+|5An%i?Z&L-%>$hu>@Wh*1wDT_a=+ng&4OM zC(;b50kz4bH#}iKd#_{p$3OgX{lOCf&CTn=ZZtb@`;ycvZ@)17>8!mRV3I z(-)2%8r0zL-jX+v8anpZqV^N1nrlWR?dM`6U0oLnGXg|Jnpdr=1ghg2RPTDR|IO#) z-pjS|!ip-#PXLSrp*sWP;XivOqFsk?CNAWxNvqLOV@$fUqhnO3iO#q(mn~c7#^%qD z+k%~I(6|*MMkHj#8l%Q46cL*f7*XNP)%5k@@oesW^5p)1`JbEfKccGq zo@IVlbs)U!?Qsd>y^JFqB;>GI_5*0h6)YD}rt8<3>M;q@wA|Xfxr3%sj(}kKA@*@zbfmBf~ z;ba*dJ6e}2XE`oG2O!?dIQ_#z3=T=2&Ld(#LX?2HZ&O})GSlE6FHJGC5aBT> z1|*=!S7C6s&_6sB?`6ExfpAdv4A*S_4d7ArIJKlE#VY_i(I@s!)tGR z!iHqTl9A&;!4>n!p5Yq8!8@`)&q2sx`UZ@*^B8f&WLT4u`jGi{#E$MV8}2*27wb-Zn3BenC8gD7QlRjTvF9o{Un0uC2NC{!tcHjs zvC9PHl!Fwj5#n8k;!j5nxx&)iQ?6Mjam%De$h;@h97hG~0=xu#{C*6;)6bRTq? z_Zg0qM%aL*x!YPD4lEaF&p%Z9WgO)q2p2G-?R%H&0ewFd8npJGiE6F4+*P|7iZqr3b7 zdVMkxWx4}Uzs6(~%-jfXF6`Uc_n7>5<1?&UoVB@2S(2V{&dPz9)E??O`w8MOYW%iWpZ!xFp8M3B`JXO;WUoad}KD z+|K=#{m8kgSW}K|j_+TDUGt27rA2yLr#^@tY&^@zwnK=|@IJ;D3BJg1ae_wa?Pr!V z+Vc>HdY^&G4fPDCe;Sb`8#flOUOk^}2@nw(J#e54Xf@Tf{`TNE zf4RSzKGNUJ_IC$%5Ma@j=5DT4zP!F(zxvjH2G7UdEUD2IW6aW(>FE{SB)a9w92pr2 z8@;_vpZOgIZF-?1U*j`&z$E%bg3NG@Hv)Ulhl1pHcwh}?PC-}vDV>y3nm TKG5EM00000NkvXXu0mjfS+axy literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/silly.png b/data/emoticons/tango/silly.png new file mode 100644 index 0000000000000000000000000000000000000000..b944b7e9f91a3bd42753852cc9ef9ee87d78b07c GIT binary patch literal 1266 zcmVC9mNAT9kfwiHTIEyP3%x~mIAOLT|0X5oUB(MY;z6pbN>u8c9P zj3#ajHY~)nbp?rW(TEXB1*zJ!^q(~8OsDP4yqWjjJuaq&!cgLdo4nlna&y1$p6`73 z98p!C#-&#R4g#plI008s#^RFRZa2>3LXMusSteU~zgzJ&q;M>8<}q@R68Yz<@+zgWA_0C$(1sKZ{iX ziBeXzh$G;*J7D&2mC=#B`res@%Q;S~-3nKcgIvG>skT8*oJ@pGby6xW6KXXoJF(Pb zc^XS2fvu%j%wwB(2`5e_NVN^-0tOx%!TNx7nMCYYPMl1Xf*C@MBc=vSEvG)dj)=o& zr(Z?!!4^Q*z-AG$M4bls`h3pjyq>Lc-H+h7I1*z>`}*S{OIT|WaY3ReF%SpDMNK_o zYC)19)gUocBZQJc`}*S&V~FG8Y{K4iQGP#1X=-mH)h4C&>lmjF6&JA?tu!_v`S9hB|>7F7#s5wVD9`5&KCAAG~9 z55EU2A|SFMu|nuCARX=12!|^oz@?XlW@Cc|jD%PXFcP5ZqoId5d9Vi5FQaJz)govB z>Z1hM(1(U*gG+A(fdm}X`WDpt1BfD~ME1ra6YpLE3Z|g66<_ zh!zp`v0)zSyV!h?kF=?w1bC==OrE_)(`tm>S;Jpve?r3*3>h@cAi9d^TA73b;34Wi z40oSEUB*ezgGr#a0GcN|o}sm-hL)NdveyuUB$2lb{%XE<3u1mU&gA4 zAm9p13)Z-GpzDaP6O=5jF+>~(zu==`8f4cSu(V(axKf#9WlX20 z3(kSp6G7oOu&ac=W%Ty#O_MOXZ>U0ox}GF2ecKA^;Y9bMQ$G|$8LJ?OPcocyre~O) z>xwiUl2Yy$`foNO5--;vi2xhE#{wa0eV~ZiERpyNFgvR(PS3;{`H>uxtRw;9By!EPj)6|gH9w+`2IBBLXD^*FY*d_Ul#Sy1QSAy7Ao3xAx4w_~yQ) z&#PKfO{QZ{m1%62tqsTCUC{8KY2TZhz5CUlErZ|9HvDcx(m_74JTY;(xGjN*NOw<9 zEl{7-u>PIr?i_h0={?_+EFP?Lf)v1V7@412jozGEi%*=tow}U2Cap$Sj4|o)v9T51 z0=jL>?Ck7x6T5aLZPC^kG--vH5s6r_#;CChCB)_hR$XgXfx^|RS1Wq+wgitFA|jh> c{htT_15m_XcfC{Z`~Uy|07*qoM6N<$f+UMgPyhe` literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/sleepy.png b/data/emoticons/tango/sleepy.png new file mode 100644 index 0000000000000000000000000000000000000000..25449039c580aba51339f2279d3b1ff316f3a2ab GIT binary patch literal 1400 zcmV-;1&8{HP)PvEHzFtxwO&F=Vl-2V!2%Of9jhE|`ost$@MNM+RdQ(@1cSMPs8E~{X>IQeC4LRgDSy9BHN zP?w_w^rG}_n*896NAGK`Ti3b(5y1%brzd&*^*%2MHGWBgvz^i+F%D4I*dWb~mDDaW z)GRLl%#6VJsMUc~;lq<{)BEx}=6L~$ZMQbDbMtcce)4na>mNqH>wS4g`&^5j5t9q( zG>ON?U8i#iVhd(0eLBC%c4u_aLo2J!t8N*rV!NEODZ#yHi8GapP9m?}=5O0fRM z21)Z2(YMHP>YB~M9XJ(}fD5KZ-OrRwAsOh5g4Omv(9w85@jZB*bcfkpNX6wME3_!5WYPXaHJ3wK%i) zQ37n}L+v7iOZzPB-wGsP1vQJ&FpJs}LJ74VsdN@J$I$Q?qFFS|GB`AZ+9?Lpf1uU_ zEh6e;!zrj)%l)?YSHJV#fqpccz>wwLkB;%#S0{MC>o?FG zPyjqc{R`p#AyAj2WJbUwP+I`agPvu>I)}acMp(Zt4w^;7Gi8 z!R9bd1TrIKtT{CZ=w*ELW;4Iqg#HMk4hpPlOVGWq8WA52Gl&SQT9ux67NT0hhIvHu zpgF|U!uY5qpm(lCN}mp-3Qom!2t}|{U?&lsM05hRS>E|*jD7pZFl0d|5Y2*4%|srj zVhK8sDxmbuQ4l{tcg{%-k{)h}ELbC@++iRD8-RF-M0jwEi(=3)2ZX5gfg-8}BJq`w zPAg-n!T1Tf=PKB*p?YkU&wKKYX~Bss1Up&w-DY7X#Ad*jyGfV;JArYlam`ZX^PaqV zY-PWO7ww|tZv91l?|JR%%0**$6HT^*&6V5JX2E7=B0E#?X`+cWRK)H=ySsAgd(S6# z>o4bG&YcI(h&f$wo=U_cPsqlOO6sqc&^wL|#?atYImum!aqDnmt&kp8+tcHXPuo`> z?OXQ3!9&NgmnCRzy}iPX=dRq@lwP&t+VPj7SJ&5>=BB7wut?4wIQI0YhQFnJZ#aGW zt^UQkK1|pCXhbqWK5^#w@o$TlBoGm4UA=lfP?OZK=CSLC?z$%F-B6b-uBdi`dVn)w zWMp`%;^%?s`0>|%t^YP}O-7AQ7-KR=`}!vI9MDUS%%w}0x{0fP)L;C<7poERM7$dvC+fg<=Z)kun(VZ3F!hbpFu*dV+X3B|J&qBCU527RMO`(Cr#JFt?NN9q>prSKSz%zqsBTZ74H*S4JKPq z>QSCS*+!_f6pMMZX{&Jbc#?E;e=el&u@x-#Nry|uf9B}%WGTEuWC_G=23HG`LR<~t zW0XM)U<;sg2y;|>4eX9Wv zqy{98S&T?#u%6z8#JR{}@zxRK+c`*MOEc+aDFu^=I1nGhu|=@O zrN2H4I*UbF&@2{Z86EqN6CaMD(H!U^SPpTUV0uysxxQW^W57m63*Pf@B*Vf@(0Pyu zaUmx@DsXe?At@ca|BeGIfaSpgyy`vJ$TtNs23C5zGRcR<%XKiwYj1Ywwt|#$KY=6= zNmM*YVl4lAL4gRHx)C!`+7JzOxLo*u1a*lA`3VqS2B6eb>PLegU*4? zU^)X!S2PPcgGDn473rK`hws{uE1A3n99nDN513u*v<+K1IAxhjxvKcbI1xw zcInPkh@%Cnk}p#oKY(4nl(TYh>S5b{q+L%Hypzd9?1c36){x#QQ80vt_ptEaN|L`F z=QrZTJ7N5;X(kg)FX*T54zz##?T^3BJeHudbAPp;$ZdIh_xMY1Jv;qL)y{OIYu#Pt zwl&Gxg`-oG7X33?4(^Umo%?e~|5xJ;zdDg@m`~mx8oE~8kU&JFbMM|-pgv_${d>=i z9oUr$UT91eyXw3!4RAk-P28QY{$pezF?8lu`dVIH)||~c=dyzX1GBaUbi!E?D`t?|k2~by8M{jSh^Uc@aTJu)$#qRC-8FZH_U0VB463QFlVI; z!sR^vS zLJ%crQG_rG5oQpL2SEnyj)5Kpbrtk5ot-mSHw%+*v0nE(nAmC$tU$ zw{iLcNDh;AoY|8AEde%d+T`y0u>EY^16%8=Baz%}{{pM)9Y#*~a>=P8QnqLi(6d@p zn28=Z*Z2Ff>gpw8VDJihGEV>H{+kk4R90Lo$7Suk^_=bgiI6XGU@AT>aMdXPAl4t> zo=i^Q_4-JrYyiqD%5N$#^JiIEu>h4^!MQ)WB2(|CZTifcp9~f0kb0I7k~WQY4GE3`~QuoH6J2Q6>EQeXsvXUfW}!}s0ah~J;bggs8yad4dMWMAc^ zCG-73Y}@AWp*CU%p1-ks8K7#8Kz;o>Z(d=htf{eXhgq`d#mMc;jH>Dy3W5dX2l5dJ z#wW%}B*wV-_ZiOr)vJ=R3w!ss_qJ%I1|2(p=+vpU^c3G50U?B_U$v?TD5)q2R6e>Y zyxkKlYH+Ng1>*!f00Cn>D^sKKWMZV}yJPWB;|aT88+z0*jN#vo92wO!NawvWYiep_ zepy*SrB$JU4k&@olG3AuGPI*z4H<;81tg?W2_Sj=_;L3}o_PgwL5S;TTK~tye*hUf V?Pd0r$cq2~002ovPDHLkV1m{lPg4K@ literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/thinking.png b/data/emoticons/tango/thinking.png new file mode 100644 index 0000000000000000000000000000000000000000..9d1d752b10aa80e01c75a646c5daab990a6bc34a GIT binary patch literal 1282 zcmV+d1^xPoP)l2 z?)x86Ri5WC&s808pSpTToN^}z5ccBIBN00R)a4oxS5Su1HyPkNjn|TpwNkX$CtZ@_ z0!^(g($-o_W3!=Qb9Hfc5@x2Xjukwn3*+kZVfLincs9TnVoDKxl1TOI{(PETyF}pw ztO`hyYE~;a0*?Cv6mO~gI9S%uKap`c$#J#I@VcAGz>!?)9*yatwk=_ismi|&i-{X2K0E>OnC5cqOoR7`as9868i>T-?Q z$+yU6(C7h}8o;12U@Rg5NDUfGHQ^cn6%1w6E`lv#+*+8OtZJ=_AmR!$Q*WBOdJ~6} zh&t%Qw}XwU&k+}E;rc&5!=oO z(UsV^M}pF?K+N$|X%@YJUE7sUK7d!+4XzUaF=|7gf?AJ6`gJH4mFdEG`X&QQk7v2r zE%r&5$)tYcNN*Oxv~^v$0v0HynF+O5=YEp+8Z)d-ku*{ofWFhmFW= zRLWJL{b&2XHS=Ois)vua7OHM^68p zyIi(rR*l{_#>`$D8oIB`MAvMY9UUERX2XW8t=Jb0nzcg8h$O67W7JrM0I_9(dDq%` sz`J_&>Z;znCg4d!L>~Dx`iuwv1GdahBb)sINdN!<07*qoM6N<$ft<8 literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/tongue.png b/data/emoticons/tango/tongue.png new file mode 100644 index 0000000000000000000000000000000000000000..144318dd1d05253f5f2cd77705e2596aca9c4839 GIT binary patch literal 1260 zcmV=^W8h0Uqf4<(8AEt+Qw=nEW~PbQQXL|Gz4Wyf(tep5|hR>h8RPXrE$SR zhzrFSLRgs&Q4taoW0h!9s|aXqJN>aunCY~gnL9K0e#gZWS{+K*@Fh37$(QrI_kC~9 zIijjO%3~fW9dI9bdsv)e4+jzYaTySwJpk%5iqAEak=QMUxuC&D2-zs2l|kv12p4JX zXqV28C@pP<<{h=m>qyep>QGbo|RkRC|INOp-|&0|$S z!qieN;|MtJW03h%<#M{HRqs^X$@U?R9*+mDO;Rb}AC7JwdUZkI1=Ha9!SLk7O>VL;(~-xA|P=vji_lxRt1qL zNCedof#lEwshC8#$YF8n1d2Tzq_v}+WV=+nSwswoi{fzZ^P8MKKZ+6q)5OQ0{><6) zqpNNVh(ooKhvaUdwWGb*!@+t7Lcer&ME%MXU{ST8772JnJU%8D4 zh^$7e5L9Q8&WwXy9`nSETE{CL4D9FijNJd z(9&jb8K@HwpFP^V1ND9b!icG$)&o-_wZ8=qZv(KBxCKFU=5Oq+X|Ffq- zqKHILTttMgpS;Atu1;KI_-gz%LIJdZm=;)=wfJ0HOJwbHMs$3lK|q&4i#XvfbbO+OGEyfHyTveb6O&}7H57Xm)#X~D&4XRM z2UnY8%X4x5&gGa|T%>7l1hskGXdh%U%G|_c>=wiI4lZb*9*5*|y6Bi*oY3|fTx(EP zAF)}w-iVRkzQX*bJY6RojN623x{=H2qIw)!KfddxY2h7tn`HZtq+W>!_6F6JD9n`7Y(IK)r00V#zxy@!K!Lu#{b4s&*!=qL%rmb(zVKng&Sb0U z+}&WdwaNO0W9Mfz_-mr-O=srM{jp={i%iQeMkMPO<9Ei!u9i0x5E1Fyx33XsPH514 z;;E^ZcO|^Nt%>rUCdW?#+zCQ6(@Wvs$CqPcXKp617Olyu(M4lSc64N9QP)8?T$$b7 z-EMsA)`TtFO$JR^A!0;AR;)2yp-FJP+!hZp9 WdOZGMYi4%<0000IY*b=UbYX}XWkHOI+C@!}g$cwJ zalrxz3xiQp6Bopyh&&o&8VPDC&niL-g_h7MZD%@<``zPWrchE#*l?3Cx%tjL|9}4H zDXPkYJm!I-1O7d&>%~jgvld|~J}bns5I}u8u^dI|h+g0azSUqV2$?EGqs`JFVOpuI zsg~NB2o+U^_}qeV@G1=SIlXvkjBA%JYlc@#cj(#sBiJS;WoedBxJB1AMM*p=Hh&AJ z0#ZUDs#!b%&z}yxf2$m9OKU#!S5+GtAKsGQZAKQGBI3t6s#9d!nEp|rf9$b&N)PUkP?(INDNFFYT`(d z5Q%_MHo`6ho{KTY17?h-l@%Z%M3Xq@)Aal!sOC|Z0+&HFi)bDf zq#@qGcthT;0Ki)O^Zl4}Hz5#$1$6`q8rigACP)+whA_rs(}pS_2R4a0+lhbr6oL{o zR6*l2cR}7JP@hgtwlDLNG32FnAc7qK3A#>^dZLlN-}La?iBSNaT|9$Lug@gfeH#DT zF!IXVfZ`qd5pwMXtVs$iN42|anY{cKdGjma2tBtN@B7{CY>#vBc<%Po)&n<4jg0ci ztkcN4P2|lnzz|x#3T@qoSdLC6P&&w42N=W@^Qwp{&F|*<_`L`sUcoGiYJhy_3JEX% z2M5+FO2_To-tOe};-%{8l|*eND^`JkwjUxW8^Sggl007bf&&7~nSMeyqR8^qg-Sbi z2<`dzdh7!8#wYAuqayW@JS9=TL#Ibeuxae$5Y)v`FN_okK#C z5%}&Yx!HRLR7JqvG=0uhnMB}>YHc&P^Q51#CK@sZNZ)0L&!h2@@2 z0Ne~hSFa40oVhp>?b>xdaU|``pc)-A#te3LbPVYP&?#4DU0t0Yn?1YKW!(&emO3G9 zL_$uSF>0Jb4smILVc)r7VC=++6O(rHlm!2GL`25#dY_K}0i-xJ1SW;DmjD0&07*qo IM6N<$f^(2qYXATM literal 0 HcmV?d00001 diff --git a/data/emoticons/tango/victory.png b/data/emoticons/tango/victory.png new file mode 100644 index 0000000000000000000000000000000000000000..d0b635e9b80d6d1673bf7c1baaa10537fc0c116e GIT binary patch literal 1387 zcmV-x1(f=UP)`)7;o~F z`ZEC!XsYQIS+_m`pgp@oMvqypztJPLSi)cus{#_IT-73ufaA`A?p;b|N~B{Wh(OOS zUj~1DqVidNndz@lk1f)%J`u#fBZz&AbZm4_TZIP;RTnYU7c1+yu!5qUCP|I_1KZibo(3k5syo_qz>EK0^+i}`y`w1 zYhzc}5bY~31v7^)_YSi8{?+X19%j`;%YXvTr|%#*AhA##j-FxD#Ix$>pOh0^xmJTE zH4+AC#28S^T{kaeQEDM=O9iG&bLXuqX>4#<(OQXkC8E})L`!OMy*TbKCtP~EE(JdP zt3+i~2el2xMj4DmsD_AyoL}#uwn$?t26hTGO=E)xS|C*)0}Vj~ln@)8hWYgdmyR=g z07%GkY8ucegW3{8iT!;U&;n=;b)vlIO?i(Ni8-nJnr7sS&b<2EGV}UA!$rv#^k)tH zXSDO9T&&`HV#yA$xw1WN25frzWXj-ZidfZZ;uRavogd{i@SjP(pg+&llvykDX)z}Y z&QnzhZ;Nc)R7q-~ME(Fa{0$9Hl#|>A7`G0mq78;e)OL3I<5TvvM|v-M{Zb@#+Y<}Z|{W85S?A3>1b|lcB{@iFKLVR9D^pUP+>$oE7lk_R-uI0yg=5q tHVYK``ub+==CcC+DP(#}0qB2B{2dXOe;N14SWt73#?_A`J25UjcS{5z#N~=UT zOKn5FG&Mx2sWVh>EpMhLU^-=WbS%&0*lqQAHE~==9*^JyF(fiJzK?@P;z4bd6bmy18bxIT zmTD{;v24b-l@tm&Y~3c|;E_1V`o64B?;{l~_DZXXM}Fquk$BO+O`tKvtVf*nDC-at z1zeO8*gWV0*g1qb!cGl*dLnDH-tLulJy6gijzl=CUEQ&OX{@z~xFBJyMnU3WDp6C7 zm`acYNE9T3YKTBGXjgYkBAlg1oK*z57WPxyP*1X6ie3s41LC4Mh&Uh>C^0Zqoc#PM zr}~GM)EE$lQgIHFjY4fheXfQ5s}%@)q^Tk57jFX=RSRnQ?943=9R7w&Ka9eXH3Q!c zGw|I_L_lQ8VuhfTLYf+)2z!=cAJlVuY0d~8sHnmU6^$0`ExY1itlsP`)nMoba49+*6(eVt$uh-R?XrF-9YR7r(IjC-Bxgh_o;J#-?EQ+cJHdhWB0UMxr7~}BfYjr>obOF^YkOw?OOAo_6 z$Ut3&NKb%?qc#tk<)^E2XfTi3d9bsf88nyy&4A5-&NF!R9@s^&S&S2c^h6nJRmeV< znNC^bR)H>pE>bGxIdwW+2Dh_lkOrGxde3sQKSi;)SSHXcV%EWQ%Hnf*HGwjyqhom| z{4_!Vu?y_oIY-#_IQ;$${pY7iP5#Y9>K>=RxXWAbO%e`y?A?_Eod?a~gm<8$V|kRp z``O;Fa Date: Fri, 20 Nov 2009 22:09:05 +0100 Subject: [PATCH 40/73] fix resizing of the MUC occupant treeview when we resize chat window. Fixes #5433 --- src/groupchat_control.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/groupchat_control.py b/src/groupchat_control.py index e71eefe32..e4db34610 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -317,6 +317,7 @@ class GroupchatControl(ChatControlBase): id_ = self.list_treeview.connect('style-set', self.on_list_treeview_style_set) self.handlers[id_] = self.list_treeview + self.resize_from_another_muc = False # we want to know when the the widget resizes, because that is # an indication that the hpaned has moved... # FIXME: Find a better indicator that the hpaned has moved. @@ -446,17 +447,29 @@ class GroupchatControl(ChatControlBase): menu.show_all() + def resize_occupant_treeview(self, position): + self.resize_from_another_muc = True + self.hpaned.set_position(position) + def reset_flag(): + self.resize_from_another_muc = False + # Reset the flag when everything will be redrawn, and in particular when + # on_treeview_size_allocate will have been called. + gobject.idle_add(reset_flag) + def on_treeview_size_allocate(self, widget, allocation): '''The MUC treeview has resized. Move the hpaned in all tabs to match''' + if self.resize_from_another_muc: + # Don't send the event to other MUC + return hpaned_position = self.hpaned.get_position() for account in gajim.gc_connected: for room_jid in [i for i in gajim.gc_connected[account] if \ - gajim.gc_connected[account][i]]: + gajim.gc_connected[account][i] and i != self.room_jid]: ctrl = gajim.interface.msg_win_mgr.get_gc_control(room_jid, account) if not ctrl: ctrl = gajim.interface.minimized_controls[account][room_jid] if ctrl: - ctrl.hpaned.set_position(hpaned_position) + ctrl.resize_occupant_treeview(hpaned_position) def iter_contact_rows(self): '''iterate over all contact rows in the tree model''' From 84ad8fb7234e8b530136efe41014b944eeab0300 Mon Sep 17 00:00:00 2001 From: red-agent Date: Sat, 21 Nov 2009 11:31:49 +0200 Subject: [PATCH 41/73] Added myself into the annals of history Sorry for not disclosing my real name to the wide audience. I'm just that paranoid... --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index a3cfefe88..c23914bef 100644 --- a/AUTHORS +++ b/AUTHORS @@ -3,6 +3,7 @@ CURRENT DEVELOPERS: Nikos Kouremenos (kourem AT gmail.com) Yann Leboulanger (asterix AT lagaule.org) Julien Pivotto (roidelapluie AT gmail.com) +red-agent (hell.director AT gmail.com) Jonathan Schleifer (js-gajim AT webkeks.org) Travis Shirk (travis AT pobox.com) Brendan Taylor (whateley AT gmail.com) From 102126b330366322c1769ade3d9c0eb8f4e0dcca Mon Sep 17 00:00:00 2001 From: red-agent Date: Sun, 22 Nov 2009 13:33:19 +0200 Subject: [PATCH 42/73] Fixed broken resource setting --- src/common/connection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/connection.py b/src/common/connection.py index 3c8cf537d..823f56824 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -159,6 +159,7 @@ class CommonConnection: resource = Template(resource).safe_substitute({ 'hostname': socket.gethostname() }) + return resource def dispatch(self, event, data): '''always passes account name as first param''' From 22ab1c955328fdbd30e6e3610ac74231306a4397 Mon Sep 17 00:00:00 2001 From: red-agent Date: Sun, 22 Nov 2009 20:05:30 +0200 Subject: [PATCH 43/73] Fixed refactoring artifact --- src/profile_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/profile_window.py b/src/profile_window.py index f3fe1faa4..132357b0b 100644 --- a/src/profile_window.py +++ b/src/profile_window.py @@ -322,7 +322,7 @@ class ProfileWindow: nick = '' if 'NICKNAME' in vcard_: nick = vcard_['NICKNAME'] - gajim.connections[self.account].send_nickname(self.account, nick) + gajim.connections[self.account].send_nickname(nick) if nick == '': nick = gajim.config.get_per('accounts', self.account, 'name') gajim.nicks[self.account] = nick From a53e906a921c6bac5758085925fdaeadcf2fae35 Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sun, 22 Nov 2009 22:07:48 +0100 Subject: [PATCH 44/73] Ignore error stanzas with event tag. This prevents dialogs poping up with showing "Service unavailable". --- src/common/pep.py | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/common/pep.py b/src/common/pep.py index 742325b4c..0195341a7 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -23,9 +23,6 @@ ## along with Gajim. If not, see . ## -import common.gajim -from common import xmpp - MOODS = { 'afraid': _('Afraid'), 'amazed': _('Amazed'), @@ -194,16 +191,18 @@ ACTIVITIES = { TUNE_DATA = ['artist', 'title', 'source', 'track', 'length'] +import gobject +import gtk + import logging log = logging.getLogger('gajim.c.pep') -import helpers -import atom -import gtkgui_helpers -import gobject -import gajim -import gtk +from common import helpers +from common import atom +from common import xmpp +from common import gajim +import gtkgui_helpers def translate_mood(mood): if mood in MOODS: @@ -230,7 +229,7 @@ class AbstractPEP(object): self._pep_specific_data, self._retracted = self._extract_info(items) self._update_contacts(jid, account) - if jid == common.gajim.get_jid_from_account(account): + if jid == gajim.get_jid_from_account(account): self._update_account(account) def _extract_info(self, items): @@ -238,7 +237,7 @@ class AbstractPEP(object): raise NotImplementedError def _update_contacts(self, jid, account): - for contact in common.gajim.contacts.get_contacts(account, jid): + for contact in gajim.contacts.get_contacts(account, jid): if self._retracted: if self.type in contact.pep: del contact.pep[self.type] @@ -246,7 +245,7 @@ class AbstractPEP(object): contact.pep[self.type] = self def _update_account(self, account): - acc = common.gajim.connections[account] + acc = gajim.connections[account] if self._retracted: if self.type in acc.pep: del acc.pep[self.type] @@ -266,7 +265,7 @@ class UserMoodPEP(AbstractPEP): '''XEP-0107: User Mood''' type = 'mood' - namespace = common.xmpp.NS_MOOD + namespace = xmpp.NS_MOOD def _extract_info(self, items): mood_dict = {} @@ -306,7 +305,7 @@ class UserTunePEP(AbstractPEP): '''XEP-0118: User Tune''' type = 'tune' - namespace = common.xmpp.NS_TUNE + namespace = xmpp.NS_TUNE def _extract_info(self, items): tune_dict = {} @@ -352,7 +351,7 @@ class UserActivityPEP(AbstractPEP): '''XEP-0108: User Activity''' type = 'activity' - namespace = common.xmpp.NS_ACTIVITY + namespace = xmpp.NS_ACTIVITY def _extract_info(self, items): activity_dict = {} @@ -418,7 +417,7 @@ class UserNicknamePEP(AbstractPEP): '''XEP-0172: User Nickname''' type = 'nickname' - namespace = common.xmpp.NS_NICK + namespace = xmpp.NS_NICK def _extract_info(self, items): nick = '' @@ -434,16 +433,16 @@ class UserNicknamePEP(AbstractPEP): def _update_contacts(self, jid, account): # TODO: use dict instead nick = '' if self._retracted else self._pep_specific_data - for contact in common.gajim.contacts.get_contacts(account, jid): + for contact in gajim.contacts.get_contacts(account, jid): contact.contact_name = nick def _update_account(self, account): # TODO: use dict instead if self._retracted: - common.gajim.nicks[account] = common.gajim.config.get_per('accounts', + gajim.nicks[account] = gajim.config.get_per('accounts', account, 'name') else: - common.gajim.nicks[account] = self._pep_specific_data + gajim.nicks[account] = self._pep_specific_data SUPPORTED_PERSONAL_USER_EVENTS = [UserMoodPEP, UserTunePEP, UserActivityPEP, @@ -454,8 +453,8 @@ class ConnectionPEP(object): def _pubsubEventCB(self, xmpp_dispatcher, msg): ''' Called when we receive with pubsub event. ''' if msg.getTag('error'): - log.warning('PubsubEventCB received error stanza') - return + log.debug('PubsubEventCB received error stanza. Ignoring') + raise NodeProcessed jid = helpers.get_full_jid_from_iq(msg) event_tag = msg.getTag('event') @@ -474,7 +473,7 @@ class ConnectionPEP(object): # but to be sure... self.dispatch('ATOM_ENTRY', (atom.OldEntry(node=entry),)) - raise common.xmpp.NodeProcessed + raise xmpp.NodeProcessed def send_activity(self, activity, subactivity=None, message=None): if not self.pep_supported: From 07c008cbaafd788d193f0de75e27793e8ec147aa Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sun, 22 Nov 2009 22:10:55 +0100 Subject: [PATCH 45/73] Remove TODO which was more or less a "might be useful one day" --- src/common/pep.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/common/pep.py b/src/common/pep.py index 0195341a7..a991c35c7 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -431,16 +431,13 @@ class UserNicknamePEP(AbstractPEP): return (nick, retracted) def _update_contacts(self, jid, account): - # TODO: use dict instead nick = '' if self._retracted else self._pep_specific_data for contact in gajim.contacts.get_contacts(account, jid): contact.contact_name = nick def _update_account(self, account): - # TODO: use dict instead if self._retracted: - gajim.nicks[account] = gajim.config.get_per('accounts', - account, 'name') + gajim.nicks[account] = gajim.config.get_per('accounts', account, 'name') else: gajim.nicks[account] = self._pep_specific_data From 96b9326b0f17235975b79bc772e267ce0d94909b Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sun, 22 Nov 2009 22:14:05 +0100 Subject: [PATCH 46/73] Move function closer to where it is used. This makes it easier to reason about its usage. --- src/common/pep.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/common/pep.py b/src/common/pep.py index a991c35c7..3437d4a46 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -204,12 +204,6 @@ from common import gajim import gtkgui_helpers -def translate_mood(mood): - if mood in MOODS: - return MOODS[mood] - else: - return mood - class AbstractPEP(object): @@ -293,13 +287,19 @@ class UserMoodPEP(AbstractPEP): def asMarkupText(self): assert not self._retracted untranslated_mood = self._pep_specific_data['mood'] - mood = translate_mood(untranslated_mood) + mood = _translate_mood(untranslated_mood) markuptext = '%s' % gobject.markup_escape_text(mood) if 'text' in self._pep_specific_data: text = self._pep_specific_data['text'] markuptext += ' (%s)' % gobject.markup_escape_text(text) return markuptext + def _translate_mood(mood): + if mood in MOODS: + return MOODS[mood] + else: + return mood + class UserTunePEP(AbstractPEP): '''XEP-0118: User Tune''' From fb456b1ee4503bae02712e1b6b41aec4b028f382 Mon Sep 17 00:00:00 2001 From: Stephan Erb Date: Sun, 22 Nov 2009 22:57:52 +0100 Subject: [PATCH 47/73] Make dependencies of ConnectionPEP explicit. This means ConnectionPEP now knows the objects on which it calls method. Before, it just assumed that: "it will be subclassed and that the subclass defines a few methods". Big advantage is that false positives in the pylint report are gone --- src/common/connection_handlers.py | 2 ++ src/common/pep.py | 45 +++++++++++++++++-------------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 15a979415..e8abbae74 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1458,6 +1458,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, ConnectionBytestream.__init__(self) ConnectionCommands.__init__(self) ConnectionPubSub.__init__(self) + ConnectionPEP.__init__(self, account=self.name, dispatcher=self, + pubsub_connection=self) ConnectionJingle.__init__(self) ConnectionHandlersBase.__init__(self) self.gmail_url = None diff --git a/src/common/pep.py b/src/common/pep.py index 3437d4a46..34f3bda46 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -247,12 +247,12 @@ class AbstractPEP(object): acc.pep[self.type] = self def asPixbufIcon(self): - '''To be implemented by subclasses''' - raise NotImplementedError + '''SHOULD be implemented by subclasses''' + return None def asMarkupText(self): - '''To be implemented by subclasses''' - raise NotImplementedError + '''SHOULD be implemented by subclasses''' + return '' class UserMoodPEP(AbstractPEP): @@ -287,14 +287,14 @@ class UserMoodPEP(AbstractPEP): def asMarkupText(self): assert not self._retracted untranslated_mood = self._pep_specific_data['mood'] - mood = _translate_mood(untranslated_mood) + mood = self._translate_mood(untranslated_mood) markuptext = '%s' % gobject.markup_escape_text(mood) if 'text' in self._pep_specific_data: text = self._pep_specific_data['text'] markuptext += ' (%s)' % gobject.markup_escape_text(text) return markuptext - def _translate_mood(mood): + def _translate_mood(self, mood): if mood in MOODS: return MOODS[mood] else: @@ -446,20 +446,25 @@ SUPPORTED_PERSONAL_USER_EVENTS = [UserMoodPEP, UserTunePEP, UserActivityPEP, UserNicknamePEP] class ConnectionPEP(object): - + + def __init__(self, account, dispatcher, pubsub_connection): + self._account = account + self._dispatcher = dispatcher + self._pubsub_connection = pubsub_connection + def _pubsubEventCB(self, xmpp_dispatcher, msg): ''' Called when we receive with pubsub event. ''' if msg.getTag('error'): log.debug('PubsubEventCB received error stanza. Ignoring') - raise NodeProcessed + raise xmpp.NodeProcessed jid = helpers.get_full_jid_from_iq(msg) event_tag = msg.getTag('event') for pep_class in SUPPORTED_PERSONAL_USER_EVENTS: - pep = pep_class.get_tag_as_PEP(jid, self.name, event_tag) + pep = pep_class.get_tag_as_PEP(jid, self._account, event_tag) if pep: - self.dispatch('PEP_RECEIVED', (jid, pep.type)) + self._dispatcher.dispatch('PEP_RECEIVED', (jid, pep.type)) items = event_tag.getTag('items') if items: @@ -468,7 +473,8 @@ class ConnectionPEP(object): if 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),)) + self._dispatcher.dispatch('ATOM_ENTRY', + (atom.OldEntry(node=entry),)) raise xmpp.NodeProcessed @@ -483,14 +489,14 @@ class ConnectionPEP(object): if message: i = item.addChild('text') i.addData(message) - self.send_pb_publish('', xmpp.NS_ACTIVITY, item, '0') + self._pubsub_connection.send_pb_publish('', xmpp.NS_ACTIVITY, item, '0') def retract_activity(self): if not self.pep_supported: return # not all server support retract, so send empty pep first self.send_activity(None) - self.send_pb_retract('', xmpp.NS_ACTIVITY, '0') + self._pubsub_connection.send_pb_retract('', xmpp.NS_ACTIVITY, '0') def send_mood(self, mood, message=None): if not self.pep_supported: @@ -501,13 +507,13 @@ class ConnectionPEP(object): if message: i = item.addChild('text') i.addData(message) - self.send_pb_publish('', xmpp.NS_MOOD, item, '0') + self._pubsub_connection.send_pb_publish('', xmpp.NS_MOOD, item, '0') def retract_mood(self): if not self.pep_supported: return self.send_mood(None) - self.send_pb_retract('', xmpp.NS_MOOD, '0') + self._pubsub_connection.send_pb_retract('', xmpp.NS_MOOD, '0') def send_tune(self, artist='', title='', source='', track=0, length=0, items=None): @@ -531,28 +537,27 @@ class ConnectionPEP(object): i.addData(length) if items: item.addChild(payload=items) - self.send_pb_publish('', xmpp.NS_TUNE, item, '0') + self._pubsub_connection.send_pb_publish('', xmpp.NS_TUNE, item, '0') def retract_tune(self): if not self.pep_supported: return # not all server support retract, so send empty pep first self.send_tune(None) - self.send_pb_retract('', xmpp.NS_TUNE, '0') + self._pubsub_connection.send_pb_retract('', xmpp.NS_TUNE, '0') def send_nickname(self, nick): if not self.pep_supported: return item = xmpp.Node('nick', {'xmlns': xmpp.NS_NICK}) item.addData(nick) - self.send_pb_publish('', xmpp.NS_NICK, item, '0') + self._pubsub_connection.send_pb_publish('', xmpp.NS_NICK, item, '0') def retract_nickname(self): if not self.pep_supported: return # not all server support retract, so send empty pep first self.send_nickname(None) - self.send_pb_retract('', xmpp.NS_NICK, '0') - + self._pubsub_connection.send_pb_retract('', xmpp.NS_NICK, '0') # vim: se ts=3: From 8be7f9d2e102cc54c0d950591f3ee8e4cd9356cb Mon Sep 17 00:00:00 2001 From: Alexander Cherniuk Date: Mon, 23 Nov 2009 16:08:09 +0200 Subject: [PATCH 48/73] Setting my identity straight --- AUTHORS | 2 +- src/command_system/__init__.py | 2 +- src/command_system/dispatching.py | 2 +- src/command_system/errors.py | 2 +- src/command_system/framework.py | 2 +- src/command_system/implementation/__init__.py | 2 +- src/command_system/implementation/custom.py | 2 +- src/command_system/implementation/hosts.py | 2 +- src/command_system/implementation/middleware.py | 2 +- src/command_system/implementation/standard.py | 2 +- src/command_system/mapping.py | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/AUTHORS b/AUTHORS index c23914bef..3d1502324 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,9 +1,9 @@ CURRENT DEVELOPERS: +Alexander Cherniuk (ts33kr AT gmail.com) Nikos Kouremenos (kourem AT gmail.com) Yann Leboulanger (asterix AT lagaule.org) Julien Pivotto (roidelapluie AT gmail.com) -red-agent (hell.director AT gmail.com) Jonathan Schleifer (js-gajim AT webkeks.org) Travis Shirk (travis AT pobox.com) Brendan Taylor (whateley AT gmail.com) diff --git a/src/command_system/__init__.py b/src/command_system/__init__.py index c0a48f863..ff61c186e 100644 --- a/src/command_system/__init__.py +++ b/src/command_system/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/command_system/dispatching.py b/src/command_system/dispatching.py index 2bfd76b96..7f365a915 100644 --- a/src/command_system/dispatching.py +++ b/src/command_system/dispatching.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/command_system/errors.py b/src/command_system/errors.py index 992e83ccf..877a22acb 100644 --- a/src/command_system/errors.py +++ b/src/command_system/errors.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/command_system/framework.py b/src/command_system/framework.py index db9eb2e78..30f5cd53c 100644 --- a/src/command_system/framework.py +++ b/src/command_system/framework.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/command_system/implementation/__init__.py b/src/command_system/implementation/__init__.py index 66d097f42..c77c23e3f 100644 --- a/src/command_system/implementation/__init__.py +++ b/src/command_system/implementation/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/command_system/implementation/custom.py b/src/command_system/implementation/custom.py index 4f54670da..e4aa32dbf 100644 --- a/src/command_system/implementation/custom.py +++ b/src/command_system/implementation/custom.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/command_system/implementation/hosts.py b/src/command_system/implementation/hosts.py index b38bb1a35..a90dab464 100644 --- a/src/command_system/implementation/hosts.py +++ b/src/command_system/implementation/hosts.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/command_system/implementation/middleware.py b/src/command_system/implementation/middleware.py index 9ef4bea29..2f262f8ed 100644 --- a/src/command_system/implementation/middleware.py +++ b/src/command_system/implementation/middleware.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/command_system/implementation/standard.py b/src/command_system/implementation/standard.py index 3b46a30db..d0e585fec 100644 --- a/src/command_system/implementation/standard.py +++ b/src/command_system/implementation/standard.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/src/command_system/mapping.py b/src/command_system/mapping.py index 707866a20..ecf8f0783 100644 --- a/src/command_system/mapping.py +++ b/src/command_system/mapping.py @@ -1,4 +1,4 @@ -# Copyright (C) 2009 red-agent +# Copyright (C) 2009 Alexander Cherniuk # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by From 3a190b832849cf73d28ed5eb7300f05f242da2bc Mon Sep 17 00:00:00 2001 From: Alexander Cherniuk Date: Tue, 24 Nov 2009 12:20:40 +0200 Subject: [PATCH 49/73] Fixed a typo --- src/common/connection_handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 15a979415..63e25bc00 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1860,7 +1860,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, log.warn('Invalid JID: %s, ignoring it' % item.getAttr('jid')) continue name = item.getAttr('name') - contact = gajim.contact.get_contact(self.name, jid) + contact = gajim.contacts.get_contact(self.name, jid) groups = [] same_groups = True for group in item.getTags('group'): From 94f6d6b79ac357c98ec6d3089f891ec5d78968e4 Mon Sep 17 00:00:00 2001 From: Alexander Cherniuk Date: Tue, 24 Nov 2009 14:24:35 +0200 Subject: [PATCH 50/73] Fixed timezone parsing --- src/common/connection_handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 63e25bc00..35679d771 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1735,7 +1735,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, # wrong answer return tzo = qp.getTag('tzo').getData() - if tzo == 'Z': + if tzo.lower() == 'z': tzo = '0:0' tzoh, tzom = tzo.split(':') utc_time = qp.getTag('utc').getData() From eb11c24eea6b367d1774cfa8ae3b38c03e72085a Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Tue, 24 Nov 2009 14:33:49 +0100 Subject: [PATCH 51/73] egg.trayicon is dead. Light a candle :'( Fixes #3021, #5246 --- Makefile.am | 1 - README.html | 5 +- configure.ac | 16 +- scripts/gajim.in | 1 - src/Makefile.am | 31 +-- src/eggtrayicon.c | 584 ----------------------------------------- src/eggtrayicon.h | 80 ------ src/features_window.py | 13 - src/gui_interface.py | 18 +- src/statusicon.py | 354 ++++++++++++++++++++++++- src/systray.py | 459 -------------------------------- src/tooltips.py | 4 +- src/trayicon.defs | 59 ----- src/trayicon.override | 47 ---- src/trayiconmodule.c | 43 --- 15 files changed, 353 insertions(+), 1362 deletions(-) delete mode 100644 src/eggtrayicon.c delete mode 100644 src/eggtrayicon.h delete mode 100644 src/systray.py delete mode 100644 src/trayicon.defs delete mode 100644 src/trayicon.override delete mode 100644 src/trayiconmodule.c diff --git a/Makefile.am b/Makefile.am index 019f7a1d9..f708a0855 100644 --- a/Makefile.am +++ b/Makefile.am @@ -50,7 +50,6 @@ MAINTAINERCLEANFILES = \ aclocal.m4 \ libtool \ po/POTFILES.in \ - src/trayicon_la-eggtrayicon.loT \ m4/intltool.m4 MAINTAINERCLEANDIRS = \ diff --git a/README.html b/README.html index 558d23a80..8ddade813 100644 --- a/README.html +++ b/README.html @@ -16,7 +16,7 @@ Welcome to Gajim and thank you for trying out our client.

Runtime Requirements

  • python2.5 or higher
  • -
  • pygtk2.12 or higher
  • +
  • pygtk2.16 or higher
  • python-libglade
  • pysqlite2 (if you have python 2.5, you already have this)
@@ -34,7 +34,6 @@ Gajim is a GTK+ app that loves GNOME. You can do 'make' so you don't require gno
  • For zeroconf (bonjour), the "enable link-local messaging" checkbox, you need dbus-glib, python-avahi
  • dnsutils (or whatever package provides the nslookup binary) for SRV support
  • gtkspell and aspell-LANG where lang is your locale eg. en, fr etc
  • -
  • GnomePythonExtras 2.10 or above (aka gnome-python-desktop) so you can avoid compiling trayicon and gtkspell
  • gnome-python-desktop (for GnomeKeyring support)
  • notification-daemon or notify-python (and D-Bus) to get cooler popups
  • D-Bus running to have gajim-remote working. Some distributions split dbus-x11, which is needed for dbus to work with Gajim. Version >= 0.80 is required.
  • @@ -53,8 +52,6 @@ the xml lib that *comes* with python and not pyxml or whatever.
    • python-dev
    • python-gtk2-dev
    • -
    • libgtk2.0-dev aka. gtk2-devel
    • -
    • libgtkspell-dev (for the gtkspell module)
    • intltool (>= 0.40.1)
    diff --git a/configure.ac b/configure.ac index 6363cb90b..af9840989 100644 --- a/configure.ac +++ b/configure.ac @@ -39,7 +39,7 @@ AM_NLS dnl **** dnl pygtk and gtk+ dnl **** -PKG_CHECK_MODULES([PYGTK], [gtk+-2.0 >= 2.12.0 pygtk-2.0 >= 2.12.0]) +PKG_CHECK_MODULES([PYGTK], [gtk+-2.0 >= 2.16.0 pygtk-2.0 >= 2.16.0]) AC_SUBST(PYGTK_CFLAGS) AC_SUBST(PYGTK_LIBS) PYGTK_DEFS=`$PKG_CONFIG --variable=defsdir pygtk-2.0` @@ -50,15 +50,6 @@ if test "x$PYTHON" = "x:"; then AC_MSG_ERROR([Python not found]) fi -dnl **** -dnl tray icon -dnl **** -AC_ARG_ENABLE(trayicon, - [ --disable-trayicon do not build trayicon module [default yes]], - enable_trayicon=$enableval, enable_trayicon=yes) -test "x$enable_trayicon" = "xyes" && have_trayicon=true || have_trayicon=false -AM_CONDITIONAL(BUILD_TRAYICON, $have_trayicon) - ACLOCAL_AMFLAGS="\${ACLOCAL_FLAGS}" AC_SUBST(ACLOCAL_AMFLAGS) @@ -91,8 +82,3 @@ AC_CONFIG_FILES([ po/Makefile.in ]) AC_OUTPUT -echo " -***************************** - Build features: - trayicon ......... ${have_trayicon} -*****************************" diff --git a/scripts/gajim.in b/scripts/gajim.in index 711ddac1d..178d69b16 100644 --- a/scripts/gajim.in +++ b/scripts/gajim.in @@ -33,5 +33,4 @@ export datadir=@DATADIR@/gajim PYTHON_EXEC=@PYTHON@ cd ${datadir}/src -export PYTHONPATH="$PYTHONPATH:@LIBDIR@/gajim" exec ${PYTHON_EXEC} -OO $APP.py "$@" diff --git a/src/Makefile.am b/src/Makefile.am index 24e7e2234..9c5e23b85 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,31 +1,7 @@ -CLEANFILES = \ - trayicon.c INCLUDES = \ $(PYTHON_INCLUDES) export MACOSX_DEPLOYMENT_TARGET=10.4 -if BUILD_TRAYICON -trayiconlib_LTLIBRARIES = trayicon.la -trayiconlibdir = $(pkglibdir) -trayicon_la_LIBADD = $(PYGTK_LIBS) -trayicon_la_SOURCES = \ - eggtrayicon.c \ - trayiconmodule.c - -nodist_trayicon_la_SOURCES = \ - trayicon.c - -trayicon_la_LDFLAGS = \ - -module -avoid-version -trayicon_la_CFLAGS = $(PYGTK_CFLAGS) - -trayicon.c: - pygtk-codegen-2.0 --prefix trayicon \ - --register $(PYGTK_DEFS)/gdk-types.defs \ - --register $(PYGTK_DEFS)/gtk-types.defs \ - --override $(srcdir)/trayicon.override \ - $(srcdir)/trayicon.defs > $@ -endif gajimsrcdir = $(pkgdatadir)/src gajimsrc_PYTHON = $(srcdir)/*.py @@ -56,12 +32,7 @@ EXTRA_DIST = $(gajimsrc_PYTHON) \ $(gajimsrc2_PYTHON) \ $(gajimsrc3_PYTHON) \ $(gajimsrc4_PYTHON) \ - $(gajimsrc5_PYTHON) \ - eggtrayicon.c \ - trayiconmodule.c \ - eggtrayicon.h \ - trayicon.defs \ - trayicon.override + $(gajimsrc5_PYTHON) dist-hook: rm -f $(distdir)/ipython_view.py diff --git a/src/eggtrayicon.c b/src/eggtrayicon.c deleted file mode 100644 index 56b3a0fb9..000000000 --- a/src/eggtrayicon.c +++ /dev/null @@ -1,584 +0,0 @@ -/* eggtrayicon.c - * Copyright (C) 2002 Anders Carlsson - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - */ - -#include -#include -#include - -#include "eggtrayicon.h" - -#include -#if defined (GDK_WINDOWING_X11) -#include -#include -#elif defined (GDK_WINDOWING_WIN32) -#include -#endif - -#ifndef EGG_COMPILATION -#ifndef _ -#define _(x) dgettext (GETTEXT_PACKAGE, x) -#define N_(x) x -#endif -#else -#define _(x) x -#define N_(x) x -#endif - -#define SYSTEM_TRAY_REQUEST_DOCK 0 -#define SYSTEM_TRAY_BEGIN_MESSAGE 1 -#define SYSTEM_TRAY_CANCEL_MESSAGE 2 - -#define SYSTEM_TRAY_ORIENTATION_HORZ 0 -#define SYSTEM_TRAY_ORIENTATION_VERT 1 - -enum { - PROP_0, - PROP_ORIENTATION -}; - -static GtkPlugClass *parent_class = NULL; - -static void egg_tray_icon_init (EggTrayIcon *icon); -static void egg_tray_icon_class_init (EggTrayIconClass *klass); - -static void egg_tray_icon_get_property (GObject *object, - guint prop_id, - GValue *value, - GParamSpec *pspec); - -static void egg_tray_icon_add (GtkContainer *container, GtkWidget *widget); - -static void egg_tray_icon_realize (GtkWidget *widget); -static void egg_tray_icon_unrealize (GtkWidget *widget); - -#ifdef GDK_WINDOWING_X11 -static void egg_tray_icon_update_manager_window (EggTrayIcon *icon, - gboolean dock_if_realized); -static void egg_tray_icon_manager_window_destroyed (EggTrayIcon *icon); -#endif - -GType -egg_tray_icon_get_type (void) -{ - static GType our_type = 0; - - if (our_type == 0) - { - static const GTypeInfo our_info = - { - sizeof (EggTrayIconClass), - (GBaseInitFunc) NULL, - (GBaseFinalizeFunc) NULL, - (GClassInitFunc) egg_tray_icon_class_init, - NULL, /* class_finalize */ - NULL, /* class_data */ - sizeof (EggTrayIcon), - 0, /* n_preallocs */ - (GInstanceInitFunc) egg_tray_icon_init - }; - - our_type = g_type_register_static (GTK_TYPE_PLUG, "EggTrayIcon", &our_info, 0); - } - - return our_type; -} - -static void -egg_tray_icon_init (EggTrayIcon *icon) -{ - icon->stamp = 1; - icon->orientation = GTK_ORIENTATION_HORIZONTAL; - - gtk_widget_add_events (GTK_WIDGET (icon), GDK_PROPERTY_CHANGE_MASK); -} - -static void -egg_tray_icon_class_init (EggTrayIconClass *klass) -{ - GObjectClass *gobject_class = (GObjectClass *)klass; - GtkWidgetClass *widget_class = (GtkWidgetClass *)klass; - GtkContainerClass *container_class = (GtkContainerClass *)klass; - - parent_class = g_type_class_peek_parent (klass); - - gobject_class->get_property = egg_tray_icon_get_property; - - widget_class->realize = egg_tray_icon_realize; - widget_class->unrealize = egg_tray_icon_unrealize; - - container_class->add = egg_tray_icon_add; - - g_object_class_install_property (gobject_class, - PROP_ORIENTATION, - g_param_spec_enum ("orientation", - _("Orientation"), - _("The orientation of the tray."), - GTK_TYPE_ORIENTATION, - GTK_ORIENTATION_HORIZONTAL, - G_PARAM_READABLE)); - -#if defined (GDK_WINDOWING_X11) - /* Nothing */ -#elif defined (GDK_WINDOWING_WIN32) - g_warning ("Port eggtrayicon to Win32"); -#else - g_warning ("Port eggtrayicon to this GTK+ backend"); -#endif -} - -static void -egg_tray_icon_get_property (GObject *object, - guint prop_id, - GValue *value, - GParamSpec *pspec) -{ - EggTrayIcon *icon = EGG_TRAY_ICON (object); - - switch (prop_id) - { - case PROP_ORIENTATION: - g_value_set_enum (value, icon->orientation); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); - break; - } -} - -#ifdef GDK_WINDOWING_X11 - -static Display * -egg_tray_icon_get_x_display(EggTrayIcon *icon) -{ - Display *xdisplay = NULL; - - GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (icon)); - if (!GDK_IS_DISPLAY (display)) - display = gdk_display_get_default (); - - xdisplay = GDK_DISPLAY_XDISPLAY (display); - - return xdisplay; -} - -static void -egg_tray_icon_get_orientation_property (EggTrayIcon *icon) -{ - Display *xdisplay; - Atom type; - int format; - union { - gulong *prop; - guchar *prop_ch; - } prop = { NULL }; - gulong nitems; - gulong bytes_after; - int error, result; - - g_assert (icon->manager_window != None); - - xdisplay = egg_tray_icon_get_x_display(icon); - if (xdisplay == NULL) - return; - - gdk_error_trap_push (); - type = None; - result = XGetWindowProperty (xdisplay, - icon->manager_window, - icon->orientation_atom, - 0, G_MAXLONG, FALSE, - XA_CARDINAL, - &type, &format, &nitems, - &bytes_after, &(prop.prop_ch)); - error = gdk_error_trap_pop (); - - if (error || result != Success) - return; - - if (type == XA_CARDINAL) - { - GtkOrientation orientation; - - orientation = (prop.prop [0] == SYSTEM_TRAY_ORIENTATION_HORZ) ? - GTK_ORIENTATION_HORIZONTAL : - GTK_ORIENTATION_VERTICAL; - - if (icon->orientation != orientation) - { - icon->orientation = orientation; - - g_object_notify (G_OBJECT (icon), "orientation"); - } - } - - if (prop.prop) - XFree (prop.prop); -} - -static GdkFilterReturn -egg_tray_icon_manager_filter (GdkXEvent *xevent, GdkEvent *event, gpointer user_data) -{ - EggTrayIcon *icon = user_data; - XEvent *xev = (XEvent *)xevent; - - if (xev->xany.type == ClientMessage && - xev->xclient.message_type == icon->manager_atom && - xev->xclient.data.l[1] == icon->selection_atom) - { - egg_tray_icon_update_manager_window (icon, TRUE); - } - else if (xev->xany.window == icon->manager_window) - { - if (xev->xany.type == PropertyNotify && - xev->xproperty.atom == icon->orientation_atom) - { - egg_tray_icon_get_orientation_property (icon); - } - if (xev->xany.type == DestroyNotify) - { - egg_tray_icon_manager_window_destroyed (icon); - } - } - return GDK_FILTER_CONTINUE; -} - -#endif - -static void -egg_tray_icon_unrealize (GtkWidget *widget) -{ -#ifdef GDK_WINDOWING_X11 - EggTrayIcon *icon = EGG_TRAY_ICON (widget); - GdkWindow *root_window; - - if (icon->manager_window != None) - { - GdkWindow *gdkwin; - - gdkwin = gdk_window_lookup_for_display (gtk_widget_get_display (widget), - icon->manager_window); - - gdk_window_remove_filter (gdkwin, egg_tray_icon_manager_filter, icon); - } - - root_window = gdk_screen_get_root_window (gtk_widget_get_screen (widget)); - - gdk_window_remove_filter (root_window, egg_tray_icon_manager_filter, icon); - - if (GTK_WIDGET_CLASS (parent_class)->unrealize) - (* GTK_WIDGET_CLASS (parent_class)->unrealize) (widget); -#endif -} - -#ifdef GDK_WINDOWING_X11 - -static void -egg_tray_icon_send_manager_message (EggTrayIcon *icon, - long message, - Window window, - long data1, - long data2, - long data3) -{ - XClientMessageEvent ev; - Display *display; - - ev.type = ClientMessage; - ev.window = window; - ev.message_type = icon->system_tray_opcode_atom; - ev.format = 32; - ev.data.l[0] = gdk_x11_get_server_time (GTK_WIDGET (icon)->window); - ev.data.l[1] = message; - ev.data.l[2] = data1; - ev.data.l[3] = data2; - ev.data.l[4] = data3; - - display = egg_tray_icon_get_x_display(icon); - - if (display == NULL) - return; - - gdk_error_trap_push (); - XSendEvent (display, - icon->manager_window, False, NoEventMask, (XEvent *)&ev); - XSync (display, False); - gdk_error_trap_pop (); -} - -static void -egg_tray_icon_send_dock_request (EggTrayIcon *icon) -{ - egg_tray_icon_send_manager_message (icon, - SYSTEM_TRAY_REQUEST_DOCK, - icon->manager_window, - gtk_plug_get_id (GTK_PLUG (icon)), - 0, 0); -} - -static void -egg_tray_icon_update_manager_window (EggTrayIcon *icon, - gboolean dock_if_realized) -{ - Display *xdisplay; - - if (icon->manager_window != None) - return; - - xdisplay = egg_tray_icon_get_x_display(icon); - - if (xdisplay == NULL) - return; - - XGrabServer (xdisplay); - - icon->manager_window = XGetSelectionOwner (xdisplay, - icon->selection_atom); - - if (icon->manager_window != None) - XSelectInput (xdisplay, - icon->manager_window, StructureNotifyMask|PropertyChangeMask); - - XUngrabServer (xdisplay); - XFlush (xdisplay); - - if (icon->manager_window != None) - { - GdkWindow *gdkwin; - - gdkwin = gdk_window_lookup_for_display (gtk_widget_get_display (GTK_WIDGET (icon)), - icon->manager_window); - - gdk_window_add_filter (gdkwin, egg_tray_icon_manager_filter, icon); - - if (dock_if_realized && GTK_WIDGET_REALIZED (icon)) - egg_tray_icon_send_dock_request (icon); - - egg_tray_icon_get_orientation_property (icon); - } -} - -static gboolean -transparent_expose_event (GtkWidget *widget, GdkEventExpose *event, gpointer user_data) -{ - gdk_window_clear_area (widget->window, event->area.x, event->area.y, - event->area.width, event->area.height); - return FALSE; -} - -static void -make_transparent_again (GtkWidget *widget, GtkStyle *previous_style, - gpointer user_data) -{ - gdk_window_set_back_pixmap (widget->window, NULL, TRUE); -} - -static void -make_transparent (GtkWidget *widget, gpointer user_data) -{ - if (GTK_WIDGET_NO_WINDOW (widget) || GTK_WIDGET_APP_PAINTABLE (widget)) - return; - - gtk_widget_set_app_paintable (widget, TRUE); - gtk_widget_set_double_buffered (widget, FALSE); - gdk_window_set_back_pixmap (widget->window, NULL, TRUE); - g_signal_connect (widget, "expose_event", - G_CALLBACK (transparent_expose_event), NULL); - g_signal_connect_after (widget, "style_set", - G_CALLBACK (make_transparent_again), NULL); -} - -static void -egg_tray_icon_manager_window_destroyed (EggTrayIcon *icon) -{ - GdkWindow *gdkwin; - - g_return_if_fail (icon->manager_window != None); - - gdkwin = gdk_window_lookup_for_display (gtk_widget_get_display (GTK_WIDGET (icon)), - icon->manager_window); - - gdk_window_remove_filter (gdkwin, egg_tray_icon_manager_filter, icon); - - icon->manager_window = None; - - egg_tray_icon_update_manager_window (icon, TRUE); -} - -#endif - -static void -egg_tray_icon_realize (GtkWidget *widget) -{ -#ifdef GDK_WINDOWING_X11 - EggTrayIcon *icon = EGG_TRAY_ICON (widget); - GdkScreen *screen; - Display *xdisplay; - char buffer[256]; - GdkWindow *root_window; - - if (GTK_WIDGET_CLASS (parent_class)->realize) - GTK_WIDGET_CLASS (parent_class)->realize (widget); - - make_transparent (widget, NULL); - - xdisplay = egg_tray_icon_get_x_display(icon); - - if (xdisplay == NULL) - return; - - screen = gtk_widget_get_screen (widget); - - /* Now see if there's a manager window around */ - g_snprintf (buffer, sizeof (buffer), - "_NET_SYSTEM_TRAY_S%d", - gdk_screen_get_number (screen)); - - icon->selection_atom = XInternAtom (xdisplay, buffer, False); - - icon->manager_atom = XInternAtom (xdisplay, "MANAGER", False); - - icon->system_tray_opcode_atom = XInternAtom (xdisplay, - "_NET_SYSTEM_TRAY_OPCODE", - False); - - icon->orientation_atom = XInternAtom (xdisplay, - "_NET_SYSTEM_TRAY_ORIENTATION", - False); - - egg_tray_icon_update_manager_window (icon, FALSE); - egg_tray_icon_send_dock_request (icon); - - root_window = gdk_screen_get_root_window (screen); - - /* Add a root window filter so that we get changes on MANAGER */ - gdk_window_add_filter (root_window, - egg_tray_icon_manager_filter, icon); -#endif -} - -static void -egg_tray_icon_add (GtkContainer *container, GtkWidget *widget) -{ - g_signal_connect (widget, "realize", - G_CALLBACK (make_transparent), NULL); - GTK_CONTAINER_CLASS (parent_class)->add (container, widget); -} - -EggTrayIcon * -egg_tray_icon_new_for_screen (GdkScreen *screen, const char *name) -{ - g_return_val_if_fail (GDK_IS_SCREEN (screen), NULL); - - return g_object_new (EGG_TYPE_TRAY_ICON, "screen", screen, "title", name, NULL); -} - -EggTrayIcon* -egg_tray_icon_new (const gchar *name) -{ - return g_object_new (EGG_TYPE_TRAY_ICON, "title", name, NULL); -} - -guint -egg_tray_icon_send_message (EggTrayIcon *icon, - gint timeout, - const gchar *message, - gint len) -{ - guint stamp; - - g_return_val_if_fail (EGG_IS_TRAY_ICON (icon), 0); - g_return_val_if_fail (timeout >= 0, 0); - g_return_val_if_fail (message != NULL, 0); - -#ifdef GDK_WINDOWING_X11 - if (icon->manager_window == None) - return 0; -#endif - - if (len < 0) - len = strlen (message); - - stamp = icon->stamp++; - -#ifdef GDK_WINDOWING_X11 - /* Get ready to send the message */ - egg_tray_icon_send_manager_message (icon, SYSTEM_TRAY_BEGIN_MESSAGE, - (Window)gtk_plug_get_id (GTK_PLUG (icon)), - timeout, len, stamp); - - /* Now to send the actual message */ - gdk_error_trap_push (); - while (len > 0) - { - XClientMessageEvent ev; - Display *xdisplay; - - xdisplay = egg_tray_icon_get_x_display(icon); - - if (xdisplay == NULL) - return 0; - - ev.type = ClientMessage; - ev.window = (Window)gtk_plug_get_id (GTK_PLUG (icon)); - ev.format = 8; - ev.message_type = XInternAtom (xdisplay, - "_NET_SYSTEM_TRAY_MESSAGE_DATA", False); - if (len > 20) - { - memcpy (&ev.data, message, 20); - len -= 20; - message += 20; - } - else - { - memcpy (&ev.data, message, len); - len = 0; - } - - XSendEvent (xdisplay, - icon->manager_window, False, StructureNotifyMask, (XEvent *)&ev); - XSync (xdisplay, False); - } - gdk_error_trap_pop (); -#endif - - return stamp; -} - -void -egg_tray_icon_cancel_message (EggTrayIcon *icon, - guint id) -{ - g_return_if_fail (EGG_IS_TRAY_ICON (icon)); - g_return_if_fail (id > 0); -#ifdef GDK_WINDOWING_X11 - egg_tray_icon_send_manager_message (icon, SYSTEM_TRAY_CANCEL_MESSAGE, - (Window)gtk_plug_get_id (GTK_PLUG (icon)), - id, 0, 0); -#endif -} - -GtkOrientation -egg_tray_icon_get_orientation (EggTrayIcon *icon) -{ - g_return_val_if_fail (EGG_IS_TRAY_ICON (icon), GTK_ORIENTATION_HORIZONTAL); - - return icon->orientation; -} diff --git a/src/eggtrayicon.h b/src/eggtrayicon.h deleted file mode 100644 index 557fdb20c..000000000 --- a/src/eggtrayicon.h +++ /dev/null @@ -1,80 +0,0 @@ -/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ -/* eggtrayicon.h - * Copyright (C) 2002 Anders Carlsson - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the - * Free Software Foundation, Inc., 59 Temple Place - Suite 330, - * Boston, MA 02111-1307, USA. - */ - -#ifndef __EGG_TRAY_ICON_H__ -#define __EGG_TRAY_ICON_H__ - -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif - -G_BEGIN_DECLS - -#define EGG_TYPE_TRAY_ICON (egg_tray_icon_get_type ()) -#define EGG_TRAY_ICON(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), EGG_TYPE_TRAY_ICON, EggTrayIcon)) -#define EGG_TRAY_ICON_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), EGG_TYPE_TRAY_ICON, EggTrayIconClass)) -#define EGG_IS_TRAY_ICON(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), EGG_TYPE_TRAY_ICON)) -#define EGG_IS_TRAY_ICON_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), EGG_TYPE_TRAY_ICON)) -#define EGG_TRAY_ICON_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), EGG_TYPE_TRAY_ICON, EggTrayIconClass)) - -typedef struct _EggTrayIcon EggTrayIcon; -typedef struct _EggTrayIconClass EggTrayIconClass; - -struct _EggTrayIcon -{ - GtkPlug parent_instance; - - guint stamp; - -#ifdef GDK_WINDOWING_X11 - Atom selection_atom; - Atom manager_atom; - Atom system_tray_opcode_atom; - Atom orientation_atom; - Window manager_window; -#endif - GtkOrientation orientation; -}; - -struct _EggTrayIconClass -{ - GtkPlugClass parent_class; -}; - -GType egg_tray_icon_get_type (void); - -EggTrayIcon *egg_tray_icon_new_for_screen (GdkScreen *screen, - const gchar *name); - -EggTrayIcon *egg_tray_icon_new (const gchar *name); - -guint egg_tray_icon_send_message (EggTrayIcon *icon, - gint timeout, - const char *message, - gint len); -void egg_tray_icon_cancel_message (EggTrayIcon *icon, - guint id); - -GtkOrientation egg_tray_icon_get_orientation (EggTrayIcon *icon); - -G_END_DECLS - -#endif /* __EGG_TRAY_ICON_H__ */ diff --git a/src/features_window.py b/src/features_window.py index 609959e65..c89b597cb 100644 --- a/src/features_window.py +++ b/src/features_window.py @@ -83,10 +83,6 @@ class FeaturesWindow: _('Passive popups notifying for new events.'), _('Requires python-notify or instead python-dbus in conjunction with notification-daemon.'), _('Feature not available under Windows.')), - _('Trayicon'): (self.trayicon_available, - _('A icon in systemtray reflecting the current presence.'), - _('Requires python-gnome2-extras or compiled trayicon module from Gajim sources.'), - _('Requires PyGTK >= 2.10.')), _('Automatic status'): (self.idle_available, _('Ability to measure idle time, in order to set auto status.'), _('Requires libxss library.'), @@ -240,15 +236,6 @@ class FeaturesWindow: return False return True - def trayicon_available(self): - if os.name == 'nt': - return True - try: - import systray - except Exception: - return False - return True - def idle_available(self): from common import sleepy return sleepy.SUPPORTED diff --git a/src/gui_interface.py b/src/gui_interface.py index 98c1f3298..dc8959f91 100644 --- a/src/gui_interface.py +++ b/src/gui_interface.py @@ -3122,7 +3122,7 @@ class Interface: gajim.ipython_window = window def run(self): - if self.systray_capabilities and gajim.config.get('trayicon') != 'never': + if gajim.config.get('trayicon') != 'never': self.show_systray() self.roster = roster_window.RosterWindow() @@ -3357,21 +3357,9 @@ class Interface: gtkgui_helpers.make_jabber_state_images() self.systray_enabled = False - self.systray_capabilities = False - if (os.name == 'nt'): - import statusicon - self.systray = statusicon.StatusIcon() - self.systray_capabilities = True - else: # use ours, not GTK+ one - # [FIXME: remove this when we migrate to 2.10 and we can do - # cool tooltips somehow and (not dying to keep) animation] - import systray - self.systray_capabilities = systray.HAS_SYSTRAY_CAPABILITIES - if self.systray_capabilities: - self.systray = systray.Systray() - else: - gajim.config.set('trayicon', 'never') + import statusicon + self.systray = statusicon.StatusIcon() path_to_file = os.path.join(gajim.DATA_DIR, 'pixmaps', 'gajim.png') pix = gtk.gdk.pixbuf_new_from_file(path_to_file) diff --git a/src/statusicon.py b/src/statusicon.py index 90bdb8c91..e161d18a4 100644 --- a/src/statusicon.py +++ b/src/statusicon.py @@ -25,26 +25,62 @@ import sys import gtk -import systray +import gobject +import os + +import dialogs +import config +import tooltips +import gtkgui_helpers +import tooltips from common import gajim from common import helpers +from common import pep -class StatusIcon(systray.Systray): +class StatusIcon: '''Class for the notification area icon''' - #NOTE: gtk api does NOT allow: - # leave, enter motion notify - # and can't do cool tooltips we use def __init__(self): - systray.Systray.__init__(self) + self.single_message_handler_id = None + self.new_chat_handler_id = None + # click somewhere else does not popdown menu. workaround this. + self.added_hide_menuitem = False + self.status = 'offline' + self.xml = gtkgui_helpers.get_glade('systray_context_menu.glade') + self.systray_context_menu = self.xml.get_widget('systray_context_menu') + self.xml.signal_autoconnect(self) + self.popup_menus = [] self.status_icon = None + self.tooltip = tooltips.NotificationAreaTooltip() + + def subscribe_events(self): + '''Register listeners to the events class''' + gajim.events.event_added_subscribe(self.on_event_added) + gajim.events.event_removed_subscribe(self.on_event_removed) + + def unsubscribe_events(self): + '''Unregister listeners to the events class''' + gajim.events.event_added_unsubscribe(self.on_event_added) + gajim.events.event_removed_unsubscribe(self.on_event_removed) + + def on_event_added(self, event): + '''Called when an event is added to the event list''' + if event.show_in_systray: + self.set_img() + + def on_event_removed(self, event_list): + '''Called when one or more events are removed from the event list''' + self.set_img() def show_icon(self): if not self.status_icon: self.status_icon = gtk.StatusIcon() + self.status_icon.set_property('has-tooltip', True) self.status_icon.connect('activate', self.on_status_icon_left_clicked) self.status_icon.connect('popup-menu', self.on_status_icon_right_clicked) + self.status_icon.connect('query-tooltip', + self.on_status_icon_query_tooltip) self.set_img() self.status_icon.set_visible(True) @@ -53,6 +89,11 @@ class StatusIcon(systray.Systray): def on_status_icon_right_clicked(self, widget, event_button, event_time): self.make_menu(event_button, event_time) + def on_status_icon_query_tooltip(self, widget, x, y, keyboard_mode, tooltip): + self.tooltip.populate() + tooltip.set_custom(self.tooltip.hbox) + return True + def hide_icon(self): self.status_icon.set_visible(False) self.unsubscribe_events() @@ -64,8 +105,6 @@ class StatusIcon(systray.Systray): '''apart from image, we also update tooltip text here''' if not gajim.interface.systray_enabled: return - text = helpers.get_notification_icon_tooltip_text() - self.status_icon.set_tooltip(text) if gajim.events.get_nb_systray_events(): state = 'event' self.status_icon.set_blinking(True) @@ -79,7 +118,304 @@ class StatusIcon(systray.Systray): self.status_icon.set_from_pixbuf(image.get_pixbuf()) #FIXME: oops they forgot to support GIF animation? #or they were lazy to get it to work under Windows! WTF! - #elif image.get_storage_type() == gtk.IMAGE_ANIMATION: + elif image.get_storage_type() == gtk.IMAGE_ANIMATION: + self.status_icon.set_from_pixbuf( + image.get_animation().get_static_image()) # self.img_tray.set_from_animation(image.get_animation()) + def change_status(self, global_status): + ''' set tray image to 'global_status' ''' + # change image and status, only if it is different + if global_status is not None and self.status != global_status: + self.status = global_status + self.set_img() + + def start_chat(self, widget, account, jid): + contact = gajim.contacts.get_first_contact_from_jid(account, jid) + if gajim.interface.msg_win_mgr.has_window(jid, account): + gajim.interface.msg_win_mgr.get_window(jid, account).set_active_tab( + jid, account) + elif contact: + gajim.interface.new_chat(contact, account) + gajim.interface.msg_win_mgr.get_window(jid, account).set_active_tab( + jid, account) + + def on_single_message_menuitem_activate(self, widget, account): + dialogs.SingleMessageWindow(account, action='send') + + def on_new_chat(self, widget, account): + dialogs.NewChatDialog(account) + + def make_menu(self, event_button, event_time): + '''create chat with and new message (sub) menus/menuitems''' + for m in self.popup_menus: + m.destroy() + + chat_with_menuitem = self.xml.get_widget('chat_with_menuitem') + single_message_menuitem = self.xml.get_widget( + 'single_message_menuitem') + status_menuitem = self.xml.get_widget('status_menu') + join_gc_menuitem = self.xml.get_widget('join_gc_menuitem') + sounds_mute_menuitem = self.xml.get_widget('sounds_mute_menuitem') + + if self.single_message_handler_id: + single_message_menuitem.handler_disconnect( + self.single_message_handler_id) + self.single_message_handler_id = None + if self.new_chat_handler_id: + chat_with_menuitem.disconnect(self.new_chat_handler_id) + self.new_chat_handler_id = None + + sub_menu = gtk.Menu() + self.popup_menus.append(sub_menu) + status_menuitem.set_submenu(sub_menu) + + gc_sub_menu = gtk.Menu() # gc is always a submenu + join_gc_menuitem.set_submenu(gc_sub_menu) + + # We need our own set of status icons, let's make 'em! + iconset = gajim.config.get('iconset') + path = os.path.join(helpers.get_iconset_path(iconset), '16x16') + state_images = gtkgui_helpers.load_iconset(path) + + if 'muc_active' in state_images: + join_gc_menuitem.set_image(state_images['muc_active']) + + for show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'): + uf_show = helpers.get_uf_show(show, use_mnemonic = True) + item = gtk.ImageMenuItem(uf_show) + item.set_image(state_images[show]) + sub_menu.append(item) + item.connect('activate', self.on_show_menuitem_activate, show) + + item = gtk.SeparatorMenuItem() + sub_menu.append(item) + + item = gtk.ImageMenuItem(_('_Change Status Message...')) + path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'kbd_input.png') + img = gtk.Image() + img.set_from_file(path) + item.set_image(img) + sub_menu.append(item) + item.connect('activate', self.on_change_status_message_activate) + + connected_accounts = gajim.get_number_of_connected_accounts() + if connected_accounts < 1: + item.set_sensitive(False) + + connected_accounts_with_private_storage = 0 + + item = gtk.SeparatorMenuItem() + sub_menu.append(item) + + uf_show = helpers.get_uf_show('offline', use_mnemonic = True) + item = gtk.ImageMenuItem(uf_show) + item.set_image(state_images['offline']) + sub_menu.append(item) + item.connect('activate', self.on_show_menuitem_activate, 'offline') + + iskey = connected_accounts > 0 and not (connected_accounts == 1 and + gajim.connections[gajim.connections.keys()[0]].is_zeroconf) + chat_with_menuitem.set_sensitive(iskey) + single_message_menuitem.set_sensitive(iskey) + join_gc_menuitem.set_sensitive(iskey) + + accounts_list = sorted(gajim.contacts.get_accounts()) + # items that get shown whether an account is zeroconf or not + if connected_accounts > 1: # 2 or more connections? make submenus + account_menu_for_chat_with = gtk.Menu() + chat_with_menuitem.set_submenu(account_menu_for_chat_with) + self.popup_menus.append(account_menu_for_chat_with) + + for account in accounts_list: + if gajim.account_is_connected(account): + # for chat_with + item = gtk.MenuItem(_('using account %s') % account) + account_menu_for_chat_with.append(item) + item.connect('activate', self.on_new_chat, account) + + elif connected_accounts == 1: # one account + # one account connected, no need to show 'as jid' + for account in gajim.connections: + if gajim.connections[account].connected > 1: + # for start chat + self.new_chat_handler_id = chat_with_menuitem.connect( + 'activate', self.on_new_chat, account) + break # No other connected account + + # menu items that don't apply to zeroconf connections + if connected_accounts == 1 or (connected_accounts == 2 and \ + gajim.zeroconf_is_connected()): + # only one 'real' (non-zeroconf) account is connected, don't need + # submenus + for account in gajim.connections: + if gajim.account_is_connected(account) and \ + not gajim.config.get_per('accounts', account, 'is_zeroconf'): + if gajim.connections[account].private_storage_supported: + connected_accounts_with_private_storage += 1 + + # for single message + single_message_menuitem.remove_submenu() + self.single_message_handler_id = single_message_menuitem.\ + connect('activate', + self.on_single_message_menuitem_activate, account) + # join gc + gajim.interface.roster.add_bookmarks_list(gc_sub_menu, + account) + break # No other account connected + else: + # 2 or more 'real' accounts are connected, make submenus + account_menu_for_single_message = gtk.Menu() + single_message_menuitem.set_submenu( + account_menu_for_single_message) + self.popup_menus.append(account_menu_for_single_message) + + for account in accounts_list: + if gajim.connections[account].is_zeroconf or \ + not gajim.account_is_connected(account): + continue + if gajim.connections[account].private_storage_supported: + connected_accounts_with_private_storage += 1 + # for single message + item = gtk.MenuItem(_('using account %s') % account) + item.connect('activate', + self.on_single_message_menuitem_activate, account) + account_menu_for_single_message.append(item) + + # join gc + gc_item = gtk.MenuItem(_('using account %s') % account, False) + gc_sub_menu.append(gc_item) + gc_menuitem_menu = gtk.Menu() + gajim.interface.roster.add_bookmarks_list(gc_menuitem_menu, + account) + gc_item.set_submenu(gc_menuitem_menu) + gc_sub_menu.show_all() + + newitem = gtk.SeparatorMenuItem() # separator + gc_sub_menu.append(newitem) + newitem = gtk.ImageMenuItem(_('_Manage Bookmarks...')) + img = gtk.image_new_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU) + newitem.set_image(img) + newitem.connect('activate', + gajim.interface.roster.on_manage_bookmarks_menuitem_activate) + gc_sub_menu.append(newitem) + if connected_accounts_with_private_storage == 0: + newitem.set_sensitive(False) + + sounds_mute_menuitem.set_active(not gajim.config.get('sounds_on')) + + if os.name == 'nt': + if self.added_hide_menuitem is False: + self.systray_context_menu.prepend(gtk.SeparatorMenuItem()) + item = gtk.MenuItem(_('Hide this menu')) + self.systray_context_menu.prepend(item) + self.added_hide_menuitem = True + + self.systray_context_menu.show_all() + self.systray_context_menu.popup(None, None, None, 0, + event_time) + + def on_show_all_events_menuitem_activate(self, widget): + events = gajim.events.get_systray_events() + for account in events: + for jid in events[account]: + for event in events[account][jid]: + gajim.interface.handle_event(account, jid, event.type_) + + def on_sounds_mute_menuitem_activate(self, widget): + gajim.config.set('sounds_on', not widget.get_active()) + gajim.interface.save_config() + + def on_show_roster_menuitem_activate(self, widget): + win = gajim.interface.roster.window + win.present() + + def on_preferences_menuitem_activate(self, widget): + if 'preferences' in gajim.interface.instances: + gajim.interface.instances['preferences'].window.present() + else: + gajim.interface.instances['preferences'] = config.PreferencesWindow() + + def on_quit_menuitem_activate(self, widget): + gajim.interface.roster.on_quit_request() + + def on_left_click(self): + win = gajim.interface.roster.window + if len(gajim.events.get_systray_events()) == 0: + # No pending events, so toggle visible/hidden for roster window + if not win.iconify_initially and (win.get_property( + 'has-toplevel-focus') or os.name == 'nt'): + # visible in ANY virtual desktop? + + # we could be in another VD right now. eg vd2 + # and we want to show it in vd2 + if not gtkgui_helpers.possibly_move_window_in_current_desktop(win): + win.set_property('skip-taskbar-hint', False) + win.iconify() # else we hide it from VD that was visible in + win.set_property('skip-taskbar-hint', True) + else: + win.deiconify() + if not gajim.config.get('roster_window_skip_taskbar'): + win.set_property('skip-taskbar-hint', False) + win.present_with_time(gtk.get_current_event_time()) + else: + self.handle_first_event() + + def handle_first_event(self): + account, jid, event = gajim.events.get_first_systray_event() + if not event: + return + gajim.interface.handle_event(account, jid, event.type_) + + def on_middle_click(self): + '''middle click raises window to have complete focus (fe. get kbd events) + but if already raised, it hides it''' + win = gajim.interface.roster.window + if win.is_active(): # is it fully raised? (eg does it receive kbd events?) + win.hide() + else: + win.present() + + def on_clicked(self, widget, event): + self.on_tray_leave_notify_event(widget, None) + if event.type != gtk.gdk.BUTTON_PRESS: + return + if event.button == 1: # Left click + self.on_left_click() + elif event.button == 2: # middle click + self.on_middle_click() + elif event.button == 3: # right click + self.make_menu(event.button, event.time) + + def on_show_menuitem_activate(self, widget, show): + # we all add some fake (we cannot select those nor have them as show) + # but this helps to align with roster's status_combobox index positions + l = ['online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'SEPARATOR', + 'CHANGE_STATUS_MSG_MENUITEM', 'SEPARATOR', 'offline'] + index = l.index(show) + if not helpers.statuses_unified(): + gajim.interface.roster.status_combobox.set_active(index + 2) + return + current = gajim.interface.roster.status_combobox.get_active() + if index != current: + gajim.interface.roster.status_combobox.set_active(index) + + def on_change_status_message_activate(self, widget): + model = gajim.interface.roster.status_combobox.get_model() + active = gajim.interface.roster.status_combobox.get_active() + status = model[active][2].decode('utf-8') + def on_response(message, pep_dict): + if message is None: # None if user press Cancel + return + accounts = gajim.connections.keys() + for acct in accounts: + if not gajim.config.get_per('accounts', acct, + 'sync_with_global_status'): + continue + show = gajim.SHOW_LIST[gajim.connections[acct].connected] + gajim.interface.roster.send_status(acct, show, message) + gajim.interface.roster.send_pep(acct, pep_dict) + dlg = dialogs.ChangeStatusMessageDialog(on_response, status) + dlg.dialog.present() + # vim: se ts=3: diff --git a/src/systray.py b/src/systray.py deleted file mode 100644 index e2c09331b..000000000 --- a/src/systray.py +++ /dev/null @@ -1,459 +0,0 @@ -# -*- coding:utf-8 -*- -## src/systray.py -## -## Copyright (C) 2003-2005 Vincent Hanquez -## Copyright (C) 2003-2008 Yann Leboulanger -## Copyright (C) 2005 Norman Rasmussen -## Copyright (C) 2005-2006 Dimitur Kirov -## Travis Shirk -## Copyright (C) 2005-2007 Nikos Kouremenos -## Copyright (C) 2006 Stefan Bethge -## Copyright (C) 2006-2008 Jean-Marie Traissard -## Copyright (C) 2007 Lukas Petrovicky -## Julien Pivotto -## -## This file is part of Gajim. -## -## Gajim is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published -## by the Free Software Foundation; version 3 only. -## -## Gajim is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. -## -## You should have received a copy of the GNU General Public License -## along with Gajim. If not, see . -## - -import gtk -import gobject -import os - -import dialogs -import config -import tooltips -import gtkgui_helpers - -from common import gajim -from common import helpers -from common import pep - -HAS_SYSTRAY_CAPABILITIES = True - -try: - import egg.trayicon as trayicon # gnomepythonextras trayicon -except Exception: - try: - import trayicon # our trayicon - except Exception: - gajim.log.debug('No trayicon module available') - HAS_SYSTRAY_CAPABILITIES = False - - -class Systray: - '''Class for icon in the notification area - This class is both base class (for statusicon.py) and normal class - for trayicon in GNU/Linux''' - - def __init__(self): - self.single_message_handler_id = None - self.new_chat_handler_id = None - self.t = None - # click somewhere else does not popdown menu. workaround this. - self.added_hide_menuitem = False - self.img_tray = gtk.Image() - self.status = 'offline' - self.xml = gtkgui_helpers.get_glade('systray_context_menu.glade') - self.systray_context_menu = self.xml.get_widget('systray_context_menu') - self.xml.signal_autoconnect(self) - self.popup_menus = [] - - def subscribe_events(self): - '''Register listeners to the events class''' - gajim.events.event_added_subscribe(self.on_event_added) - gajim.events.event_removed_subscribe(self.on_event_removed) - - def unsubscribe_events(self): - '''Unregister listeners to the events class''' - gajim.events.event_added_unsubscribe(self.on_event_added) - gajim.events.event_removed_unsubscribe(self.on_event_removed) - - def on_event_added(self, event): - '''Called when an event is added to the event list''' - if event.show_in_systray: - self.set_img() - - def on_event_removed(self, event_list): - '''Called when one or more events are removed from the event list''' - self.set_img() - - def set_img(self): - if not gajim.interface.systray_enabled: - return - if gajim.events.get_nb_systray_events(): - state = 'event' - else: - state = self.status - if state != 'event' and gajim.config.get('trayicon') == 'on_event': - self.t.hide() - else: - self.t.show() - image = gajim.interface.jabber_state_images['16'][state] - if image.get_storage_type() == gtk.IMAGE_ANIMATION: - self.img_tray.set_from_animation(image.get_animation()) - elif image.get_storage_type() == gtk.IMAGE_PIXBUF: - self.img_tray.set_from_pixbuf(image.get_pixbuf()) - - def change_status(self, global_status): - ''' set tray image to 'global_status' ''' - # change image and status, only if it is different - if global_status is not None and self.status != global_status: - self.status = global_status - self.set_img() - - def start_chat(self, widget, account, jid): - contact = gajim.contacts.get_first_contact_from_jid(account, jid) - if gajim.interface.msg_win_mgr.has_window(jid, account): - gajim.interface.msg_win_mgr.get_window(jid, account).set_active_tab( - jid, account) - elif contact: - gajim.interface.new_chat(contact, account) - gajim.interface.msg_win_mgr.get_window(jid, account).set_active_tab( - jid, account) - - def on_single_message_menuitem_activate(self, widget, account): - dialogs.SingleMessageWindow(account, action = 'send') - - def on_new_chat(self, widget, account): - dialogs.NewChatDialog(account) - - def make_menu(self, event_button, event_time): - '''create chat with and new message (sub) menus/menuitems''' - for m in self.popup_menus: - m.destroy() - - chat_with_menuitem = self.xml.get_widget('chat_with_menuitem') - single_message_menuitem = self.xml.get_widget( - 'single_message_menuitem') - status_menuitem = self.xml.get_widget('status_menu') - join_gc_menuitem = self.xml.get_widget('join_gc_menuitem') - sounds_mute_menuitem = self.xml.get_widget('sounds_mute_menuitem') - - if self.single_message_handler_id: - single_message_menuitem.handler_disconnect( - self.single_message_handler_id) - self.single_message_handler_id = None - if self.new_chat_handler_id: - chat_with_menuitem.disconnect(self.new_chat_handler_id) - self.new_chat_handler_id = None - - sub_menu = gtk.Menu() - self.popup_menus.append(sub_menu) - status_menuitem.set_submenu(sub_menu) - - gc_sub_menu = gtk.Menu() # gc is always a submenu - join_gc_menuitem.set_submenu(gc_sub_menu) - - # We need our own set of status icons, let's make 'em! - iconset = gajim.config.get('iconset') - path = os.path.join(helpers.get_iconset_path(iconset), '16x16') - state_images = gtkgui_helpers.load_iconset(path) - - if 'muc_active' in state_images: - join_gc_menuitem.set_image(state_images['muc_active']) - - for show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'): - uf_show = helpers.get_uf_show(show, use_mnemonic = True) - item = gtk.ImageMenuItem(uf_show) - item.set_image(state_images[show]) - sub_menu.append(item) - item.connect('activate', self.on_show_menuitem_activate, show) - - item = gtk.SeparatorMenuItem() - sub_menu.append(item) - - item = gtk.ImageMenuItem(_('_Change Status Message...')) - path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'kbd_input.png') - img = gtk.Image() - img.set_from_file(path) - item.set_image(img) - sub_menu.append(item) - item.connect('activate', self.on_change_status_message_activate) - - connected_accounts = gajim.get_number_of_connected_accounts() - if connected_accounts < 1: - item.set_sensitive(False) - - connected_accounts_with_private_storage = 0 - - item = gtk.SeparatorMenuItem() - sub_menu.append(item) - - uf_show = helpers.get_uf_show('offline', use_mnemonic = True) - item = gtk.ImageMenuItem(uf_show) - item.set_image(state_images['offline']) - sub_menu.append(item) - item.connect('activate', self.on_show_menuitem_activate, 'offline') - - iskey = connected_accounts > 0 and not (connected_accounts == 1 and - gajim.connections[gajim.connections.keys()[0]].is_zeroconf) - chat_with_menuitem.set_sensitive(iskey) - single_message_menuitem.set_sensitive(iskey) - join_gc_menuitem.set_sensitive(iskey) - - accounts_list = sorted(gajim.contacts.get_accounts()) - # items that get shown whether an account is zeroconf or not - if connected_accounts > 1: # 2 or more connections? make submenus - account_menu_for_chat_with = gtk.Menu() - chat_with_menuitem.set_submenu(account_menu_for_chat_with) - self.popup_menus.append(account_menu_for_chat_with) - - for account in accounts_list: - if gajim.account_is_connected(account): - # for chat_with - item = gtk.MenuItem(_('using account %s') % account) - account_menu_for_chat_with.append(item) - item.connect('activate', self.on_new_chat, account) - - elif connected_accounts == 1: # one account - # one account connected, no need to show 'as jid' - for account in gajim.connections: - if gajim.connections[account].connected > 1: - # for start chat - self.new_chat_handler_id = chat_with_menuitem.connect( - 'activate', self.on_new_chat, account) - break # No other connected account - - # menu items that don't apply to zeroconf connections - if connected_accounts == 1 or (connected_accounts == 2 and \ - gajim.zeroconf_is_connected()): - # only one 'real' (non-zeroconf) account is connected, don't need - # submenus - for account in gajim.connections: - if gajim.account_is_connected(account) and \ - not gajim.config.get_per('accounts', account, 'is_zeroconf'): - if gajim.connections[account].private_storage_supported: - connected_accounts_with_private_storage += 1 - - # for single message - single_message_menuitem.remove_submenu() - self.single_message_handler_id = single_message_menuitem.\ - connect('activate', - self.on_single_message_menuitem_activate, account) - # join gc - gajim.interface.roster.add_bookmarks_list(gc_sub_menu, - account) - break # No other account connected - else: - # 2 or more 'real' accounts are connected, make submenus - account_menu_for_single_message = gtk.Menu() - single_message_menuitem.set_submenu( - account_menu_for_single_message) - self.popup_menus.append(account_menu_for_single_message) - - for account in accounts_list: - if gajim.connections[account].is_zeroconf or \ - not gajim.account_is_connected(account): - continue - if gajim.connections[account].private_storage_supported: - connected_accounts_with_private_storage += 1 - # for single message - item = gtk.MenuItem(_('using account %s') % account) - item.connect('activate', - self.on_single_message_menuitem_activate, account) - account_menu_for_single_message.append(item) - - # join gc - gc_item = gtk.MenuItem(_('using account %s') % account, False) - gc_sub_menu.append(gc_item) - gc_menuitem_menu = gtk.Menu() - gajim.interface.roster.add_bookmarks_list(gc_menuitem_menu, - account) - gc_item.set_submenu(gc_menuitem_menu) - gc_sub_menu.show_all() - - newitem = gtk.SeparatorMenuItem() # separator - gc_sub_menu.append(newitem) - newitem = gtk.ImageMenuItem(_('_Manage Bookmarks...')) - img = gtk.image_new_from_stock(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_MENU) - newitem.set_image(img) - newitem.connect('activate', - gajim.interface.roster.on_manage_bookmarks_menuitem_activate) - gc_sub_menu.append(newitem) - if connected_accounts_with_private_storage == 0: - newitem.set_sensitive(False) - - sounds_mute_menuitem.set_active(not gajim.config.get('sounds_on')) - - if os.name == 'nt': - if self.added_hide_menuitem is False: - self.systray_context_menu.prepend(gtk.SeparatorMenuItem()) - item = gtk.MenuItem(_('Hide this menu')) - self.systray_context_menu.prepend(item) - self.added_hide_menuitem = True - - self.systray_context_menu.show_all() - self.systray_context_menu.popup(None, None, None, 0, - event_time) - - def on_show_all_events_menuitem_activate(self, widget): - events = gajim.events.get_systray_events() - for account in events: - for jid in events[account]: - for event in events[account][jid]: - gajim.interface.handle_event(account, jid, event.type_) - - def on_sounds_mute_menuitem_activate(self, widget): - gajim.config.set('sounds_on', not widget.get_active()) - gajim.interface.save_config() - - def on_show_roster_menuitem_activate(self, widget): - win = gajim.interface.roster.window - win.present() - - def on_preferences_menuitem_activate(self, widget): - if 'preferences' in gajim.interface.instances: - gajim.interface.instances['preferences'].window.present() - else: - gajim.interface.instances['preferences'] = config.PreferencesWindow() - - def on_quit_menuitem_activate(self, widget): - gajim.interface.roster.on_quit_request() - - def on_left_click(self): - win = gajim.interface.roster.window - if len(gajim.events.get_systray_events()) == 0: - # No pending events, so toggle visible/hidden for roster window - if not win.iconify_initially and (win.get_property( - 'has-toplevel-focus') or os.name == 'nt'): - # visible in ANY virtual desktop? - - # we could be in another VD right now. eg vd2 - # and we want to show it in vd2 - if not gtkgui_helpers.possibly_move_window_in_current_desktop(win): - win.set_property('skip-taskbar-hint', False) - win.iconify() # else we hide it from VD that was visible in - win.set_property('skip-taskbar-hint', True) - else: - win.deiconify() - if not gajim.config.get('roster_window_skip_taskbar'): - win.set_property('skip-taskbar-hint', False) - win.present_with_time(gtk.get_current_event_time()) - else: - self.handle_first_event() - - def handle_first_event(self): - account, jid, event = gajim.events.get_first_systray_event() - if not event: - return - gajim.interface.handle_event(account, jid, event.type_) - - def on_middle_click(self): - '''middle click raises window to have complete focus (fe. get kbd events) - but if already raised, it hides it''' - win = gajim.interface.roster.window - if win.is_active(): # is it fully raised? (eg does it receive kbd events?) - win.hide() - else: - win.present() - - def on_clicked(self, widget, event): - self.on_tray_leave_notify_event(widget, None) - if event.type != gtk.gdk.BUTTON_PRESS: - return - if event.button == 1: # Left click - self.on_left_click() - elif event.button == 2: # middle click - self.on_middle_click() - elif event.button == 3: # right click - self.make_menu(event.button, event.time) - - def on_show_menuitem_activate(self, widget, show): - # we all add some fake (we cannot select those nor have them as show) - # but this helps to align with roster's status_combobox index positions - l = ['online', 'chat', 'away', 'xa', 'dnd', 'invisible', 'SEPARATOR', - 'CHANGE_STATUS_MSG_MENUITEM', 'SEPARATOR', 'offline'] - index = l.index(show) - if not helpers.statuses_unified(): - gajim.interface.roster.status_combobox.set_active(index + 2) - return - current = gajim.interface.roster.status_combobox.get_active() - if index != current: - gajim.interface.roster.status_combobox.set_active(index) - - def on_change_status_message_activate(self, widget): - model = gajim.interface.roster.status_combobox.get_model() - active = gajim.interface.roster.status_combobox.get_active() - status = model[active][2].decode('utf-8') - def on_response(message, pep_dict): - if message is None: # None if user press Cancel - return - accounts = gajim.connections.keys() - for acct in accounts: - if not gajim.config.get_per('accounts', acct, - 'sync_with_global_status'): - continue - show = gajim.SHOW_LIST[gajim.connections[acct].connected] - gajim.interface.roster.send_status(acct, show, message) - gajim.interface.roster.send_pep(acct, pep_dict) - dlg = dialogs.ChangeStatusMessageDialog(on_response, status) - dlg.dialog.present() - - def show_tooltip(self, widget): - position = widget.window.get_origin() - if self.tooltip.id == position: - size = widget.window.get_size() - self.tooltip.show_tooltip('', size[1], position[1]) - - def on_tray_motion_notify_event(self, widget, event): - position = widget.window.get_origin() - if self.tooltip.timeout > 0: - if self.tooltip.id != position: - self.tooltip.hide_tooltip() - if self.tooltip.timeout == 0 and \ - self.tooltip.id != position: - self.tooltip.id = position - self.tooltip.timeout = gobject.timeout_add(500, - self.show_tooltip, widget) - - def on_tray_leave_notify_event(self, widget, event): - position = widget.window.get_origin() - if self.tooltip.timeout > 0 and \ - self.tooltip.id == position: - self.tooltip.hide_tooltip() - - def on_tray_destroyed(self, widget): - '''re-add trayicon when systray is destroyed''' - self.t = None - if gajim.interface.systray_enabled: - self.show_icon() - - def show_icon(self): - if not self.t: - self.t = trayicon.TrayIcon('Gajim') - self.t.connect('destroy', self.on_tray_destroyed) - eb = gtk.EventBox() - # avoid draw seperate bg color in some gtk themes - eb.set_visible_window(False) - eb.set_events(gtk.gdk.POINTER_MOTION_MASK) - eb.connect('button-press-event', self.on_clicked) - eb.connect('motion-notify-event', self.on_tray_motion_notify_event) - eb.connect('leave-notify-event', self.on_tray_leave_notify_event) - self.tooltip = tooltips.NotificationAreaTooltip() - - self.img_tray = gtk.Image() - eb.add(self.img_tray) - self.t.add(eb) - self.set_img() - self.subscribe_events() - self.t.show_all() - - def hide_icon(self): - if self.t: - self.t.destroy() - self.t = None - self.unsubscribe_events() - -# vim: se ts=3: diff --git a/src/tooltips.py b/src/tooltips.py index 847e6a17a..5e55166c0 100644 --- a/src/tooltips.py +++ b/src/tooltips.py @@ -269,7 +269,7 @@ class NotificationAreaTooltip(BaseTooltip, StatusTable): for line in acct['event_lines']: self.add_text_row(' ' + line, 1) - def populate(self, data): + def populate(self, data=''): self.create_window() self.create_table() @@ -280,7 +280,7 @@ class NotificationAreaTooltip(BaseTooltip, StatusTable): self.table.set_property('column-spacing', 1) self.hbox.add(self.table) - self.win.add(self.hbox) + self.hbox.show_all() class GCTooltip(BaseTooltip): ''' Tooltip that is shown in the GC treeview ''' diff --git a/src/trayicon.defs b/src/trayicon.defs deleted file mode 100644 index 6d253cf83..000000000 --- a/src/trayicon.defs +++ /dev/null @@ -1,59 +0,0 @@ -;; -*- scheme -*- - -; object definitions ... -(define-object TrayIcon - (in-module "Egg") - (parent "GtkPlug") - (c-name "EggTrayIcon") - (gtype-id "EGG_TYPE_TRAY_ICON") -) - -;; Enumerations and flags ... - - -;; From eggtrayicon.h - -(define-function egg_tray_icon_get_type - (c-name "egg_tray_icon_get_type") - (return-type "GType") -) - -(define-function egg_tray_icon_new_for_screen - (c-name "egg_tray_icon_new_for_screen") - (return-type "EggTrayIcon*") - (parameters - '("GdkScreen*" "screen") - '("const-gchar*" "name") - ) -) - -(define-function egg_tray_icon_new - (c-name "egg_tray_icon_new") - (is-constructor-of "EggTrayIcon") - (return-type "EggTrayIcon*") - (parameters - '("const-gchar*" "name") - ) -) - -(define-method send_message - (of-object "EggTrayIcon") - (c-name "egg_tray_icon_send_message") - (return-type "guint") - (parameters - '("gint" "timeout") - '("const-char*" "message") - '("gint" "len") - ) -) - -(define-method cancel_message - (of-object "EggTrayIcon") - (c-name "egg_tray_icon_cancel_message") - (return-type "none") - (parameters - '("guint" "id") - ) -) - - diff --git a/src/trayicon.override b/src/trayicon.override deleted file mode 100644 index 75bd7ed8f..000000000 --- a/src/trayicon.override +++ /dev/null @@ -1,47 +0,0 @@ -/* -*- Mode: C; c-basic-offset: 4 -*- - * src/trayicon.override - * - * Copyright (C) 2004-2005 Yann Leboulanger - * - * This file is part of Gajim. - * - * Gajim is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published - * by the Free Software Foundation; version 3 only. - * - * Gajim is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Gajim. If not, see . - */ -%% -headers -#include - -#include "pygobject.h" -#include "eggtrayicon.h" -%% -modulename trayicon -%% -import gtk.Plug as PyGtkPlug_Type -import gtk.gdk.Screen as PyGdkScreen_Type -%% -ignore-glob - *_get_type -%% -override egg_tray_icon_send_message kwargs -static PyObject* -_wrap_egg_tray_icon_send_message(PyGObject *self, PyObject *args, PyObject *kwargs) -{ - static char *kwlist[] = {"timeout", "message", NULL}; - int timeout, len, ret; - char *message; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "is#:TrayIcon.send_message", kwlist, &timeout, &message, &len)) - return NULL; - ret = egg_tray_icon_send_message(EGG_TRAY_ICON(self->obj), timeout, message, len); - return PyInt_FromLong(ret); -} diff --git a/src/trayiconmodule.c b/src/trayiconmodule.c deleted file mode 100644 index 7a2b1206f..000000000 --- a/src/trayiconmodule.c +++ /dev/null @@ -1,43 +0,0 @@ -/* -*- Mode: C; c-basic-offset: 4 -*- - * src/trayiconmodule.c - * - * Copyright (C) 2004-2005 Yann Leboulanger - * - * This file is part of Gajim. - * - * Gajim is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published - * by the Free Software Foundation; version 3 only. - * - * Gajim is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Gajim. If not, see . - */ - -/* include this first, before NO_IMPORT_PYGOBJECT is defined */ -#include - -void trayicon_register_classes (PyObject *d); - -extern PyMethodDef trayicon_functions[]; - -DL_EXPORT(void) -inittrayicon(void) -{ - PyObject *m, *d; - - init_pygobject (); - - m = Py_InitModule ("trayicon", trayicon_functions); - d = PyModule_GetDict (m); - - trayicon_register_classes (d); - - if (PyErr_Occurred ()) { - Py_FatalError ("can't initialise module trayicon :("); - } -} From e3dbbed2dd95da39cf0651ae4ddb1bff73db73dc Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Tue, 24 Nov 2009 15:06:04 +0100 Subject: [PATCH 52/73] auto-increment order value in privacy list entries. Fixes #5441 --- src/dialogs.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/dialogs.py b/src/dialogs.py index 4d197a896..4f2802e75 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -3080,6 +3080,8 @@ class PrivacyListWindow: self.global_rules = {} self.list_of_groups = {} + self.max_order = 0 + # Default Edit Values self.edit_rule_type = 'jid' self.allow_deny = 'allow' @@ -3175,6 +3177,8 @@ class PrivacyListWindow: else: text_item = _('Order: %(order)s, action: %(action)s') % \ {'order': rule['order'], 'action': rule['action']} + if int(rule['order']) > self.max_order: + self.max_order = int(rule['order']) self.global_rules[text_item] = rule self.list_of_rules_combobox.append_text(text_item) if len(rules) == 0: @@ -3322,7 +3326,7 @@ class PrivacyListWindow: self.edit_view_status_checkbutton.set_active(False) self.edit_send_status_checkbutton.set_active(False) self.edit_all_checkbutton.set_active(False) - self.edit_order_spinbutton.set_value(1) + self.edit_order_spinbutton.set_value(self.max_order + 1) self.edit_type_group_combobox.set_active(0) self.edit_type_subscription_combobox.set_active(0) self.add_edit_rule_label.set_label( @@ -3365,6 +3369,8 @@ class PrivacyListWindow: def on_save_rule_button_clicked(self, widget): tags=[] current_tags = self.get_current_tags() + if int(current_tags['order']) > self.max_order: + self.max_order = int(current_tags['order']) if self.active_rule == '': tags.append(current_tags) From 478454985b72464d7bccf2916a142823e714cddc Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Tue, 24 Nov 2009 15:41:16 +0100 Subject: [PATCH 53/73] fix strimg comparison according to locales. Fixes #4201 --- src/groupchat_control.py | 11 +++-------- src/roster_window.py | 20 +++++++++++--------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/groupchat_control.py b/src/groupchat_control.py index e4db34610..4653d0d76 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -29,6 +29,7 @@ import os import time +import locale import gtk import pango import gobject @@ -394,9 +395,7 @@ class GroupchatControl(ChatControlBase): nick1 = nick1.decode('utf-8') nick2 = nick2.decode('utf-8') if type1 == 'role': - if nick1 < nick2: - return -1 - return 1 + return locale.strcoll(nick1, nick2) if type1 == 'contact': gc_contact1 = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick1) @@ -420,11 +419,7 @@ class GroupchatControl(ChatControlBase): # We compare names name1 = gc_contact1.get_shown_name() name2 = gc_contact2.get_shown_name() - if name1.lower() < name2.lower(): - return -1 - if name2.lower() < name1.lower(): - return 1 - return 0 + return locale.strcoll(name1.lower(), name2.lower()) def on_msg_textview_populate_popup(self, textview, menu): '''we override the default context menu and we prepend Clear diff --git a/src/roster_window.py b/src/roster_window.py index 6e3bcdc2d..900d973c4 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -38,6 +38,7 @@ import gobject import os import sys import time +import locale import common.sleepy import history_window @@ -1558,9 +1559,7 @@ class RosterWindow: account1 = account1.decode('utf-8') account2 = account2.decode('utf-8') if type1 == 'account': - if account1 < account2: - return -1 - return 1 + return locale.strcoll(account1, account2) jid1 = model[iter1][C_JID].decode('utf-8') jid2 = model[iter2][C_JID].decode('utf-8') if type1 == 'contact': @@ -1607,20 +1606,23 @@ class RosterWindow: elif show1 > show2: return 1 # We compare names - if name1.lower() < name2.lower(): + cmp_result = locale.strcoll(name1.lower(), name2.lower()) + if cmp_result < 0: return -1 - if name2.lower() < name1.lower(): + if cmp_result > 0: return 1 if type1 == 'contact' and type2 == 'contact': # We compare account names - if account1.lower() < account2.lower(): + cmp_result = locale.strcoll(account1.lower(), account2.lower()) + if cmp_result < 0: return -1 - if account2.lower() < account1.lower(): + if cmp_result > 0: return 1 # We compare jids - if jid1.lower() < jid2.lower(): + cmp_result = locale.strcoll(jid1.lower(), jid2.lower()) + if cmp_result < 0: return -1 - if jid2.lower() < jid1.lower(): + if cmp_result > 0: return 1 return 0 From 39dc648ec81933867520d2584c6174b1b0f209aa Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Tue, 24 Nov 2009 15:42:31 +0100 Subject: [PATCH 54/73] don't use removed variables --- src/config.py | 2 -- src/roster_window.py | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/config.py b/src/config.py index a8e3b0902..d8ad836f7 100644 --- a/src/config.py +++ b/src/config.py @@ -297,8 +297,6 @@ class PreferencesWindow: systray_combobox.set_active(1) else: systray_combobox.set_active(2) - if not gajim.interface.systray_capabilities: - systray_combobox.set_sensitive(False) # sounds if gajim.config.get('sounds_on'): diff --git a/src/roster_window.py b/src/roster_window.py index 900d973c4..344c4574a 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -5809,10 +5809,8 @@ class RosterWindow: if gajim.config.get('show_roster_on_startup'): self.window.show_all() else: - if not gajim.config.get('trayicon') or not \ - gajim.interface.systray_capabilities: - # cannot happen via GUI, but I put this incase user touches - # config. without trayicon, he or she should see the roster! + if gajim.config.get('trayicon') != 'always': + # Without trayicon, user should see the roster! self.window.show_all() gajim.config.set('show_roster_on_startup', True) From 5ce4d52dc3d30684964ea8cc8a406cbc27a719fc Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Tue, 24 Nov 2009 19:41:29 +0100 Subject: [PATCH 55/73] Don't clean dict twice, Fixes #5419 --- src/gui_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui_interface.py b/src/gui_interface.py index dc8959f91..7c7825100 100644 --- a/src/gui_interface.py +++ b/src/gui_interface.py @@ -1945,10 +1945,10 @@ class Interface: # ('INSECURE_SSL_CONNECTION', account, (connection, connection_type)) server = gajim.config.get_per('accounts', account, 'hostname') def on_ok(is_checked): - del self.instances[account]['online_dialog']['insecure_ssl'] if not is_checked[0]: on_cancel() return + del self.instances[account]['online_dialog']['insecure_ssl'] if is_checked[1]: gajim.config.set_per('accounts', account, 'warn_when_insecure_ssl_connection', False) From bbf12be6f223c1c46514c00ff434b2fd039dc517 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Tue, 24 Nov 2009 20:07:05 +0100 Subject: [PATCH 56/73] set AccountCreationWizardWindow transient for RosterWindow. Fixes #5444 --- src/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config.py b/src/config.py index d8ad836f7..727744a04 100644 --- a/src/config.py +++ b/src/config.py @@ -3146,6 +3146,7 @@ class AccountCreationWizardWindow: self.xml = gtkgui_helpers.get_glade( 'account_creation_wizard_window.glade') self.window = self.xml.get_widget('account_creation_wizard_window') + self.window.set_transient_for(gajim.interface.roster.window) completion = gtk.EntryCompletion() # Connect events from comboboxentry.child From d66dd14950dbbcfec06747c142bad906ed12605b Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Tue, 24 Nov 2009 20:50:02 +0100 Subject: [PATCH 57/73] update .hgignore --- .hgignore | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.hgignore b/.hgignore index 1f56821c2..962a2aafb 100644 --- a/.hgignore +++ b/.hgignore @@ -2,13 +2,9 @@ syntax: glob *.orig *.gmo *.in -*.la -*.lo *.m4 *.pyc *.pyo -*.o -*.Plo *~ autom4te.cache data/defs.py @@ -23,6 +19,4 @@ Makefile syntax: regexp ^config\.* ^config\/ -^src\/\.libs -^src\/trayicon.c ^scripts\/gajim.* From c5843e8878591452f7a51ea6ba6a37a80c4c163c Mon Sep 17 00:00:00 2001 From: Alexander Cherniuk Date: Wed, 25 Nov 2009 09:10:30 +0200 Subject: [PATCH 58/73] Fixes #5447 --- src/common/passwords.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common/passwords.py b/src/common/passwords.py index 98a7f61d9..9000085c4 100644 --- a/src/common/passwords.py +++ b/src/common/passwords.py @@ -122,6 +122,8 @@ class GnomePasswordStorage(PasswordStorage): user = gajim.config.get_per('accounts', account_name, 'name') display_name = _('XMPP account %s@%s') % (user, server) attributes1 = dict(server=str(server), user=str(user), protocol='xmpp') + if password is None: + password = str() try: auth_token = gnomekeyring.item_create_sync( self.keyring, gnomekeyring.ITEM_NETWORK_PASSWORD, From ddb493c87d505071bab147a479d1fc1075239715 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 25 Nov 2009 08:27:06 +0100 Subject: [PATCH 59/73] always use statusicon in systray, not event icon, we already blink. --- src/statusicon.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/statusicon.py b/src/statusicon.py index e161d18a4..e28b67cfd 100644 --- a/src/statusicon.py +++ b/src/statusicon.py @@ -106,18 +106,16 @@ class StatusIcon: if not gajim.interface.systray_enabled: return if gajim.events.get_nb_systray_events(): - state = 'event' self.status_icon.set_blinking(True) else: - state = self.status self.status_icon.set_blinking(False) - #FIXME: do not always use 16x16 (ask actually used size and use that) - image = gajim.interface.jabber_state_images['16'][state] + # FIXME: do not always use 16x16 (ask actually used size and use that) + image = gajim.interface.jabber_state_images['16'][self.status] if image.get_storage_type() == gtk.IMAGE_PIXBUF: self.status_icon.set_from_pixbuf(image.get_pixbuf()) - #FIXME: oops they forgot to support GIF animation? - #or they were lazy to get it to work under Windows! WTF! + # FIXME: oops they forgot to support GIF animation? + # or they were lazy to get it to work under Windows! WTF! elif image.get_storage_type() == gtk.IMAGE_ANIMATION: self.status_icon.set_from_pixbuf( image.get_animation().get_static_image()) From 7eb24c3c53bbcb9c0517933982c673d40518204f Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 25 Nov 2009 12:25:28 +0100 Subject: [PATCH 60/73] fix traceback when using a non-BOSH proxy. Fixes #5449 --- src/common/xmpp/transports_nb.py | 2 +- src/profile_window.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py index 1aac5b52d..e6d63bcb3 100644 --- a/src/common/xmpp/transports_nb.py +++ b/src/common/xmpp/transports_nb.py @@ -72,7 +72,7 @@ def get_proxy_data_from_dict(proxy): # with proxy!=bosh or with bosh over HTTP proxy we're connecting to proxy # machine tcp_host, tcp_port = proxy['host'], proxy['port'] - if proxy['useauth']: + if proxy.get('useauth', False): proxy_user, proxy_pass = proxy['user'], proxy['pass'] return tcp_host, tcp_port, proxy_user, proxy_pass diff --git a/src/profile_window.py b/src/profile_window.py index 132357b0b..d00bfc335 100644 --- a/src/profile_window.py +++ b/src/profile_window.py @@ -68,7 +68,7 @@ class ProfileWindow: return True # loop forever def remove_statusbar(self, message_id): - self.statusbar.remove(self.context_id, message_id) + self.statusbar.remove_message(self.context_id, message_id) self.remove_statusbar_timeout_id = None def on_profile_window_destroy(self, widget): @@ -246,7 +246,7 @@ class ProfileWindow: self.set_value(i + '_entry', vcard_[i]) if self.update_progressbar_timeout_id is not None: if self.message_id: - self.statusbar.remove(self.context_id, self.message_id) + self.statusbar.remove_message(self.context_id, self.message_id) self.message_id = self.statusbar.push(self.context_id, _('Information received')) self.remove_statusbar_timeout_id = gobject.timeout_add_seconds(3, @@ -341,7 +341,7 @@ class ProfileWindow: def vcard_not_published(self): if self.message_id: - self.statusbar.remove(self.context_id, self.message_id) + self.statusbar.remove_message(self.context_id, self.message_id) self.message_id = self.statusbar.push(self.context_id, _('Information NOT published')) self.remove_statusbar_timeout_id = gobject.timeout_add_seconds(3, From 6c4724f53fe25cc8a74fe5a0313c1a52ff50f962 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 25 Nov 2009 12:31:40 +0100 Subject: [PATCH 61/73] fix attribute name. Fixes #5448 --- src/common/connection_handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index fb2425192..8bcf9b51c 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -1877,7 +1877,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, for group in contact.groups: if group not in groups: same_groups = False - if contact.subscription in ('both', 'to') and same_groups: + if contact.sub in ('both', 'to') and same_groups: continue exchange_items_list[jid] = [] exchange_items_list[jid].append(name) From 2325791eeb34209e877b4eeda2bc756aa702f97d Mon Sep 17 00:00:00 2001 From: Alexander Cherniuk Date: Wed, 25 Nov 2009 13:39:05 +0200 Subject: [PATCH 62/73] Minor refactoring --- src/message_textview.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/message_textview.py b/src/message_textview.py index 40ba81067..236ad1cb0 100644 --- a/src/message_textview.py +++ b/src/message_textview.py @@ -21,15 +21,20 @@ ## along with Gajim. If not, see . ## +import gc + import gtk import gobject import pango + import gtkgui_helpers from common import gajim class MessageTextView(gtk.TextView): - '''Class for the message textview (where user writes new messages) - for chat/groupchat windows''' + """ + Class for the message textview (where user writes new messages) for + chat/groupchat windows + """ __gsignals__ = dict( mykeypress = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, # return value @@ -272,13 +277,13 @@ class MessageTextView(gtk.TextView): else: return None - def destroy(self): - import gc - gobject.idle_add(lambda:gc.collect()) + gobject.idle_add(gc.collect) def clear(self, widget = None): - '''clear text in the textview''' + """ + Clear text in the textview + """ buffer_ = self.get_buffer() start, end = buffer_.get_bounds() buffer_.delete(start, end) From a6d2c4f2862e8fb80bdf495808a813fc21c1ff3c Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 25 Nov 2009 13:29:57 +0100 Subject: [PATCH 63/73] fix typo in a variable name --- src/gui_menu_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui_menu_builder.py b/src/gui_menu_builder.py index 0c975d5b6..80575d1ad 100644 --- a/src/gui_menu_builder.py +++ b/src/gui_menu_builder.py @@ -320,7 +320,7 @@ control=None): for item in (send_custom_status_menuitem, send_single_message_menuitem, invite_menuitem, block_menuitem, unblock_menuitem, ignore_menuitem, unignore_menuitem, set_custom_avatar_menuitem, subscription_menuitem, - manage_contact_menuitem, convert_to_gc_menuitems): + manage_contact_menuitem, convert_to_gc_menuitem): item.set_no_show_all(True) item.hide() From b6c4aaba6feffee3549cf0e52bc80027195fcd61 Mon Sep 17 00:00:00 2001 From: Alexander Cherniuk Date: Wed, 25 Nov 2009 15:01:40 +0200 Subject: [PATCH 64/73] Refactored doc-strings --- src/chat_control.py | 305 +++++++++++++++++++++++++++++--------------- 1 file changed, 199 insertions(+), 106 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 9c491b9e1..554ac1b02 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -90,8 +90,9 @@ if gajim.config.get('use_speller') and HAS_GTK_SPELL: ################################################################################ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): - '''A base class containing a banner, ConversationTextview, MessageTextView - ''' + """ + A base class containing a banner, ConversationTextview, MessageTextView + """ def make_href(self, match): url_color = gajim.config.get('urlmsgcolor') @@ -99,7 +100,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): url_color, match.group()) def get_font_attrs(self): - ''' get pango font attributes for banner from theme settings ''' + """ + Get pango font attributes for banner from theme settings + """ theme = gajim.config.get('roster_theme') bannerfont = gajim.config.get_per('themes', theme, 'bannerfont') bannerfontattrs = gajim.config.get_per('themes', theme, 'bannerfontattrs') @@ -133,33 +136,45 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): type_])) def draw_banner(self): - '''Draw the fat line at the top of the window that - houses the icon, jid, ... - ''' + """ + Draw the fat line at the top of the window that houses the icon, jid, etc + + Derived types MAY implement this. + """ self.draw_banner_text() self._update_banner_state_image() - # Derived types MAY implement this def draw_banner_text(self): - pass # Derived types SHOULD implement this + """ + Derived types SHOULD implement this + """ + pass def update_ui(self): + """ + Derived types SHOULD implement this + """ self.draw_banner() - # Derived types SHOULD implement this def repaint_themed_widgets(self): + """ + Derived types MAY implement this + """ self._paint_banner() self.draw_banner() - # Derived classes MAY implement this def _update_banner_state_image(self): - pass # Derived types MAY implement this + """ + Derived types MAY implement this + """ + pass def handle_message_textview_mykey_press(self, widget, event_keyval, - event_keymod): - # Derived should implement this rather than connecting to the event - # itself. - + event_keymod): + """ + Derives types SHOULD implement this, rather than connection to the even + itself + """ event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) event.keyval = event_keyval event.state = event_keymod @@ -212,7 +227,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): helpers.launch_browser_mailer('url', url) def __init__(self, type_id, parent_win, widget_name, contact, acct, - resource = None): + resource = None): if resource is None: # We very likely got a contact with a random resource. # This is bad, we need the highest for caps etc. @@ -386,7 +401,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): dialogs.AspellDictError(lang) def on_banner_label_populate_popup(self, label, menu): - '''We override the default context menu and add our own menutiems''' + """ + Override the default context menu and add our own menutiems + """ item = gtk.SeparatorMenuItem() menu.prepend(item) @@ -400,8 +417,10 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): menu.show_all() def on_msg_textview_populate_popup(self, textview, menu): - '''we override the default context menu and we prepend an option to switch - languages''' + """ + Override the default context menu and we prepend an option to switch + languages + """ def _on_select_dictionary(widget, lang): per_type = 'contacts' if self.type_id == message_control.TYPE_GC: @@ -445,12 +464,16 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): # moved from ChatControl def _on_banner_eventbox_button_press_event(self, widget, event): - '''If right-clicked, show popup''' + """ + If right-clicked, show popup + """ if event.button == 3: # right click self.parent_win.popup_menu(event) def _on_send_button_clicked(self, widget): - '''When send button is pressed: send the current message''' + """ + When send button is pressed: send the current message + """ if gajim.connections[self.account].connected < 2: # we are not connected dialogs.ErrorDialog(_('A connection is not available'), _('Your message can not be sent until you are connected.')) @@ -465,7 +488,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): self.send_message(message, xhtml=xhtml) def _paint_banner(self): - '''Repaint banner with theme color''' + """ + Repaint banner with theme color + """ theme = gajim.config.get('roster_theme') bgcolor = gajim.config.get_per('themes', theme, 'bannerbgcolor') textcolor = gajim.config.get_per('themes', theme, 'bannertextcolor') @@ -512,9 +537,11 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): self.handlers[id_] = widget def _on_style_set_event(self, widget, style, *opts): - '''set style of widget from style class *.Frame.Eventbox + """ + Set style of widget from style class *.Frame.Eventbox opts[0] == True -> set fg color - opts[1] == True -> set bg color''' + opts[1] == True -> set bg color + """ banner_eventbox = self.xml.get_widget('banner_eventbox') self.disconnect_style_event(widget) if opts[1]: @@ -589,11 +616,11 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): return False def _on_message_textview_mykeypress_event(self, widget, event_keyval, - event_keymod): - '''When a key is pressed: - if enter is pressed without the shift key, message (if not empty) is sent - and printed in the conversation''' - + event_keymod): + """ + When a key is pressed: if enter is pressed without the shift key, message + (if not empty) is sent and printed in the conversation + """ # NOTE: handles mykeypress which is custom signal connected to this # CB in new_tab(). for this singal see message_textview.py message_textview = widget @@ -652,8 +679,11 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): event_keymod) def _on_drag_data_received(self, widget, context, x, y, selection, - target_type, timestamp): - pass # Derived classes SHOULD implement this method + target_type, timestamp): + """ + Derived types SHOULD implement this + """ + pass def _on_drag_leave(self, widget, context, time): # FIXME: DND on non editable TextView, find a better way @@ -668,10 +698,11 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): self.conv_textview.tv.set_editable(True) def send_message(self, message, keyID='', type_='chat', chatstate=None, - msg_id=None, composing_xep=None, resource=None, - xhtml=None, callback=None, callback_args=[], process_commands=True): - '''Send the given message to the active tab. Doesn't return None if error - ''' + msg_id=None, composing_xep=None, resource=None, xhtml=None, + callback=None, callback_args=[], process_commands=True): + """ + Send the given message to the active tab. Doesn't return None if error + """ if not message or message == '\n': return None @@ -711,10 +742,13 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): self.orig_msg = None def print_conversation_line(self, text, kind, name, tim, - other_tags_for_name=[], other_tags_for_time=[], other_tags_for_text=[], - count_as_new=True, subject=None, old_kind=None, xhtml=None, simple=False, - xep0184_id=None, graphics=True): - '''prints 'chat' type messages''' + other_tags_for_name=[], other_tags_for_time=[], + other_tags_for_text=[], count_as_new=True, subject=None, + old_kind=None, xhtml=None, simple=False, xep0184_id=None, + graphics=True): + """ + Print 'chat' type messages + """ jid = self.contact.jid full_jid = self.get_full_jid() textview = self.conv_textview @@ -789,8 +823,10 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): self.parent_win.show_title(False, self) # Disabled Urgent hint def toggle_emoticons(self): - '''hide show emoticons_button and make sure emoticons_menu is always there - when needed''' + """ + Hide show emoticons_button and make sure emoticons_menu is always there + when needed + """ emoticons_button = self.xml.get_widget('emoticons_button') if gajim.config.get('emoticons_theme'): emoticons_button.show() @@ -808,12 +844,16 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): self.msg_textview.grab_focus() def on_emoticons_button_clicked(self, widget): - '''popup emoticons menu''' + """ + Popup emoticons menu + """ gajim.interface.emoticon_menuitem_clicked = self.append_emoticon gajim.interface.popup_emoticons_under_button(widget, self.parent_win) def on_formattings_button_clicked(self, widget): - '''popup formattings menu''' + """ + Popup formattings menu + """ menu = gtk.Menu() menuitems = ((_('Bold'), 'bold'), @@ -875,7 +915,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): def on_actions_button_clicked(self, widget): - '''popup action menu''' + """ + Popup action menu + """ menu = self.prepare_context_menu(hide_buttonbar_items=True) menu.show_all() gtkgui_helpers.popup_emoticons_under_button(menu, widget, @@ -895,7 +937,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): buffer_.delete(start, end) def _on_history_menuitem_activate(self, widget = None, jid = None): - '''When history menuitem is pressed: call history window''' + """ + When history menuitem is pressed: call history window + """ if not jid: jid = self.contact.jid @@ -907,7 +951,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): history_window.HistoryWindow(jid, self.account) def _on_send_file(self, gc_contact=None): - '''gc_contact can be set when we are in a groupchat control''' + """ + gc_contact can be set when we are in a groupchat control + """ def _on_ok(c): gajim.interface.instances['file_transfers'].show_file_send_request( self.account, c) @@ -935,7 +981,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): _on_ok(self.contact) def on_minimize_menuitem_toggled(self, widget): - '''When a grouchat is minimized, unparent the tab, put it in roster etc''' + """ + When a grouchat is minimized, unparent the tab, put it in roster etc + """ old_value = False minimized_gc = gajim.config.get_per('accounts', self.account, 'minimized_gc').split() @@ -965,7 +1013,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): def bring_scroll_to_end(self, textview, diff_y = 0): - ''' scrolls to the end of textview if end is not visible ''' + """ + Scroll to the end of textview if end is not visible + """ if self.scroll_to_end_id: # a scroll is already planned return @@ -986,10 +1036,12 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): return False def size_request(self, msg_textview , requisition): - ''' When message_textview changes its size. If the new height - will enlarge the window, enable the scrollbar automatic policy - Also enable scrollbar automatic policy for horizontal scrollbar - if message we have in message_textview is too big''' + """ + When message_textview changes its size: if the new height will enlarge + the window, enable the scrollbar automatic policy. Also enable scrollbar + automatic policy for horizontal scrollbar if message we have in + message_textview is too big + """ if msg_textview.window is None: return @@ -1082,8 +1134,10 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): self.redraw_after_event_removed(jid) def redraw_after_event_removed(self, jid): - ''' We just removed a 'printed_*' event, redraw contact in roster or - gc_roster and titles in roster and msg_win ''' + """ + We just removed a 'printed_*' event, redraw contact in roster or + gc_roster and titles in roster and msg_win + """ self.parent_win.redraw_tab(self) self.parent_win.show_title() # TODO : get the contact and check notify.get_show_in_roster() @@ -1142,7 +1196,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): return color def widget_set_visible(self, widget, state): - '''Show or hide a widget. state is bool''' + """ + Show or hide a widget + """ # make the last message visible, when changing to "full view" if not state: gobject.idle_add(self.conv_textview.scroll_to_end_iter) @@ -1154,7 +1210,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): widget.show_all() def chat_buttons_set_visible(self, state): - '''Toggle chat buttons. state is bool''' + """ + Toggle chat buttons + """ MessageControl.chat_buttons_set_visible(self, state) self.widget_set_visible(self.xml.get_widget('actions_hbox'), state) @@ -1173,7 +1231,9 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools): ################################################################################ class ChatControl(ChatControlBase): - '''A control for standard 1-1 chat''' + """ + A control for standard 1-1 chat + """ ( JINGLE_STATE_NOT_AVAILABLE, JINGLE_STATE_AVAILABLE, @@ -1535,10 +1595,10 @@ class ChatControl(ChatControlBase): self._set_jingle_state('video', state, sid=sid, reason=reason) def on_avatar_eventbox_enter_notify_event(self, widget, event): - ''' - we enter the eventbox area so we under conditions add a timeout - to show a bigger avatar after 0.5 sec - ''' + """ + Enter the eventbox area so we under conditions add a timeout to show a + bigger avatar after 0.5 sec + """ jid = self.contact.jid is_fake = False if self.type_id == message_control.TYPE_PM: @@ -1561,13 +1621,17 @@ class ChatControl(ChatControlBase): self.show_bigger_avatar, widget) def on_avatar_eventbox_leave_notify_event(self, widget, event): - '''we left the eventbox area that holds the avatar img''' + """ + Left the eventbox area that holds the avatar img + """ # did we add a timeout? if yes remove it if self.show_bigger_avatar_timeout_id is not None: gobject.source_remove(self.show_bigger_avatar_timeout_id) def on_avatar_eventbox_button_press_event(self, widget, event): - '''If right-clicked, show popup''' + """ + If right-clicked, show popup + """ if event.button == 3: # right click menu = gtk.Menu() menuitem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS) @@ -1585,7 +1649,9 @@ class ChatControl(ChatControlBase): return True def _on_window_motion_notify(self, widget, event): - '''it gets called no matter if it is the active window or not''' + """ + It gets called no matter if it is the active window or not + """ if self.parent_win.get_active_jid() == self.contact.jid: # if window is the active one, change vars assisting chatstate self.mouse_over_in_last_5_secs = True @@ -1638,9 +1704,10 @@ class ChatControl(ChatControlBase): banner_status_img.set_from_pixbuf(scaled_pix) def draw_banner_text(self): - '''Draw the text in the fat line at the top of the window that - houses the name, jid. - ''' + """ + Draw the text in the fat line at the top of the window that houses the + name, jid + """ contact = self.contact jid = contact.jid @@ -1823,8 +1890,11 @@ class ChatControl(ChatControlBase): self._show_lock_image(self.gpg_is_active, 'GPG', self.gpg_is_active, loggable, True) - def _show_lock_image(self, visible, enc_type = '', enc_enabled = False, chat_logged = False, authenticated = False): - '''Set lock icon visibility and create tooltip''' + def _show_lock_image(self, visible, enc_type = '', enc_enabled = False, + chat_logged = False, authenticated = False): + """ + Set lock icon visibility and create tooltip + """ #encryption %s active status_string = enc_enabled and _('is') or _('is NOT') #chat session %s be logged @@ -1859,7 +1929,9 @@ class ChatControl(ChatControlBase): def send_message(self, message, keyID='', chatstate=None, xhtml=None, process_commands=True): - '''Send a message to contact''' + """ + Send a message to contact + """ if message in ('', None, '\n'): return None @@ -1923,10 +1995,11 @@ class ChatControl(ChatControlBase): process_commands=process_commands) def check_for_possible_paused_chatstate(self, arg): - ''' did we move mouse of that window or write something in message - textview in the last 5 seconds? - if yes we go active for mouse, composing for kbd - if no we go paused if we were previously composing ''' + """ + Did we move mouse of that window or write something in message textview + in the last 5 seconds? If yes - we go active for mouse, composing for + kbd. If not - we go paused if we were previously composing + """ contact = self.contact jid = contact.jid current_state = contact.our_chatstate @@ -1950,10 +2023,10 @@ class ChatControl(ChatControlBase): return True # loop forever def check_for_possible_inactive_chatstate(self, arg): - ''' did we move mouse over that window or wrote something in message - textview in the last 30 seconds? - if yes we go active - if no we go inactive ''' + """ + Did we move mouse over that window or wrote something in message textview + in the last 30 seconds? if yes - we go active. If no - we go inactive + """ contact = self.contact current_state = contact.our_chatstate @@ -1983,7 +2056,9 @@ class ChatControl(ChatControlBase): ChatControlBase.print_conversation_line(self, msg, 'status', '', None) def print_esession_details(self): - '''print esession settings to textview''' + """ + Print esession settings to textview + """ e2e_is_active = bool(self.session) and self.session.enable_encryption if e2e_is_active: msg = _('This session is encrypted') @@ -2005,16 +2080,19 @@ class ChatControl(ChatControlBase): self.session.is_loggable(), self.session and self.session.verified_identity) def print_conversation(self, text, frm='', tim=None, encrypted=False, - subject=None, xhtml=None, simple=False, xep0184_id=None): - '''Print a line in the conversation: - if frm is set to status: it's a status message - if frm is set to error: it's an error message - The difference between status and error is mainly that with error, msg - count as a new message (in systray and in control). - if frm is set to info: it's a information message - if frm is set to print_queue: it is incomming from queue - if frm is set to another value: it's an outgoing message - if frm is not set: it's an incomming message''' + subject=None, xhtml=None, simple=False, xep0184_id=None): + """ + Print a line in the conversation + + If frm is set to status: it's a status message. + if frm is set to error: it's an error message. The difference between + status and error is mainly that with error, msg count as a new message + (in systray and in control). + If frm is set to info: it's a information message. + If frm is set to print_queue: it is incomming from queue. + If frm is set to another value: it's an outgoing message. + If frm is not set: it's an incomming message. + """ contact = self.contact if frm == 'status': @@ -2152,12 +2230,12 @@ class ChatControl(ChatControlBase): return tab_img def prepare_context_menu(self, hide_buttonbar_items=False): - '''sets compact view menuitem active state - sets active and sensitivity state for toggle_gpg_menuitem - sets sensitivity for history_menuitem (False for tranasports) - and file_transfer_menuitem - and hide()/show() for add_to_roster_menuitem - ''' + """ + Set compact view menuitem active state sets active and sensitivity state + for toggle_gpg_menuitem sets sensitivity for history_menuitem (False for + tranasports) and file_transfer_menuitem and hide()/show() for + add_to_roster_menuitem + """ menu = gui_menu_builder.get_contact_menu(self.contact, self.account, use_multiple_contacts=False, show_start_chat=False, show_encryption=True, control=self, @@ -2165,9 +2243,11 @@ class ChatControl(ChatControlBase): return menu def send_chatstate(self, state, contact = None): - ''' sends OUR chatstate as STANDLONE chat state message (eg. no body) + """ + Send OUR chatstate as STANDLONE chat state message (eg. no body) to contact only if new chatstate is different from the previous one - if jid is not specified, send to active tab''' + if jid is not specified, send to active tab + """ # JEP 85 does not allow resending the same chatstate # this function checks for that and just returns so it's safe to call it # with same state. @@ -2313,7 +2393,9 @@ class ChatControl(ChatControlBase): on_yes(self) def handle_incoming_chatstate(self): - ''' handle incoming chatstate that jid SENT TO us ''' + """ + Handle incoming chatstate that jid SENT TO us + """ self.draw_banner_text() # update chatstate in tab for this chat self.parent_win.redraw_tab(self, self.contact.chatstate) @@ -2495,8 +2577,9 @@ class ChatControl(ChatControlBase): self.conv_textview.print_empty_line() def read_queue(self): - '''read queue and print messages containted in it''' - + """ + Read queue and print messages containted in it + """ jid = self.contact.jid jid_with_resource = jid if self.resource: @@ -2551,8 +2634,10 @@ class ChatControl(ChatControlBase): control.remove_contact(nick) def show_bigger_avatar(self, small_avatar): - '''resizes the avatar, if needed, so it has at max half the screen size - and shows it''' + """ + Resize the avatar, if needed, so it has at max half the screen size and + shows it + """ if not small_avatar.window: # Tab has been closed since we hovered the avatar return @@ -2619,14 +2704,18 @@ class ChatControl(ChatControlBase): window.show_all() def _on_window_avatar_leave_notify_event(self, widget, event): - '''we just left the popup window that holds avatar''' + """ + Just left the popup window that holds avatar + """ self.bigger_avatar_window.destroy() self.bigger_avatar_window = None # Re-show the small avatar self.show_avatar() def _on_window_motion_notify_event(self, widget, event): - '''we just moved the mouse so show the cursor''' + """ + Just moved the mouse so show the cursor + """ cursor = gtk.gdk.Cursor(gtk.gdk.LEFT_PTR) self.bigger_avatar_window.window.set_cursor(cursor) @@ -2643,7 +2732,9 @@ class ChatControl(ChatControlBase): self._toggle_gpg() def _on_convert_to_gc_menuitem_activate(self, widget): - '''user want to invite some friends to chat''' + """ + User wants to invite some friends to chat + """ dialogs.TransformChatToMUC(self.account, [self.contact.jid]) def _on_toggle_e2e_menuitem_activate(self, widget): @@ -2684,7 +2775,9 @@ class ChatControl(ChatControlBase): self.draw_banner() def update_status_display(self, name, uf_show, status): - '''print the contact's status and update the status/GPG image''' + """ + Print the contact's status and update the status/GPG image + """ self.update_ui() self.parent_win.redraw_tab(self) From 8c82c35654381e042edc9886dbaf7308ab90071c Mon Sep 17 00:00:00 2001 From: Alexander Cherniuk Date: Wed, 25 Nov 2009 15:24:48 +0200 Subject: [PATCH 65/73] More doc-string refactoring --- src/groupchat_control.py | 212 ++++++++++++++++++++++++++------------- 1 file changed, 142 insertions(+), 70 deletions(-) diff --git a/src/groupchat_control.py b/src/groupchat_control.py index 4653d0d76..6db3398f5 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -64,7 +64,9 @@ C_AVATAR, # avatar of the contact ) = range(5) def set_renderer_color(treeview, renderer, set_background=True): - '''set style for group row, using PRELIGHT system color''' + """ + Set style for group row, using PRELIGHT system color + """ if set_background: bgcolor = treeview.style.bg[gtk.STATE_PRELIGHT] renderer.set_property('cell-background-gdk', bgcolor) @@ -141,7 +143,9 @@ class PrivateChatControl(ChatControl): self.TYPE_ID = 'pm' def send_message(self, message, xhtml=None, process_commands=True): - '''call this function to send our message''' + """ + Call this method to send the message + """ if not message: return @@ -383,7 +387,9 @@ class GroupchatControl(ChatControlBase): self.widget.show_all() def tree_compare_iters(self, model, iter1, iter2): - '''Compare two iters to sort them''' + """ + Compare two iters to sort them + """ type1 = model[iter1][C_TYPE] type2 = model[iter2][C_TYPE] if not type1 or not type2: @@ -422,8 +428,10 @@ class GroupchatControl(ChatControlBase): return locale.strcoll(name1.lower(), name2.lower()) def on_msg_textview_populate_popup(self, textview, menu): - '''we override the default context menu and we prepend Clear - and the ability to insert a nick''' + """ + Override the default context menu and we prepend Clear + and the ability to insert a nick + """ ChatControlBase.on_msg_textview_populate_popup(self, textview, menu) item = gtk.SeparatorMenuItem() menu.prepend(item) @@ -452,7 +460,9 @@ class GroupchatControl(ChatControlBase): gobject.idle_add(reset_flag) def on_treeview_size_allocate(self, widget, allocation): - '''The MUC treeview has resized. Move the hpaned in all tabs to match''' + """ + The MUC treeview has resized. Move the hpaned in all tabs to match + """ if self.resize_from_another_muc: # Don't send the event to other MUC return @@ -467,7 +477,9 @@ class GroupchatControl(ChatControlBase): ctrl.resize_occupant_treeview(hpaned_position) def iter_contact_rows(self): - '''iterate over all contact rows in the tree model''' + """ + Iterate over all contact rows in the tree model + """ model = self.list_treeview.get_model() role_iter = model.get_iter_root() while role_iter: @@ -478,7 +490,9 @@ class GroupchatControl(ChatControlBase): role_iter = model.iter_next(role_iter) def on_list_treeview_style_set(self, treeview, style): - '''When style (theme) changes, redraw all contacts''' + """ + When style (theme) changes, redraw all contacts + """ # Get the room_jid from treeview for contact in self.iter_contact_rows(): nick = contact[C_NICK].decode('utf-8') @@ -500,10 +514,11 @@ class GroupchatControl(ChatControlBase): self.draw_contact(nick, selected=True, focus=True) def get_tab_label(self, chatstate): - '''Markup the label if necessary. Returns a tuple such as: - (new_label_str, color) - either of which can be None - if chatstate is given that means we have HE SENT US a chatstate''' + """ + Markup the label if necessary. Returns a tuple such as: (new_label_str, + color) either of which can be None if chatstate is given that means we + have HE SENT US a chatstate + """ has_focus = self.parent_win.window.get_property('has-toplevel-focus') current_tab = self.parent_win.get_active_control() == self @@ -588,10 +603,10 @@ class GroupchatControl(ChatControlBase): banner_status_img.set_from_pixbuf(scaled_pix) def get_continued_conversation_name(self): - '''Get the name of a continued conversation. - Will return Continued Conversation if there isn't any other - contact in the room - ''' + """ + Get the name of a continued conversation. Will return Continued + Conversation if there isn't any other contact in the room + """ nicks = [] for nick in gajim.contacts.get_nick_list(self.account, self.room_jid): @@ -605,9 +620,10 @@ class GroupchatControl(ChatControlBase): return title def draw_banner_text(self): - '''Draw the text in the fat line at the top of the window that - houses the room jid, subject. - ''' + """ + Draw the text in the fat line at the top of the window that houses the + room jid, subject + """ self.name_label.set_ellipsize(pango.ELLIPSIZE_END) self.banner_status_label.set_ellipsize(pango.ELLIPSIZE_END) font_attrs, font_attrs_small = self.get_font_attrs() @@ -641,7 +657,9 @@ class GroupchatControl(ChatControlBase): self.banner_status_label.set_markup(subject_text) def prepare_context_menu(self, hide_buttonbar_items=False): - '''sets sensitivity state for configure_room''' + """ + Set sensitivity state for configure_room + """ xml = gtkgui_helpers.get_glade('gc_control_popup_menu.glade') menu = xml.get_widget('gc_control_popup_menu') @@ -745,7 +763,7 @@ class GroupchatControl(ChatControlBase): return menu def destroy_menu(self, menu, change_nick_menuitem, change_subject_menuitem, - bookmark_room_menuitem, history_menuitem): + bookmark_room_menuitem, history_menuitem): # destroy accelerators ag = gtk.accel_groups_from_object(self.parent_win.window)[0] change_nick_menuitem.remove_accelerator(ag, gtk.keysyms.n, @@ -760,7 +778,7 @@ class GroupchatControl(ChatControlBase): menu.destroy() def on_message(self, nick, msg, tim, has_timestamp=False, xhtml=None, - status_code=[]): + status_code=[]): if '100' in status_code: # Room is not anonymous self.is_anonymous = False @@ -776,8 +794,8 @@ class GroupchatControl(ChatControlBase): else: self.print_conversation(msg, nick, tim, xhtml) - def on_private_message(self, nick, msg, tim, xhtml, session, - msg_id=None, encrypted=False): + def on_private_message(self, nick, msg, tim, xhtml, session, msg_id=None, + encrypted=False): # Do we have a queue? fjid = self.room_jid + '/' + nick no_queue = len(gajim.events.get_events(self.account, fjid)) == 0 @@ -835,8 +853,7 @@ class GroupchatControl(ChatControlBase): fin = True return None - def print_old_conversation(self, text, contact='', tim=None, - xhtml = None): + def print_old_conversation(self, text, contact='', tim=None, xhtml = None): if isinstance(text, str): text = unicode(text, 'utf-8') if contact: @@ -855,11 +872,14 @@ class GroupchatControl(ChatControlBase): small_attr + ['restored_message'], count_as_new=False, xhtml=xhtml) def print_conversation(self, text, contact='', tim=None, xhtml=None, - graphics=True): - '''Print a line in the conversation: - if contact is set: it's a message from someone or an info message (contact - = 'info' in such a case) - if contact is not set: it's a message from the server or help''' + graphics=True): + """ + Print a line in the conversation + + If contact is set: it's a message from someone or an info message + (contact = 'info' in such a case). + If contact is not set: it's a message from the server or help. + """ if isinstance(text, str): text = unicode(text, 'utf-8') other_tags_for_name = [] @@ -938,8 +958,10 @@ class GroupchatControl(ChatControlBase): return nb def highlighting_for_message(self, text, tim): - '''Returns a 2-Tuple. The first says whether or not to highlight the - text, the second, what sound to play.''' + """ + Returns a 2-Tuple. The first says whether or not to highlight the text, + the second, what sound to play + """ highlight, sound = (None, None) # Are any of the defined highlighting words in the text? @@ -961,10 +983,11 @@ class GroupchatControl(ChatControlBase): return (highlight, sound) def check_and_possibly_add_focus_out_line(self): - '''checks and possibly adds focus out line for room_jid if it needs it - and does not already have it as last event. If it goes to add this line - it removes previous line first''' - + """ + Check and possibly add focus out line for room_jid if it needs it and + does not already have it as last event. If it goes to add this line + - remove previous line first + """ win = gajim.interface.msg_win_mgr.get_window(self.room_jid, self.account) if win and self.room_jid == win.get_active_jid() and\ win.window.get_property('has-toplevel-focus') and\ @@ -976,9 +999,10 @@ class GroupchatControl(ChatControlBase): self.conv_textview.show_focus_out_line() def needs_visual_notification(self, text): - '''checks text to see whether any of the words in (muc_highlight_words - and nick) appear.''' - + """ + Check text to see whether any of the words in (muc_highlight_words and + nick) appear + """ special_words = gajim.config.get('muc_highlight_words').split(';') special_words.append(self.nick) # Strip empties: ''.split(';') == [''] and would highlight everything. @@ -1072,9 +1096,11 @@ class GroupchatControl(ChatControlBase): self.list_treeview.columns_autosize() def on_send_pm(self, widget=None, model=None, iter_=None, nick=None, - msg=None): - '''opens a chat window and if msg is not None sends private message to a - contact in a room''' + msg=None): + """ + Open a chat window and if msg is not None - send private message to a + contact in a room + """ if nick is None: nick = model[iter_][C_NICK].decode('utf-8') @@ -1083,7 +1109,9 @@ class GroupchatControl(ChatControlBase): ctrl.send_message(msg) def on_send_file(self, widget, gc_contact): - '''sends a file to a contact in the room''' + """ + Send a file to a contact in the room + """ self._on_send_file(gc_contact) def draw_contact(self, nick, selected=False, focus=False): @@ -1168,8 +1196,10 @@ class GroupchatControl(ChatControlBase): self.draw_role(role) def chg_contact_status(self, nick, show, status, role, affiliation, jid, - reason, actor, statusCode, new_nick, avatar_sha, tim=None): - '''When an occupant changes his or her status''' + reason, actor, statusCode, new_nick, avatar_sha, tim=None): + """ + When an occupant changes his or her status + """ if show == 'invisible': return @@ -1448,7 +1478,7 @@ class GroupchatControl(ChatControlBase): self.print_conversation(st, tim=tim, graphics=False) def add_contact_to_roster(self, nick, show, role, affiliation, status, - jid=''): + jid=''): model = self.list_treeview.get_model() role_name = helpers.get_uf_role(role, plural=True) @@ -1514,7 +1544,9 @@ class GroupchatControl(ChatControlBase): return None def remove_contact(self, nick): - '''Remove a user from the contacts_list''' + """ + Remove a user from the contacts_list + """ model = self.list_treeview.get_model() iter_ = self.get_contact_iter(nick) if not iter_: @@ -1529,7 +1561,9 @@ class GroupchatControl(ChatControlBase): model.remove(parent_iter) def send_message(self, message, xhtml=None, process_commands=True): - '''call this function to send our message''' + """ + Call this function to send our message + """ if not message: return @@ -1752,7 +1786,9 @@ class GroupchatControl(ChatControlBase): _('You may also enter an alternate venue:'), ok_handler=on_ok) def _on_bookmark_room_menuitem_activate(self, widget): - '''bookmark the room, without autojoin and not minimized''' + """ + Bookmark the room, without autojoin and not minimized + """ password = gajim.gc_passwords.get(self.room_jid, '') gajim.interface.add_gc_bookmark(self.account, self.name, self.room_jid, \ '0', '0', password, self.nick) @@ -1775,7 +1811,7 @@ class GroupchatControl(ChatControlBase): gajim.connections[self.account].send_invite(self.room_jid, contact_jid) def handle_message_textview_mykey_press(self, widget, event_keyval, - event_keymod): + event_keymod): # NOTE: handles mykeypress which is custom signal connected to this # CB in new_room(). for this singal see message_textview.py @@ -1904,19 +1940,25 @@ class GroupchatControl(ChatControlBase): return True def on_list_treeview_row_expanded(self, widget, iter_, path): - '''When a row is expanded: change the icon of the arrow''' + """ + When a row is expanded: change the icon of the arrow + """ model = widget.get_model() image = gajim.interface.jabber_state_images['16']['opened'] model[iter_][C_IMG] = image def on_list_treeview_row_collapsed(self, widget, iter_, path): - '''When a row is collapsed: change the icon of the arrow''' + """ + When a row is collapsed: change the icon of the arrow + """ model = widget.get_model() image = gajim.interface.jabber_state_images['16']['closed'] model[iter_][C_IMG] = image def kick(self, widget, nick): - '''kick a user''' + """ + Kick a user + """ def on_ok(reason): gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'none', reason) @@ -1926,7 +1968,9 @@ class GroupchatControl(ChatControlBase): _('You may specify a reason below:'), ok_handler=on_ok) def mk_menu(self, event, iter_): - '''Make contact's popup menu''' + """ + Make contact's popup menu + """ model = self.list_treeview.get_model() nick = model[iter_][C_NICK].decode('utf-8') c = gajim.contacts.get_gc_contact(self.account, self.room_jid, nick) @@ -2070,8 +2114,10 @@ class GroupchatControl(ChatControlBase): return ctrl def on_row_activated(self, widget, path): - '''When an iter is activated (dubblick or single click if gnome is set - this way''' + """ + When an iter is activated (dubblick or single click if gnome is set this + way + """ model = widget.get_model() if len(path) == 1: # It's a group if (widget.row_expanded(path)): @@ -2083,12 +2129,16 @@ class GroupchatControl(ChatControlBase): self._start_private_message(nick) def on_list_treeview_row_activated(self, widget, path, col=0): - '''When an iter is double clicked: open the chat window''' + """ + When an iter is double clicked: open the chat window + """ if not gajim.single_click: self.on_row_activated(widget, path) def on_list_treeview_button_press_event(self, widget, event): - '''popup user's group's or agent menu''' + """ + Popup user's group's or agent menu + """ # hide tooltip, no matter the button is pressed self.tooltip.hide_tooltip() try: @@ -2199,27 +2249,37 @@ class GroupchatControl(ChatControlBase): self.tooltip.hide_tooltip() def grant_voice(self, widget, nick): - '''grant voice privilege to a user''' + """ + Grant voice privilege to a user + """ gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'participant') def revoke_voice(self, widget, nick): - '''revoke voice privilege to a user''' + """ + Revoke voice privilege to a user + """ gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'visitor') def grant_moderator(self, widget, nick): - '''grant moderator privilege to a user''' + """ + Grant moderator privilege to a user + """ gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'moderator') def revoke_moderator(self, widget, nick): - '''revoke moderator privilege to a user''' + """ + Revoke moderator privilege to a user + """ gajim.connections[self.account].gc_set_role(self.room_jid, nick, 'participant') def ban(self, widget, jid): - '''ban a user''' + """ + Ban a user + """ def on_ok(reason): gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, 'outcast', reason) @@ -2231,32 +2291,44 @@ class GroupchatControl(ChatControlBase): _('You may specify a reason below:'), ok_handler=on_ok) def grant_membership(self, widget, jid): - '''grant membership privilege to a user''' + """ + Grant membership privilege to a user + """ gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, 'member') def revoke_membership(self, widget, jid): - '''revoke membership privilege to a user''' + """ + Revoke membership privilege to a user + """ gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, 'none') def grant_admin(self, widget, jid): - '''grant administrative privilege to a user''' + """ + Grant administrative privilege to a user + """ gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, 'admin') def revoke_admin(self, widget, jid): - '''revoke administrative privilege to a user''' + """ + Revoke administrative privilege to a user + """ gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, 'member') def grant_owner(self, widget, jid): - '''grant owner privilege to a user''' + """ + Grant owner privilege to a user + """ gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, 'owner') def revoke_owner(self, widget, jid): - '''revoke owner privilege to a user''' + """ + Revoke owner privilege to a user + """ gajim.connections[self.account].gc_set_affiliation(self.room_jid, jid, 'admin') From 0ee0ade03a46dffa368e2551d510d59faba09f55 Mon Sep 17 00:00:00 2001 From: Alexander Cherniuk Date: Wed, 25 Nov 2009 15:52:32 +0200 Subject: [PATCH 66/73] More doc-string refactoring --- src/adhoc_commands.py | 105 +++++++++++++++++++++++++++++------------- 1 file changed, 73 insertions(+), 32 deletions(-) diff --git a/src/adhoc_commands.py b/src/adhoc_commands.py index 9b96f0fee..01d34eee6 100644 --- a/src/adhoc_commands.py +++ b/src/adhoc_commands.py @@ -35,17 +35,22 @@ import dialogs import dataforms_widget class CommandWindow: - '''Class for a window for single ad-hoc commands session. Note, that - there might be more than one for one account/jid pair in one moment. + """ + Class for a window for single ad-hoc commands session - TODO: maybe put this window into MessageWindow? consider this when - TODO: it will be possible to manage more than one window of one - TODO: account/jid pair in MessageWindowMgr. + Note, that there might be more than one for one account/jid pair in one + moment. - TODO: gtk 2.10 has a special wizard-widget, consider using it...''' + TODO: Maybe put this window into MessageWindow? consider this when it will + be possible to manage more than one window of one. + TODO: Account/jid pair in MessageWindowMgr. + TODO: GTK 2.10 has a special wizard-widget, consider using it... + """ def __init__(self, account, jid, commandnode=None): - '''Create new window.''' + """ + Create new window + """ # an account object self.account = gajim.connections[account] @@ -89,16 +94,29 @@ class CommandWindow: self.xml.signal_autoconnect(self) self.window.show_all() -# these functions are set up by appropriate stageX methods - def stage_finish(self, *anything): pass - def stage_back_button_clicked(self, *anything): assert False - def stage_forward_button_clicked(self, *anything): assert False - def stage_execute_button_clicked(self, *anything): assert False - def stage_close_button_clicked(self, *anything): assert False - def stage_adhoc_commands_window_delete_event(self, *anything): assert False - def do_nothing(self, *anything): return False + # These functions are set up by appropriate stageX methods. + def stage_finish(self, *anything): + pass -# widget callbacks + def stage_back_button_clicked(self, *anything): + assert False + + def stage_forward_button_clicked(self, *anything): + assert False + + def stage_execute_button_clicked(self, *anything): + assert False + + def stage_close_button_clicked(self, *anything): + assert False + + def stage_adhoc_commands_window_delete_event(self, *anything): + assert False + + def do_nothing(self, *anything): + return False + + # Widget callbacks... def on_back_button_clicked(self, *anything): return self.stage_back_button_clicked(*anything) @@ -123,8 +141,10 @@ class CommandWindow: # stage 1: waiting for command list def stage1(self): - '''Prepare the first stage. Request command list, - set appropriate state of widgets.''' + """ + Prepare the first stage. Request command list, set appropriate state of + widgets + """ # close old stage... self.stage_finish() @@ -164,9 +184,12 @@ class CommandWindow: # stage 2: choosing the command to execute def stage2(self): - '''Populate the command list vbox with radiobuttons - (FIXME: if there is more commands, maybe some kind of list?), - set widgets' state.''' + """ + Populate the command list vbox with radiobuttons + + FIXME: If there is more commands, maybe some kind of list, set widgets + state + """ # close old stage self.stage_finish() @@ -198,7 +221,9 @@ class CommandWindow: self.stage_adhoc_commands_window_delete_event = self.do_nothing def stage2_finish(self): - '''Remove widgets we created. Not needed when the window is destroyed.''' + """ + Remove widgets we created. Not needed when the window is destroyed + """ def remove_widget(widget): self.command_list_vbox.remove(widget) self.command_list_vbox.foreach(remove_widget) @@ -247,8 +272,10 @@ class CommandWindow: pass def stage3_close_button_clicked(self, widget): - ''' We are in the middle of executing command. Ask user if he really want to cancel - the process, then... cancel it. ''' + """ + We are in the middle of executing command. Ask user if he really want to + cancel the process, then cancel it + """ # this works also as a handler for window_delete_event, so we have to return appropriate # values if self.form_status == 'completed': @@ -362,7 +389,9 @@ class CommandWindow: # stage 4: no commands are exposed def stage4(self): - '''Display the message. Wait for user to close the window''' + """ + Display the message. Wait for user to close the window + """ # close old stage self.stage_finish() @@ -387,7 +416,9 @@ class CommandWindow: # stage 5: an error has occured def stage5(self, error=None, errorid=None, senderror=False): - '''Display the error message. Wait for user to close the window''' + """ + Display the error message. Wait for user to close the window + """ # FIXME: sending error to responder # close old stage self.stage_finish() @@ -430,8 +461,10 @@ class CommandWindow: # helpers to handle pulsing in progressbar def setup_pulsing(self, progressbar): - '''Set the progressbar to pulse. Makes a custom - function to repeatedly call progressbar.pulse() method.''' + """ + Set the progressbar to pulse. Makes a custom function to repeatedly call + progressbar.pulse() method + """ assert not self.pulse_id assert isinstance(progressbar, gtk.ProgressBar) @@ -443,14 +476,18 @@ class CommandWindow: self.pulse_id = gobject.timeout_add(80, callback) def remove_pulsing(self): - '''Stop pulsing, useful when especially when removing widget.''' + """ + Stop pulsing, useful when especially when removing widget + """ if self.pulse_id: gobject.source_remove(self.pulse_id) self.pulse_id=None # handling xml stanzas def request_command_list(self): - '''Request the command list. Change stage on delivery.''' + """ + Request the command list. Change stage on delivery + """ query = xmpp.Iq(typ='get', to=xmpp.JID(self.jid), queryNS=xmpp.NS_DISCO_ITEMS) query.setQuerynode(xmpp.NS_COMMANDS) @@ -481,7 +518,9 @@ class CommandWindow: self.account.connection.SendAndCallForResponse(query, callback) def send_command(self, action='execute'): - '''Send the command with data form. Wait for reply.''' + """ + Send the command with data form. Wait for reply + """ # create the stanza assert isinstance(self.commandnode, unicode) assert action in ('execute', 'prev', 'next', 'complete') @@ -510,7 +549,9 @@ class CommandWindow: self.account.connection.SendAndCallForResponse(stanza, callback) def send_cancel(self): - '''Send the command with action='cancel'. ''' + """ + Send the command with action='cancel' + """ assert self.commandnode if self.sessionid and self.account.connection: # we already have sessionid, so the service sent at least one reply. From f7b7e59935105c1fe394dd9e36c89a405cec95ad Mon Sep 17 00:00:00 2001 From: Alexander Cherniuk Date: Wed, 25 Nov 2009 16:41:44 +0200 Subject: [PATCH 67/73] More doc-string refactoring --- src/advanced_configuration_window.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/advanced_configuration_window.py b/src/advanced_configuration_window.py index c56ee27ad..b35461287 100644 --- a/src/advanced_configuration_window.py +++ b/src/advanced_configuration_window.py @@ -43,7 +43,9 @@ C_TYPE GTKGUI_GLADE = 'manage_accounts_window.glade' def rate_limit(rate): - ''' call func at most *rate* times per second ''' + """ + Call func at most *rate* times per second + """ def decorator(func): timeout = [None] def f(*args, **kwargs): @@ -131,8 +133,10 @@ class AdvancedConfigurationWindow(object): gajim.interface.instances['advanced_config'] = self def cb_value_column_data(self, col, cell, model, iter_): - '''check if it's boolen or holds password stuff and if yes - make the cellrenderertext not editable else it's editable''' + """ + Check if it's boolen or holds password stuff and if yes make the + cellrenderertext not editable, else - it's editable + """ optname = model[iter_][C_PREFNAME] opttype = model[iter_][C_TYPE] if opttype == self.types['boolean'] or optname == 'password': From 3aa07f485eb032d4498d62345546e056400b9b2f Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 25 Nov 2009 16:29:33 +0100 Subject: [PATCH 68/73] refactor connection_handlers_zeroconf.py --- src/common/connection_handlers.py | 39 ++- src/common/zeroconf/client_zeroconf.py | 3 + .../zeroconf/connection_handlers_zeroconf.py | 309 +----------------- src/common/zeroconf/connection_zeroconf.py | 2 + src/gui_interface.py | 2 + 5 files changed, 40 insertions(+), 315 deletions(-) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 8bcf9b51c..089fbd69b 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -317,15 +317,20 @@ class ConnectionBytestream: field.setValue(common.xmpp.NS_BYTESTREAM) self.connection.send(iq) + def _ft_get_our_jid(self): + our_jid = gajim.get_jid_from_account(self.name) + resource = self.server_resource + return our_jid + '/' + resource + + def _ft_get_receiver_jid(self, file_props): + return file_props['receiver'].jid + '/' + file_props['receiver'].resource + def send_file_request(self, file_props): ''' send iq for new FT request ''' if not self.connection or self.connected < 2: return - our_jid = gajim.get_jid_from_account(self.name) - resource = self.server_resource - frm = our_jid + '/' + resource - file_props['sender'] = frm - fjid = file_props['receiver'].jid + '/' + file_props['receiver'].resource + file_props['sender'] = self._ft_get_our_jid() + fjid = self._ft_get_receiver_jid(file_props) iq = common.xmpp.Protocol(name = 'iq', to = fjid, typ = 'set') iq.setID(file_props['sid']) @@ -418,6 +423,9 @@ class ConnectionBytestream: self.dispatch('FILE_REQUEST_ERROR', (jid, file_props, '')) raise common.xmpp.NodeProcessed + def _ft_get_from(self, iq_obj): + return helpers.get_full_jid_from_iq(iq_obj) + def _bytestreamSetCB(self, con, iq_obj): log.debug('_bytestreamSetCB') target = unicode(iq_obj.getAttr('to')) @@ -434,7 +442,7 @@ class ConnectionBytestream: 'target': target, 'id': id_, 'sid': sid, - 'initiator': helpers.get_full_jid_from_iq(iq_obj) + 'initiator': self._ft_get_from(iq_obj) } for attr in item.getAttrs(): host_dict[attr] = item.getAttr(attr) @@ -472,7 +480,7 @@ class ConnectionBytestream: return if not real_id.startswith('au_'): return - frm = helpers.get_full_jid_from_iq(iq_obj) + frm = self._ft_get_from(iq_obj) id_ = real_id[3:] if id_ in self.files_props: file_props = self.files_props[id_] @@ -482,9 +490,12 @@ class ConnectionBytestream: gajim.socks5queue.activate_proxy(host['idx']) raise common.xmpp.NodeProcessed + def _ft_get_streamhost_jid_attr(self, streamhost): + return helpers.parse_jid(streamhost.getAttr('jid')) + def _bytestreamResultCB(self, con, iq_obj): log.debug('_bytestreamResultCB') - frm = helpers.get_full_jid_from_iq(iq_obj) + frm = self._ft_get_from(iq_obj) real_id = unicode(iq_obj.getAttr('id')) query = iq_obj.getTag('query') gajim.proxy65_manager.resolve_result(frm, query) @@ -512,7 +523,7 @@ class ConnectionBytestream: gajim.socks5queue.activate_proxy(host['idx']) break raise common.xmpp.NodeProcessed - jid = helpers.parse_jid(streamhost.getAttr('jid')) + jid = self._ft_get_streamhost_jid_attr(streamhost) if 'streamhost-used' in file_props and \ file_props['streamhost-used'] is True: raise common.xmpp.NodeProcessed @@ -569,7 +580,7 @@ class ConnectionBytestream: if 'request-id' in file_props: # we have already sent streamhosts info return - file_props['receiver'] = helpers.get_full_jid_from_iq(iq_obj) + file_props['receiver'] = self._ft_get_from(iq_obj) si = iq_obj.getTag('si') file_tag = si.getTag('file') range_tag = None @@ -595,9 +606,9 @@ class ConnectionBytestream: def _siSetCB(self, con, iq_obj): log.debug('_siSetCB') - jid = helpers.get_jid_from_iq(iq_obj) + jid = self._ft_get_from(iq_obj) file_props = {'type': 'r'} - file_props['sender'] = helpers.get_full_jid_from_iq(iq_obj) + file_props['sender'] = jid file_props['request-id'] = unicode(iq_obj.getAttr('id')) si = iq_obj.getTag('si') profile = si.getAttr('profile') @@ -635,7 +646,7 @@ class ConnectionBytestream: file_props['mime-type'] = mime_type our_jid = gajim.get_jid_from_account(self.name) resource = self.server_resource - file_props['receiver'] = our_jid + '/' + resource + file_props['receiver'] = self._ft_get_our_jid() file_props['sid'] = unicode(si.getAttr('id')) file_props['transfered_size'] = [] gajim.socks5queue.add_file_props(self.name, file_props) @@ -656,7 +667,7 @@ class ConnectionBytestream: if file_props is None: # file properties for jid is none return - jid = helpers.get_jid_from_iq(iq_obj) + jid = self._ft_get_from(iq_obj) file_props['error'] = -3 self.dispatch('FILE_REQUEST_ERROR', (jid, file_props, '')) raise common.xmpp.NodeProcessed diff --git a/src/common/zeroconf/client_zeroconf.py b/src/common/zeroconf/client_zeroconf.py index 6b50a9fea..23834be28 100644 --- a/src/common/zeroconf/client_zeroconf.py +++ b/src/common/zeroconf/client_zeroconf.py @@ -288,6 +288,7 @@ class P2PClient(IdleObject): pass def _register_handlers(self): + self._caller.peerhost = self.Connection._sock.getsockname() self.RegisterHandler('message', lambda conn, data:self._caller._messageCB( self.Server, conn, data)) self.RegisterHandler('iq', self._caller._siSetCB, 'set', @@ -709,6 +710,8 @@ class ClientZeroconf: try: item = self.roster[to] except KeyError: + raise KeyError + print 'ret', to, self.roster.keys() # Contact offline return -1 diff --git a/src/common/zeroconf/connection_handlers_zeroconf.py b/src/common/zeroconf/connection_handlers_zeroconf.py index 4ea0e65bb..60dbb0c1b 100644 --- a/src/common/zeroconf/connection_handlers_zeroconf.py +++ b/src/common/zeroconf/connection_handlers_zeroconf.py @@ -70,311 +70,18 @@ class ConnectionVcard(connection_handlers.ConnectionVcard): pass class ConnectionBytestream(connection_handlers.ConnectionBytestream): - def send_socks5_info(self, file_props, fast = True, receiver = None, - sender = None): - ''' send iq for the present streamhosts and proxies ''' - if not isinstance(self.peerhost, tuple): - return - port = gajim.config.get('file_transfers_port') - ft_add_hosts_to_send = gajim.config.get('ft_add_hosts_to_send') - if receiver is None: - receiver = file_props['receiver'] - if sender is None: - sender = file_props['sender'] - sha_str = helpers.get_auth_sha(file_props['sid'], sender, - receiver) - file_props['sha_str'] = sha_str - ft_add_hosts = [] - if ft_add_hosts_to_send: - ft_add_hosts_to_send = [e.strip() for e in ft_add_hosts_to_send.split(',')] - for ft_host in ft_add_hosts_to_send: - try: - ft_host = socket.gethostbyname(ft_host) - ft_add_hosts.append(ft_host) - except socket.gaierror: - self.dispatch('ERROR', (_('Wrong host'), _('The host %s you configured as the ft_add_hosts_to_send advanced option is not valid, so ignored.') % ft_host)) - listener = gajim.socks5queue.start_listener(port, - sha_str, self._result_socks5_sid, file_props['sid']) - if listener is None: - file_props['error'] = -5 - self.dispatch('FILE_REQUEST_ERROR', (unicode(receiver), file_props, - '')) - self._connect_error(unicode(receiver), file_props['sid'], - file_props['sid'], code = 406) - return + def _ft_get_from(self, iq_obj): + return unicode(iq_obj.getFrom()) - iq = common.xmpp.Protocol(name = 'iq', to = unicode(receiver), - typ = 'set') - file_props['request-id'] = 'id_' + file_props['sid'] - iq.setID(file_props['request-id']) - query = iq.setTag('query') - query.setNamespace(common.xmpp.NS_BYTESTREAM) - query.setAttr('mode', 'tcp') - query.setAttr('sid', file_props['sid']) - for ft_host in ft_add_hosts: - # The streamhost, if set - ostreamhost = common.xmpp.Node(tag = 'streamhost') - query.addChild(node = ostreamhost) - ostreamhost.setAttr('port', unicode(port)) - ostreamhost.setAttr('host', ft_host) - ostreamhost.setAttr('jid', sender) - for thehost in self.peerhost: - thehost = self.peerhost[0] - streamhost = common.xmpp.Node(tag = 'streamhost') # My IP - query.addChild(node = streamhost) - streamhost.setAttr('port', unicode(port)) - streamhost.setAttr('host', thehost) - streamhost.setAttr('jid', sender) - self.connection.send(iq) + def _ft_get_our_jid(self): + return gajim.get_jid_from_account(self.name) - def send_file_request(self, file_props): - ''' send iq for new FT request ''' - if not self.connection or self.connected < 2: - return - our_jid = gajim.get_jid_from_account(self.name) - frm = our_jid - file_props['sender'] = frm - fjid = file_props['receiver'].jid - iq = common.xmpp.Protocol(name = 'iq', to = fjid, - typ = 'set') - iq.setID(file_props['sid']) - self.files_props[file_props['sid']] = file_props - si = iq.setTag('si') - si.setNamespace(common.xmpp.NS_SI) - si.setAttr('profile', common.xmpp.NS_FILE) - si.setAttr('id', file_props['sid']) - file_tag = si.setTag('file') - file_tag.setNamespace(common.xmpp.NS_FILE) - file_tag.setAttr('name', file_props['name']) - file_tag.setAttr('size', file_props['size']) - desc = file_tag.setTag('desc') - if 'desc' in file_props: - desc.setData(file_props['desc']) - file_tag.setTag('range') - feature = si.setTag('feature') - feature.setNamespace(common.xmpp.NS_FEATURE) - _feature = common.xmpp.DataForm(typ='form') - feature.addChild(node=_feature) - field = _feature.setField('stream-method') - field.setAttr('type', 'list-single') - field.addOption(common.xmpp.NS_BYTESTREAM) - self.connection.send(iq) + def _ft_get_receiver_jid(self, file_props): + return file_props['receiver'].jid - def _bytestreamSetCB(self, con, iq_obj): - log.debug('_bytestreamSetCB') - target = unicode(iq_obj.getAttr('to')) - id_ = unicode(iq_obj.getAttr('id')) - query = iq_obj.getTag('query') - sid = unicode(query.getAttr('sid')) - file_props = gajim.socks5queue.get_file_props( - self.name, sid) - streamhosts=[] - for item in query.getChildren(): - if item.getName() == 'streamhost': - host_dict={ - 'state': 0, - 'target': target, - 'id': id_, - 'sid': sid, - 'initiator': unicode(iq_obj.getFrom()) - } - for attr in item.getAttrs(): - host_dict[attr] = item.getAttr(attr) - streamhosts.append(host_dict) - if file_props is None: - if sid in self.files_props: - file_props = self.files_props[sid] - file_props['fast'] = streamhosts - if file_props['type'] == 's': - if 'streamhosts' in file_props: - file_props['streamhosts'].extend(streamhosts) - else: - file_props['streamhosts'] = streamhosts - if not gajim.socks5queue.get_file_props(self.name, sid): - gajim.socks5queue.add_file_props(self.name, file_props) - gajim.socks5queue.connect_to_hosts(self.name, sid, - self.send_success_connect_reply, None) - raise common.xmpp.NodeProcessed + def _ft_get_streamhost_jid_attr(self, streamhost): + return streamhost.getAttr('jid') - file_props['streamhosts'] = streamhosts - if file_props['type'] == 'r': - gajim.socks5queue.connect_to_hosts(self.name, sid, - self.send_success_connect_reply, self._connect_error) - raise common.xmpp.NodeProcessed - - def _ResultCB(self, con, iq_obj): - log.debug('_ResultCB') - # if we want to respect jep-0065 we have to check for proxy - # activation result in any result iq - real_id = unicode(iq_obj.getAttr('id')) - if not real_id.startswith('au_'): - return - frm = unicode(iq_obj.getFrom()) - id_ = real_id[3:] - if id_ in self.files_props: - file_props = self.files_props[id_] - if file_props['streamhost-used']: - for host in file_props['proxyhosts']: - if host['initiator'] == frm and 'idx' in host: - gajim.socks5queue.activate_proxy(host['idx']) - raise common.xmpp.NodeProcessed - - def _bytestreamResultCB(self, con, iq_obj): - log.debug('_bytestreamResultCB') - frm = unicode(iq_obj.getFrom()) - real_id = unicode(iq_obj.getAttr('id')) - query = iq_obj.getTag('query') - gajim.proxy65_manager.resolve_result(frm, query) - - try: - streamhost = query.getTag('streamhost-used') - except Exception: # this bytestream result is not what we need - pass - id_ = real_id[3:] - if id_ in self.files_props: - file_props = self.files_props[id_] - else: - raise common.xmpp.NodeProcessed - if streamhost is None: - # proxy approves the activate query - if real_id.startswith('au_'): - id_ = real_id[3:] - if 'streamhost-used' not in file_props or \ - file_props['streamhost-used'] is False: - raise common.xmpp.NodeProcessed - if 'proxyhosts' not in file_props: - raise common.xmpp.NodeProcessed - for host in file_props['proxyhosts']: - if host['initiator'] == frm and \ - unicode(query.getAttr('sid')) == file_props['sid']: - gajim.socks5queue.activate_proxy(host['idx']) - break - raise common.xmpp.NodeProcessed - jid = streamhost.getAttr('jid') - if 'streamhost-used' in file_props and \ - file_props['streamhost-used'] is True: - raise common.xmpp.NodeProcessed - - if real_id.startswith('au_'): - gajim.socks5queue.send_file(file_props, self.name) - raise common.xmpp.NodeProcessed - - proxy = None - if 'proxyhosts' in file_props: - for proxyhost in file_props['proxyhosts']: - if proxyhost['jid'] == jid: - proxy = proxyhost - - if proxy is not None: - file_props['streamhost-used'] = True - if 'streamhosts' not in file_props: - file_props['streamhosts'] = [] - file_props['streamhosts'].append(proxy) - file_props['is_a_proxy'] = True - receiver = socks5.Socks5Receiver(gajim.idlequeue, proxy, file_props['sid'], file_props) - gajim.socks5queue.add_receiver(self.name, receiver) - proxy['idx'] = receiver.queue_idx - gajim.socks5queue.on_success = self._proxy_auth_ok - raise common.xmpp.NodeProcessed - - else: - gajim.socks5queue.send_file(file_props, self.name) - if 'fast' in file_props: - fasts = file_props['fast'] - if len(fasts) > 0: - self._connect_error(frm, fasts[0]['id'], file_props['sid'], - code = 406) - - raise common.xmpp.NodeProcessed - - def _siResultCB(self, con, iq_obj): - log.debug('_siResultCB') - self.peerhost = con._owner.Connection._sock.getsockname() - id_ = iq_obj.getAttr('id') - if id_ not in self.files_props: - # no such jid - return - file_props = self.files_props[id_] - if file_props is None: - # file properties for jid is none - return - if 'request-id' in file_props: - # we have already sent streamhosts info - return - file_props['receiver'] = unicode(iq_obj.getFrom()) - si = iq_obj.getTag('si') - file_tag = si.getTag('file') - range_tag = None - if file_tag: - range_tag = file_tag.getTag('range') - if range_tag: - offset = range_tag.getAttr('offset') - if offset: - file_props['offset'] = int(offset) - length = range_tag.getAttr('length') - if length: - file_props['length'] = int(length) - feature = si.setTag('feature') - if feature.getNamespace() != common.xmpp.NS_FEATURE: - return - form_tag = feature.getTag('x') - form = common.xmpp.DataForm(node=form_tag) - field = form.getField('stream-method') - if field.getValue() != common.xmpp.NS_BYTESTREAM: - return - self.send_socks5_info(file_props, fast = True) - raise common.xmpp.NodeProcessed - - def _siSetCB(self, con, iq_obj): - log.debug('_siSetCB') - jid = unicode(iq_obj.getFrom()) - si = iq_obj.getTag('si') - profile = si.getAttr('profile') - mime_type = si.getAttr('mime-type') - if profile != common.xmpp.NS_FILE: - return - file_tag = si.getTag('file') - file_props = {'type': 'r'} - for attribute in file_tag.getAttrs(): - if attribute in ('name', 'size', 'hash', 'date'): - val = file_tag.getAttr(attribute) - if val is None: - continue - file_props[attribute] = val - file_desc_tag = file_tag.getTag('desc') - if file_desc_tag is not None: - file_props['desc'] = file_desc_tag.getData() - - if mime_type is not None: - file_props['mime-type'] = mime_type - our_jid = gajim.get_jid_from_account(self.name) - file_props['receiver'] = our_jid - file_props['sender'] = unicode(iq_obj.getFrom()) - file_props['request-id'] = unicode(iq_obj.getAttr('id')) - file_props['sid'] = unicode(si.getAttr('id')) - file_props['transfered_size'] = [] - gajim.socks5queue.add_file_props(self.name, file_props) - self.dispatch('FILE_REQUEST', (jid, file_props)) - raise common.xmpp.NodeProcessed - - def _siErrorCB(self, con, iq_obj): - log.debug('_siErrorCB') - si = iq_obj.getTag('si') - profile = si.getAttr('profile') - if profile != common.xmpp.NS_FILE: - return - id_ = iq_obj.getAttr('id') - if id_ not in self.files_props: - # no such jid - return - file_props = self.files_props[id_] - if file_props is None: - # file properties for jid is none - return - jid = unicode(iq_obj.getFrom()) - file_props['error'] = -3 - self.dispatch('FILE_REQUEST_ERROR', (jid, file_props, '')) - raise common.xmpp.NodeProcessed class ConnectionHandlersZeroconf(ConnectionVcard, ConnectionBytestream, ConnectionCommands, ConnectionPEP, connection_handlers.ConnectionHandlersBase): diff --git a/src/common/zeroconf/connection_zeroconf.py b/src/common/zeroconf/connection_zeroconf.py index e1cc30971..dff41e39b 100644 --- a/src/common/zeroconf/connection_zeroconf.py +++ b/src/common/zeroconf/connection_zeroconf.py @@ -85,6 +85,8 @@ class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf): 'custom_port', 5298) gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'is_zeroconf', True) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, + 'use_ft_proxies', False) #XXX make sure host is US-ASCII self.host = unicode(socket.gethostname()) gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'hostname', diff --git a/src/gui_interface.py b/src/gui_interface.py index 7c7825100..7a948d5c1 100644 --- a/src/gui_interface.py +++ b/src/gui_interface.py @@ -1324,6 +1324,7 @@ class Interface: def handle_event_file_request_error(self, account, array): # ('FILE_REQUEST_ERROR', account, (jid, file_props, error_msg)) jid, file_props, errmsg = array + jid = gajim.get_jid_without_resource(jid) ft = self.instances['file_transfers'] ft.set_status(file_props['type'], file_props['sid'], 'stop') errno = file_props['error'] @@ -1353,6 +1354,7 @@ class Interface: def handle_event_file_request(self, account, array): jid = array[0] + jid = gajim.get_jid_without_resource(jid) if jid not in gajim.contacts.get_jid_list(account): keyID = '' attached_keys = gajim.config.get_per('accounts', account, From 31f5e22f6278b95c44cedcb24a762d5ff8fb5a15 Mon Sep 17 00:00:00 2001 From: Alexander Cherniuk Date: Wed, 25 Nov 2009 17:45:05 +0200 Subject: [PATCH 69/73] One more portion of doc-strings refactoring --- src/atom_window.py | 18 ++- src/config.py | 138 +++++++++++----- src/conversation_textview.py | 75 +++++---- src/dataforms_widget.py | 64 +++++--- src/dialogs.py | 304 +++++++++++++++++++++++++---------- 5 files changed, 417 insertions(+), 182 deletions(-) diff --git a/src/atom_window.py b/src/atom_window.py index 56449846f..4712dfc41 100644 --- a/src/atom_window.py +++ b/src/atom_window.py @@ -35,7 +35,9 @@ class AtomWindow: @classmethod def newAtomEntry(cls, entry): - ''' Queue new entry, open window if there's no one opened. ''' + """ + Queue new entry, open window if there's no one opened + """ cls.entries.append(entry) if cls.window is None: @@ -48,7 +50,9 @@ class AtomWindow: cls.window = None def __init__(self): - ''' Create new window... only if we have anything to show. ''' + """ + Create new window... only if we have anything to show + """ assert len(self.__class__.entries)>0 self.entry = None # the entry actually displayed @@ -69,7 +73,9 @@ class AtomWindow: 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. ''' + """ + Get next entry from the queue and display it in the window + """ assert len(self.__class__.entries)>0 newentry = self.__class__.entries.pop(0) @@ -103,8 +109,10 @@ class AtomWindow: self.entry = newentry def updateCounter(self): - ''' We display number of events on the top of window, sometimes it needs to be - changed...''' + """ + Display number of events on the top of window, sometimes it needs to be + changed + """ count = len(self.__class__.entries) if count>0: self.new_entry_label.set_text(i18n.ngettext( diff --git a/src/config.py b/src/config.py index 727744a04..adf5078c5 100644 --- a/src/config.py +++ b/src/config.py @@ -63,17 +63,23 @@ from common.exceptions import GajimGeneralException #---------- PreferencesWindow class -------------# class PreferencesWindow: - '''Class for Preferences window''' + """ + Class for Preferences window + """ def on_preferences_window_destroy(self, widget): - '''close window''' + """ + Close window + """ del gajim.interface.instances['preferences'] def on_close_button_clicked(self, widget): self.window.destroy() def __init__(self): - '''Initialize Preferences window''' + """ + Initialize Preferences window + """ self.xml = gtkgui_helpers.get_glade('preferences_window.glade') self.window = self.xml.get_widget('preferences_window') self.window.set_transient_for(gajim.interface.roster.window) @@ -495,8 +501,10 @@ class PreferencesWindow: self.window.hide() def get_per_account_option(self, opt): - '''Return the value of the option opt if it's the same in all accounts - else returns "mixed"''' + """ + Return the value of the option opt if it's the same in all accounts else + returns "mixed" + """ if len(gajim.connections) == 0: # a non existant key return default value return gajim.config.get_per('accounts', '__default__', opt) @@ -585,7 +593,9 @@ class PreferencesWindow: self.toggle_emoticons() def toggle_emoticons(self): - '''Update emoticons state in Opened Chat Windows''' + """ + Update emoticons state in Opened Chat Windows + """ for win in gajim.interface.msg_win_mgr.windows(): win.toggle_emoticons() @@ -779,7 +789,9 @@ class PreferencesWindow: self.sounds_preferences.window.present() def update_text_tags(self): - '''Update color tags in Opened Chat Windows''' + """ + Update color tags in opened chat windows + """ for win in gajim.interface.msg_win_mgr.windows(): win.update_tags() @@ -800,7 +812,9 @@ class PreferencesWindow: gajim.interface.save_config() def update_text_font(self): - '''Update text font in Opened Chat Windows''' + """ + Update text font in opened chat windows + """ for win in gajim.interface.msg_win_mgr.windows(): win.update_font() @@ -879,7 +893,9 @@ class PreferencesWindow: gajim.interface.save_config() def _set_color(self, state, widget_name, option): - ''' set color value in prefs and update the UI ''' + """ + Set color value in prefs and update the UI + """ if state: color = self.xml.get_widget(widget_name).get_color() color_string = gtkgui_helpers.make_color_string(color) @@ -1346,7 +1362,10 @@ class ManageProxiesWindow: #---------- AccountsWindow class -------------# class AccountsWindow: - '''Class for accounts window: list of accounts''' + """ + Class for accounts window: list of accounts + """ + def on_accounts_window_destroy(self, widget): del gajim.interface.instances['accounts'] @@ -1414,7 +1433,9 @@ class AccountsWindow: iter_ = model.iter_next(iter_) def init_accounts(self): - '''initialize listStore with existing accounts''' + """ + Initialize listStore with existing accounts + """ self.remove_button.set_sensitive(False) self.rename_button.set_sensitive(False) self.current_account = None @@ -1440,7 +1461,9 @@ class AccountsWindow: elif self.need_relogin and self.current_account and \ gajim.connections[self.current_account].connected > 0: def login(account, show_before, status_before): - ''' login with previous status''' + """ + Login with previous status + """ # first make sure connection is really closed, # 0.5 may not be enough gajim.connections[account].disconnect(True) @@ -1473,7 +1496,9 @@ class AccountsWindow: self.resend_presence = False def on_accounts_treeview_cursor_changed(self, widget): - '''Activate modify buttons when a row is selected, update accounts info''' + """ + Activate modify buttons when a row is selected, update accounts info + """ sel = self.accounts_treeview.get_selection() (model, iter_) = sel.get_selected() if iter_: @@ -1739,7 +1764,9 @@ class AccountsWindow: gajim.config.get_per('accounts', account, 'use_ft_proxies')) def on_add_button_clicked(self, widget): - '''When add button is clicked: open an account information window''' + """ + When add button is clicked: open an account information window + """ if 'account_creation_wizard' in gajim.interface.instances: gajim.interface.instances['account_creation_wizard'].window.present() else: @@ -1747,8 +1774,10 @@ class AccountsWindow: AccountCreationWizardWindow() def on_remove_button_clicked(self, widget): - '''When delete button is clicked: - Remove an account from the listStore and from the config file''' + """ + When delete button is clicked: Remove an account from the listStore and + from the config file + """ if not self.current_account: return account = self.current_account @@ -2409,8 +2438,11 @@ class AccountsWindow: 'zeroconf_email', email) class FakeDataForm(gtk.Table, object): - '''Class for forms that are in XML format value1 - infos in a table {entry1: value1, }''' + """ + Class for forms that are in XML format value1 infos in a + table {entry1: value1} + """ + def __init__(self, infos): gtk.Table.__init__(self) self.infos = infos @@ -2418,7 +2450,9 @@ class FakeDataForm(gtk.Table, object): self._draw_table() def _draw_table(self): - '''Draw the table''' + """ + Draw the table + """ nbrow = 0 if 'instructions' in self.infos: nbrow = 1 @@ -2452,9 +2486,11 @@ class FakeDataForm(gtk.Table, object): return self.infos class ServiceRegistrationWindow: - '''Class for Service registration window: - Window that appears when we want to subscribe to a service - if is_form we use dataforms_widget else we use service_registarion_window''' + """ + Class for Service registration window. Window that appears when we want to + subscribe to a service if is_form we use dataforms_widget else we use + service_registarion_window + """ def __init__(self, service, infos, account, is_form): self.service = service self.account = account @@ -2501,7 +2537,7 @@ class ServiceRegistrationWindow: self.window.destroy() class GroupchatConfigWindow: - '''GroupchatConfigWindow class''' + def __init__(self, account, room_jid, form = None): self.account = account self.room_jid = room_jid @@ -2654,7 +2690,9 @@ class GroupchatConfigWindow: self.remove_button[affiliation].set_sensitive(True) def affiliation_list_received(self, users_dict): - '''Fill the affiliation treeview''' + """ + Fill the affiliation treeview + """ for jid in users_dict: affiliation = users_dict[jid]['affiliation'] if affiliation not in self.affiliation_labels.keys(): @@ -2703,8 +2741,10 @@ class GroupchatConfigWindow: #---------- RemoveAccountWindow class -------------# class RemoveAccountWindow: - '''ask for removing from gajim only or from gajim and server too - and do removing of the account given''' + """ + Ask for removing from gajim only or from gajim and server too and do + removing of the account given + """ def on_remove_account_window_destroy(self, widget): if self.account in gajim.interface.instances: @@ -2904,7 +2944,9 @@ class ManageBookmarksWindow: del gajim.interface.instances['manage_bookmarks'] def on_add_bookmark_button_clicked(self, widget): - '''Add a new bookmark.''' + """ + Add a new bookmark + """ # Get the account that is currently used # (the parent of the currently selected item) (model, iter_) = self.selection.get_selected() @@ -2929,9 +2971,9 @@ class ManageBookmarksWindow: self.view.set_cursor(model.get_path(iter_)) def on_remove_bookmark_button_clicked(self, widget): - ''' - Remove selected bookmark. - ''' + """ + Remove selected bookmark + """ (model, iter_) = self.selection.get_selected() if not iter_: # Nothing selected return @@ -2944,9 +2986,9 @@ class ManageBookmarksWindow: self.clear_fields() def check_valid_bookmark(self): - ''' - Check if all neccessary fields are entered correctly. - ''' + """ + Check if all neccessary fields are entered correctly + """ (model, iter_) = self.selection.get_selected() if not model.iter_parent(iter_): @@ -2963,10 +3005,10 @@ class ManageBookmarksWindow: return True def on_ok_button_clicked(self, widget): - ''' - Parse the treestore data into our new bookmarks array, - then send the new bookmarks to the server. - ''' + """ + Parse the treestore data into our new bookmarks array, then send the new + bookmarks to the server. + """ (model, iter_) = self.selection.get_selected() if iter_ and model.iter_parent(iter_): #bookmark selected, check it @@ -2997,9 +3039,9 @@ class ManageBookmarksWindow: self.window.destroy() def bookmark_selected(self, selection): - ''' + """ Fill in the bookmark's data into the fields. - ''' + """ (model, iter_) = selection.get_selected() if not iter_: @@ -3444,8 +3486,10 @@ class AccountCreationWizardWindow: return True # loop forever def new_acc_connected(self, form, is_form, ssl_msg, ssl_err, ssl_cert, - ssl_fingerprint): - '''connection to server succeded, present the form to the user.''' + ssl_fingerprint): + """ + Connection to server succeded, present the form to the user + """ if self.update_progressbar_timeout_id is not None: gobject.source_remove(self.update_progressbar_timeout_id) self.back_button.show() @@ -3479,7 +3523,9 @@ class AccountCreationWizardWindow: self.notebook.set_current_page(4) # show form page def new_acc_not_connected(self, reason): - '''Account creation failed: connection to server failed''' + """ + Account creation failed: connection to server failed + """ if self.account not in gajim.connections: return if self.update_progressbar_timeout_id is not None: @@ -3499,7 +3545,9 @@ class AccountCreationWizardWindow: self.notebook.set_current_page(6) # show finish page def acc_is_ok(self, config): - '''Account creation succeeded''' + """ + Account creation succeeded + """ self.create_vars(config) self.show_finish_page() @@ -3507,7 +3555,9 @@ class AccountCreationWizardWindow: gobject.source_remove(self.update_progressbar_timeout_id) def acc_is_not_ok(self, reason): - '''Account creation failed''' + """ + Account creation failed + """ self.back_button.show() self.cancel_button.show() self.go_online_checkbutton.hide() diff --git a/src/conversation_textview.py b/src/conversation_textview.py index fa5dcd25e..7744ef685 100644 --- a/src/conversation_textview.py +++ b/src/conversation_textview.py @@ -158,8 +158,10 @@ class TextViewImage(gtk.Image): class ConversationTextview(gobject.GObject): - '''Class for the conversation textview (where user reads already said - messages) for chat/groupchat windows''' + """ + Class for the conversation textview (where user reads already said messages) + for chat/groupchat windows + """ __gsignals__ = dict( quote = (gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION, None, # return value @@ -177,8 +179,10 @@ class ConversationTextview(gobject.GObject): SCROLL_DELAY = 33 # milliseconds def __init__(self, account, used_in_history_window = False): - '''if used_in_history_window is True, then we do not show - Clear menuitem in context menu''' + """ + If used_in_history_window is True, then we do not show Clear menuitem in + context menu + """ gobject.GObject.__init__(self) self.used_in_history_window = used_in_history_window @@ -642,8 +646,9 @@ class ConversationTextview(gobject.GObject): return False def on_textview_motion_notify_event(self, widget, event): - '''change the cursor to a hand when we are over a mail or an - url''' + """ + Change the cursor to a hand when we are over a mail or an url + """ pointer_x, pointer_y = self.tv.window.get_pointer()[0:2] x, y = self.tv.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, pointer_x, pointer_y) @@ -688,7 +693,9 @@ class ConversationTextview(gobject.GObject): self.change_cursor = True def clear(self, tv = None): - '''clear text in the textview''' + """ + Clear text in the textview + """ buffer_ = self.tv.get_buffer() start, end = buffer_.get_bounds() buffer_.delete(start, end) @@ -698,15 +705,18 @@ class ConversationTextview(gobject.GObject): self.focus_out_end_mark = None def visit_url_from_menuitem(self, widget, link): - '''basically it filters out the widget instance''' + """ + Basically it filters out the widget instance + """ helpers.launch_browser_mailer('url', link) def on_textview_populate_popup(self, textview, menu): - '''we override the default context menu and we prepend Clear - (only if used_in_history_window is False) - and if we have sth selected we show a submenu with actions on - the phrase (see on_conversation_textview_button_press_event)''' - + """ + Override the default context menu and we prepend Clear (only if + used_in_history_window is False) and if we have sth selected we show a + submenu with actions on the phrase (see + on_conversation_textview_button_press_event) + """ separator_menuitem_was_added = False if not self.used_in_history_window: item = gtk.SeparatorMenuItem() @@ -971,13 +981,13 @@ class ConversationTextview(gobject.GObject): def detect_and_print_special_text(self, otext, other_tags, graphics=True): - '''detects special text (emots & links & formatting) - prints normal text before any special text it founts, - then print special text (that happens many times until - last special text is printed) and then returns the index + """ + Detect special text (emots & links & formatting), print normal text + before any special text it founds, then print special text (that happens + many times until last special text is printed) and then return the index after *last* special text, so we can print it in - print_conversation_line()''' - + print_conversation_line() + """ buffer_ = self.tv.get_buffer() insert_tags_func = buffer_.insert_with_tags_by_name @@ -1023,8 +1033,10 @@ class ConversationTextview(gobject.GObject): return buffer_.get_end_iter() def print_special_text(self, special_text, other_tags, graphics=True): - '''is called by detect_and_print_special_text and prints - special text (emots, links, formatting)''' + """ + Is called by detect_and_print_special_text and prints special text + (emots, links, formatting) + """ tags = [] use_other_tags = True text_is_valid_uri = False @@ -1163,9 +1175,12 @@ class ConversationTextview(gobject.GObject): buffer_.insert_with_tags_by_name(end_iter, '\n', 'eol') def print_conversation_line(self, text, jid, kind, name, tim, - other_tags_for_name=[], other_tags_for_time=[], other_tags_for_text=[], - subject=None, old_kind=None, xhtml=None, simple=False, graphics=True): - '''prints 'chat' type messages''' + other_tags_for_name=[], other_tags_for_time=[], + other_tags_for_text=[], subject=None, old_kind=None, xhtml=None, + simple=False, graphics=True): + """ + Print 'chat' type messages + """ buffer_ = self.tv.get_buffer() buffer_.begin_user_action() if self.marks_queue.full(): @@ -1263,8 +1278,10 @@ class ConversationTextview(gobject.GObject): buffer_.end_user_action() def get_time_to_show(self, tim): - '''Get the time, with the day before if needed and return it. - It DOESN'T format a fuzzy time''' + """ + Get the time, with the day before if needed and return it. It DOESN'T + format a fuzzy time + """ format = '' # get difference in days since epoch (86400 = 24*3600) # number of days since epoch for current time (in GMT) - @@ -1317,8 +1334,10 @@ class ConversationTextview(gobject.GObject): self.print_empty_line() def print_real_text(self, text, text_tags=[], name=None, xhtml=None, - graphics=True): - '''this adds normal and special text. call this to add text''' + graphics=True): + """ + Add normal and special text. call this to add text + """ if xhtml: try: if name and (text.startswith('/me ') or text.startswith('/me\n')): diff --git a/src/dataforms_widget.py b/src/dataforms_widget.py index 18195dccd..4a9681dab 100644 --- a/src/dataforms_widget.py +++ b/src/dataforms_widget.py @@ -38,7 +38,10 @@ import itertools class DataFormWidget(gtk.Alignment, object): # "public" interface - ''' Data Form widget. Use like any other widget. ''' + """ + Data Form widget. Use like any other widget + """ + def __init__(self, dataformnode=None): ''' Create a widget. ''' gtk.Alignment.__init__(self, xscale=1.0, yscale=1.0) @@ -65,7 +68,9 @@ class DataFormWidget(gtk.Alignment, object): selection.set_mode(gtk.SELECTION_MULTIPLE) def set_data_form(self, dataform): - ''' Set the data form (xmpp.DataForm) displayed in widget. ''' + """ + Set the data form (xmpp.DataForm) displayed in widget + """ assert isinstance(dataform, dataforms.DataForm) self.del_data_form() @@ -84,7 +89,9 @@ class DataFormWidget(gtk.Alignment, object): gtkgui_helpers.label_set_autowrap(self.instructions_label) def get_data_form(self): - ''' Data form displayed in the widget or None if no form. ''' + """ + Data form displayed in the widget or None if no form + """ return self._data_form def del_data_form(self): @@ -95,8 +102,10 @@ class DataFormWidget(gtk.Alignment, object): 'Data form presented in a widget') def get_title(self): - ''' Get the title of data form, as a unicode object. If no - title or no form, returns u''. Useful for setting window title. ''' + """ + Get the title of data form, as a unicode object. If no title or no form, + returns u''. Useful for setting window title + """ if self._data_form is not None: if self._data_form.title is not None: return self._data_form.title @@ -117,9 +126,11 @@ class DataFormWidget(gtk.Alignment, object): pass def clean_data_form(self): - '''Remove data about existing form. This metod is empty, because - it is rewritten by build_*_data_form, according to type of form - which is actually displayed.''' + """ + Remove data about existing form. This metod is empty, because it is + rewritten by build_*_data_form, according to type of form which is + actually displayed + """ pass def build_single_data_form(self): @@ -138,14 +149,18 @@ class DataFormWidget(gtk.Alignment, object): self.clean_data_form = self.clean_single_data_form def clean_single_data_form(self): - '''(Called as clean_data_form, read the docs of clean_data_form()). - Remove form from widget.''' + """ + Called as clean_data_form, read the docs of clean_data_form(). Remove + form from widget + """ self.singleform.destroy() self.clean_data_form = self.empty_method # we won't call it twice del self.singleform def build_multiple_data_form(self): - '''Invoked when new multiple form is to be created.''' + """ + Invoked when new multiple form is to be created + """ assert isinstance(self._data_form, dataforms.MultipleDataForm) self.clean_data_form() @@ -196,13 +211,17 @@ class DataFormWidget(gtk.Alignment, object): self.refresh_multiple_buttons() def clean_multiple_data_form(self): - '''(Called as clean_data_form, read the docs of clean_data_form()). - Remove form from widget.''' + """ + Called as clean_data_form, read the docs of clean_data_form(). Remove + form from widget + """ self.clean_data_form = self.empty_method # we won't call it twice del self.multiplemodel def refresh_multiple_buttons(self): - ''' Checks for treeview state and makes control buttons sensitive.''' + """ + Checks for treeview state and makes control buttons sensitive + """ selection = self.records_treeview.get_selection() model = self.records_treeview.get_model() count = selection.count_selected_rows() @@ -273,9 +292,12 @@ class DataFormWidget(gtk.Alignment, object): self.refresh_multiple_buttons() class SingleForm(gtk.Table, object): - ''' Widget that represent DATAFORM_SINGLE mode form. Because this is used - not only to display single forms, but to form input windows of multiple-type - forms, it is in another class.''' + """ + Widget that represent DATAFORM_SINGLE mode form. Because this is used not + only to display single forms, but to form input windows of multiple-type + forms, it is in another class + """ + def __init__(self, dataform): assert isinstance(dataform, dataforms.SimpleDataForm) @@ -284,9 +306,11 @@ class SingleForm(gtk.Table, object): self.set_row_spacings(6) def decorate_with_tooltip(widget, field): - ''' Adds a tooltip containing field's description to a widget. - Creates EventBox if widget doesn't have its own gdk window. - Returns decorated widget. ''' + """ + Adds a tooltip containing field's description to a widget. Creates + EventBox if widget doesn't have its own gdk window. Returns decorated + widget + """ if field.description != '': if widget.flags() & gtk.NO_WINDOW: evbox = gtk.EventBox() diff --git a/src/dialogs.py b/src/dialogs.py index 4f2802e75..2ca3bbcc2 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -61,9 +61,14 @@ from common import dataforms from common.exceptions import GajimGeneralException class EditGroupsDialog: - '''Class for the edit group dialog window''' + """ + Class for the edit group dialog window + """ + def __init__(self, list_): - '''list_ is a list of (contact, account) tuples''' + """ + list_ is a list of (contact, account) tuples + """ self.xml = gtkgui_helpers.get_glade('edit_groups_dialog.glade') self.dialog = self.xml.get_widget('edit_groups_dialog') self.dialog.set_transient_for(gajim.interface.roster.window) @@ -96,7 +101,9 @@ class EditGroupsDialog: self.dialog.destroy() def remove_group(self, group): - '''remove group group from all contacts and all their brothers''' + """ + Remove group group from all contacts and all their brothers + """ for (contact, account) in self.list_: gajim.interface.roster.remove_contact_from_groups(contact.jid, account, [group]) @@ -104,7 +111,9 @@ class EditGroupsDialog: gajim.interface.roster.draw_group(_('General'), account) def add_group(self, group): - '''add group group to all contacts and all their brothers''' + """ + Add group group to all contacts and all their brothers + """ for (contact, account) in self.list_: gajim.interface.roster.add_contact_to_groups(contact.jid, account, [group]) @@ -199,7 +208,9 @@ class EditGroupsDialog: column.set_attributes(renderer, active=1, inconsistent=2) class PassphraseDialog: - '''Class for Passphrase dialog''' + """ + Class for Passphrase dialog + """ def __init__(self, titletext, labeltext, checkbuttontext=None, ok_handler=None, cancel_handler=None): self.xml = gtkgui_helpers.get_glade('passphrase_dialog.glade') @@ -258,7 +269,10 @@ class PassphraseDialog: self.cancel_handler() class ChooseGPGKeyDialog: - '''Class for GPG key dialog''' + """ + Class for GPG key dialog + """ + def __init__(self, title_text, prompt_text, secret_keys, on_response, selected=None): '''secret_keys : {keyID: userName, ...}''' @@ -428,9 +442,9 @@ class ChangeActivityDialog: self.subactivity = data[1] def on_ok_button_clicked(self, widget): - ''' + """ Return activity and messsage (None if no activity selected) - ''' + """ if self.checkbutton.get_active(): self.on_response(self.activity, self.subactivity, self.entry.get_text().decode('utf-8')) @@ -524,10 +538,10 @@ class ChangeMoodDialog: self.window.destroy() class TimeoutDialog: - ''' + """ Class designed to be derivated to create timeout'd dialogs (dialogs that closes automatically after a timeout) - ''' + """ def __init__(self, timeout, on_timeout): self.countdown_left = timeout self.countdown_enabled = True @@ -540,7 +554,9 @@ class TimeoutDialog: gobject.timeout_add_seconds(1, self.countdown) def on_timeout(): - '''To be implemented in derivated classes''' + """ + To be implemented in derivated classes + """ pass def countdown(self): @@ -638,7 +654,9 @@ class ChangeStatusMessageDialog(TimeoutDialog): self.dialog.show_all() def draw_activity(self): - '''Set activity button''' + """ + Set activity button + """ img = self.xml.get_widget('activity_image') label = self.xml.get_widget('activity_button_label') if 'activity' in self.pep_dict and self.pep_dict['activity'] in \ @@ -661,7 +679,9 @@ class ChangeStatusMessageDialog(TimeoutDialog): label.set_text('') def draw_mood(self): - '''Set mood button''' + """ + Set mood button + """ img = self.xml.get_widget('mood_image') label = self.xml.get_widget('mood_button_label') if self.pep_dict['mood'] in pep.MOODS: @@ -803,13 +823,17 @@ class ChangeStatusMessageDialog(TimeoutDialog): self.pep_dict['mood_text']) class AddNewContactWindow: - '''Class for AddNewContactWindow''' + """ + Class for AddNewContactWindow + """ + uid_labels = {'jabber': _('Jabber ID:'), 'aim': _('AIM Address:'), 'gadu-gadu': _('GG Number:'), 'icq': _('ICQ Number:'), 'msn': _('MSN Address:'), 'yahoo': _('Yahoo! Address:')} + def __init__(self, account=None, jid=None, user_nick=None, group=None): self.account = account if account is None: @@ -983,11 +1007,15 @@ _('Please fill in the data of the contact you want to add in account %s') %accou self.window.destroy() def on_cancel_button_clicked(self, widget): - '''When Cancel button is clicked''' + """ + When Cancel button is clicked + """ self.window.destroy() def on_add_button_clicked(self, widget): - '''When Subscribe button is clicked''' + """ + When Subscribe button is clicked + """ jid = self.uid_entry.get_text().decode('utf-8').strip() if not jid: return @@ -1111,7 +1139,10 @@ _('Please fill in the data of the contact you want to add in account %s') %accou self.add_button.set_sensitive(False) class AboutDialog: - '''Class for about dialog''' + """ + Class for about dialog + """ + def __init__(self): dlg = gtk.AboutDialog() dlg.set_transient_for(gajim.interface.roster.window) @@ -1184,7 +1215,9 @@ class AboutDialog: return str_[0:-1] # remove latest . def get_path(self, filename): - '''where can we find this Credits file ?''' + """ + Where can we find this Credits file? + """ if os.path.isfile(os.path.join(gajim.defs.docdir, filename)): return os.path.join(gajim.defs.docdir, filename) elif os.path.isfile('../' + filename): @@ -1273,14 +1306,18 @@ class HigDialog(gtk.MessageDialog): self.destroy() def popup(self): - '''show dialog''' + """ + Show dialog + """ vb = self.get_children()[0].get_children()[0] # Give focus to top vbox vb.set_flags(gtk.CAN_FOCUS) vb.grab_focus() self.show_all() class FileChooserDialog(gtk.FileChooserDialog): - '''Non-blocking FileChooser Dialog around gtk.FileChooserDialog''' + """ + Non-blocking FileChooser Dialog around gtk.FileChooserDialog + """ def __init__(self, title_text, action, buttons, default_response, select_multiple = False, current_folder = None, on_response_ok = None, on_response_cancel = None): @@ -1332,7 +1369,10 @@ class AspellDictError: gajim.config.set('use_speller', False) class ConfirmationDialog(HigDialog): - '''HIG compliant confirmation dialog.''' + """ + HIG compliant confirmation dialog + """ + def __init__(self, pritext, sectext='', on_response_ok=None, on_response_cancel=None): self.user_response_ok = on_response_ok @@ -1359,7 +1399,10 @@ class ConfirmationDialog(HigDialog): self.destroy() class NonModalConfirmationDialog(HigDialog): - '''HIG compliant non modal confirmation dialog.''' + """ + HIG compliant non modal confirmation dialog + """ + def __init__(self, pritext, sectext='', on_response_ok=None, on_response_cancel=None): self.user_response_ok = on_response_ok @@ -1386,8 +1429,11 @@ class NonModalConfirmationDialog(HigDialog): self.destroy() class WarningDialog(HigDialog): + """ + HIG compliant warning dialog + """ + def __init__(self, pritext, sectext=''): - '''HIG compliant warning dialog.''' HigDialog.__init__( self, None, gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, pritext, sectext) self.set_modal(False) @@ -1396,8 +1442,11 @@ class WarningDialog(HigDialog): self.popup() class InformationDialog(HigDialog): + """ + HIG compliant info dialog + """ + def __init__(self, pritext, sectext=''): - '''HIG compliant info dialog.''' HigDialog.__init__(self, None, gtk.MESSAGE_INFO, gtk.BUTTONS_OK, pritext, sectext) self.set_modal(False) @@ -1405,16 +1454,22 @@ class InformationDialog(HigDialog): self.popup() class ErrorDialog(HigDialog): + """ + HIG compliant error dialog + """ + def __init__(self, pritext, sectext=''): - '''HIG compliant error dialog.''' HigDialog.__init__( self, None, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, pritext, sectext) self.popup() class YesNoDialog(HigDialog): + """ + HIG compliant YesNo dialog + """ + def __init__(self, pritext, sectext='', checktext='', on_response_yes=None, - on_response_no=None): - '''HIG compliant YesNo dialog.''' + on_response_no=None): self.user_response_yes = on_response_yes self.user_response_no = on_response_no HigDialog.__init__( self, None, @@ -1448,15 +1503,20 @@ class YesNoDialog(HigDialog): self.destroy() def is_checked(self): - ''' Get active state of the checkbutton ''' + """ + Get active state of the checkbutton + """ if not self.checkbutton: return False return self.checkbutton.get_active() class ConfirmationDialogCheck(ConfirmationDialog): - '''HIG compliant confirmation dialog with checkbutton.''' - def __init__(self, pritext, sectext='', checktext='', - on_response_ok=None, on_response_cancel=None, is_modal=True): + """ + HIG compliant confirmation dialog with checkbutton + """ + + def __init__(self, pritext, sectext='', checktext='', on_response_ok=None, + on_response_cancel=None, is_modal=True): self.user_response_ok = on_response_ok self.user_response_cancel = on_response_cancel @@ -1495,11 +1555,16 @@ class ConfirmationDialogCheck(ConfirmationDialog): self.destroy() def is_checked(self): - ''' Get active state of the checkbutton ''' + """ + Get active state of the checkbutton + """ return self.checkbutton.get_active() class ConfirmationDialogDubbleCheck(ConfirmationDialog): - '''HIG compliant confirmation dialog with 2 checkbuttons.''' + """ + HIG compliant confirmation dialog with 2 checkbuttons + """ + def __init__(self, pritext, sectext='', checktext1='', checktext2='', on_response_ok=None, on_response_cancel=None, is_modal=True): self.user_response_ok = on_response_ok @@ -1560,7 +1625,10 @@ class ConfirmationDialogDubbleCheck(ConfirmationDialog): return [is_checked_1, is_checked_2] class FTOverwriteConfirmationDialog(ConfirmationDialog): - '''HIG compliant confirmation dialog to overwrite or resume a file transfert''' + """ + HIG compliant confirmation dialog to overwrite or resume a file transfert + """ + def __init__(self, pritext, sectext='', propose_resume=True, on_response=None): HigDialog.__init__(self, None, gtk.MESSAGE_QUESTION, gtk.BUTTONS_CANCEL, @@ -1597,7 +1665,10 @@ class FTOverwriteConfirmationDialog(ConfirmationDialog): self.destroy() class CommonInputDialog: - '''Common Class for Input dialogs''' + """ + Common Class for Input dialogs + """ + def __init__(self, title, label_str, is_modal, ok_handler, cancel_handler): self.dialog = self.xml.get_widget('input_dialog') label = self.xml.get_widget('label') @@ -1633,9 +1704,12 @@ class CommonInputDialog: self.dialog.destroy() class InputDialog(CommonInputDialog): - '''Class for Input dialog''' + """ + Class for Input dialog + """ + def __init__(self, title, label_str, input_str=None, is_modal=True, - ok_handler=None, cancel_handler=None): + ok_handler=None, cancel_handler=None): self.xml = gtkgui_helpers.get_glade('input_dialog.glade') CommonInputDialog.__init__(self, title, label_str, is_modal, ok_handler, cancel_handler) @@ -1651,9 +1725,12 @@ class InputDialog(CommonInputDialog): return self.input_entry.get_text().decode('utf-8') class InputDialogCheck(InputDialog): - '''Class for Input dialog''' + """ + Class for Input dialog + """ + def __init__(self, title, label_str, checktext='', input_str=None, - is_modal=True, ok_handler=None, cancel_handler=None): + is_modal=True, ok_handler=None, cancel_handler=None): self.xml = gtkgui_helpers.get_glade('input_dialog.glade') InputDialog.__init__(self, title, label_str, input_str=input_str, is_modal=is_modal, ok_handler=ok_handler, @@ -1683,7 +1760,9 @@ class InputDialogCheck(InputDialog): return self.input_entry.get_text().decode('utf-8') def is_checked(self): - ''' Get active state of the checkbutton ''' + """ + Get active state of the checkbutton + """ try: return self.checkbutton.get_active() except Exception: @@ -1691,7 +1770,10 @@ class InputDialogCheck(InputDialog): return False class ChangeNickDialog(InputDialogCheck): - '''Class for changing room nickname in case of conflict''' + """ + Class for changing room nickname in case of conflict + """ + def __init__(self, account, room_jid, title, prompt, check_text=None): InputDialogCheck.__init__(self, title, '', checktext=check_text, input_str='', is_modal=True, ok_handler=None, cancel_handler=None) @@ -1773,7 +1855,10 @@ class ChangeNickDialog(InputDialogCheck): self.room_queue.append((account, room_jid, prompt)) class InputTextDialog(CommonInputDialog): - '''Class for multilines Input dialog (more place than InputDialog)''' + """ + Class for multilines Input dialog (more place than InputDialog) + """ + def __init__(self, title, label_str, input_str=None, is_modal=True, ok_handler=None, cancel_handler=None): self.xml = gtkgui_helpers.get_glade('input_text_dialog.glade') @@ -1790,7 +1875,10 @@ class InputTextDialog(CommonInputDialog): return self.input_buffer.get_text(start_iter, end_iter).decode('utf-8') class DubbleInputDialog: - '''Class for Dubble Input dialog''' + """ + Class for Dubble Input dialog + """ + def __init__(self, title, label_str1, label_str2, input_str1=None, input_str2=None, is_modal=True, ok_handler=None, cancel_handler=None): self.xml = gtkgui_helpers.get_glade('dubbleinput_dialog.glade') @@ -1876,7 +1964,9 @@ class SubscriptionRequestWindow: self.window.destroy() def on_authorize_button_clicked(self, widget): - '''accept the request''' + """ + Accept the request + """ gajim.connections[self.account].send_authorization(self.jid) self.window.destroy() contact = gajim.contacts.get_contact(self.account, self.jid) @@ -1884,7 +1974,9 @@ class SubscriptionRequestWindow: AddNewContactWindow(self.account, self.jid, self.user_nick) def on_contact_info_activate(self, widget): - '''ask vcard''' + """ + Ask vcard + """ if self.jid in gajim.interface.instances[self.account]['infos']: gajim.interface.instances[self.account]['infos'][self.jid].window.present() else: @@ -1896,11 +1988,15 @@ class SubscriptionRequestWindow: get_widget('information_notebook').remove_page(0) def on_start_chat_activate(self, widget): - '''open chat''' + """ + Open chat + """ gajim.interface.new_chat_from_jid(self.account, self.jid) def on_deny_button_clicked(self, widget): - '''refuse the request''' + """ + Refuse the request + """ gajim.connections[self.account].refuse_authorization(self.jid) contact = gajim.contacts.get_contact(self.account, self.jid) if contact and _('Not in Roster') in contact.get_shown_groups(): @@ -1908,7 +2004,9 @@ class SubscriptionRequestWindow: self.window.destroy() def on_actions_button_clicked(self, widget): - '''popup action menu''' + """ + Popup action menu + """ menu = self.prepare_popup_menu() menu.show_all() gtkgui_helpers.popup_emoticons_under_button(menu, widget, self.window.window) @@ -1916,11 +2014,12 @@ class SubscriptionRequestWindow: class JoinGroupchatWindow: def __init__(self, account=None, room_jid='', nick='', password='', - automatic=False): - '''automatic is a dict like {'invities': []} - If automatic is not empty, this means room must be automaticaly configured - and when done, invities must be automatically invited''' - + automatic=False): + """ + Automatic is a dict like {'invities': []}. If automatic is not empty, + this means room must be automaticaly configured and when done, invities + must be automatically invited + """ if account: if room_jid != '' and room_jid in gajim.gc_connected[account] and\ gajim.gc_connected[account][room_jid]: @@ -2007,7 +2106,9 @@ class JoinGroupchatWindow: self.window.show_all() def on_join_groupchat_window_destroy(self, widget): - '''close window''' + """ + Close window + """ if self.account and 'join_gc' in gajim.interface.instances[self.account]: # remove us from open windows del gajim.interface.instances[self.account]['join_gc'] @@ -2039,7 +2140,9 @@ class JoinGroupchatWindow: self._room_jid_entry.set_text(room_jid) def on_cancel_button_clicked(self, widget): - '''When Cancel button is clicked''' + """ + When Cancel button is clicked + """ self.window.destroy() def on_bookmark_checkbutton_toggled(self, widget): @@ -2050,7 +2153,9 @@ class JoinGroupchatWindow: auto_join_checkbutton.set_sensitive(False) def on_join_button_clicked(self, widget): - '''When Join button is clicked''' + """ + When Join button is clicked + """ if not self.account: ErrorDialog(_('Invalid Account'), _('You have to choose an account from which you want to join the ' @@ -2138,7 +2243,9 @@ class SynchroniseSelectAccountDialog: self.window.destroy() def init_accounts(self): - '''initialize listStore with existing accounts''' + """ + Initialize listStore with existing accounts + """ model = self.accounts_treeview.get_model() model.clear() for remote_account in gajim.connections: @@ -2204,7 +2311,9 @@ class SynchroniseSelectContactsDialog: self.window.destroy() def init_contacts(self): - '''initialize listStore with existing accounts''' + """ + Initialize listStore with existing accounts + """ model = self.contacts_treeview.get_model() model.clear() @@ -2267,7 +2376,9 @@ class NewChatDialog(InputDialog): self.dialog.show_all() def new_chat_response(self, jid): - ''' called when ok button is clicked ''' + """ + Called when ok button is clicked + """ if gajim.connections[self.account].connected <= 1: #if offline or connecting ErrorDialog(_('Connection not available'), @@ -2433,10 +2544,10 @@ class PopupNotificationWindow: self.adjust_height_and_move_popup_notification_windows() class SingleMessageWindow: - '''SingleMessageWindow can send or show a received - singled message depending on action argument which can be 'send' - or 'receive'. - ''' + """ + SingleMessageWindow can send or show a received singled message depending on + action argument which can be 'send' or 'receive' + """ # Keep a reference on windows so garbage collector don't restroy them instances = [] def __init__(self, account, to='', action='', from_whom='', subject='', @@ -2847,9 +2958,12 @@ class XMLConsoleWindow: TRANSLATED_ACTION = {'add': _('add'), 'modify': _('modify'), 'remove': _('remove')} class RosterItemExchangeWindow: - ''' Windows used when someone send you a exchange contact suggestion ''' + """ + Windows used when someone send you a exchange contact suggestion + """ + def __init__(self, account, action, exchange_list, jid_from, - message_body=None): + message_body=None): self.account = account self.action = action self.exchange_list = exchange_list @@ -3067,8 +3181,10 @@ class RosterItemExchangeWindow: class PrivacyListWindow: - '''Window that is used for creating NEW or EDITING already there privacy - lists''' + """ + Window that is used for creating NEW or EDITING already there privacy lists + """ + def __init__(self, account, privacy_list_name, action): '''action is 'EDIT' or 'NEW' depending on if we create a new priv list or edit an already existing one''' @@ -3406,9 +3522,10 @@ class PrivacyListWindow: self.window.destroy() class PrivacyListsWindow: - '''Window that is the main window for Privacy Lists; - we can list there the privacy lists and ask to create a new one - or edit an already there one''' + """ + Window that is the main window for Privacy Lists; we can list there the + privacy lists and ask to create a new one or edit an already there one + """ def __init__(self, account): self.account = account self.privacy_lists_save = [] @@ -3565,9 +3682,10 @@ class InvitationReceivedDialog: class ProgressDialog: def __init__(self, title_text, during_text, messages_queue): - '''during text is what to show during the procedure, - messages_queue has the message to show - in the textview''' + """ + During text is what to show during the procedure, messages_queue has the + message to show in the textview + """ self.xml = gtkgui_helpers.get_glade('progress_dialog.glade') self.dialog = self.xml.get_widget('progress_dialog') self.label = self.xml.get_widget('label') @@ -3594,10 +3712,14 @@ class ProgressDialog: class SoundChooserDialog(FileChooserDialog): def __init__(self, path_to_snd_file='', on_response_ok=None, - on_response_cancel=None): - '''optionally accepts path_to_snd_file so it has that as selected''' + on_response_cancel=None): + """ + Optionally accepts path_to_snd_file so it has that as selected + """ def on_ok(widget, callback): - '''check if file exists and call callback''' + """ + Check if file exists and call callback + """ path_to_snd_file = self.get_filename() path_to_snd_file = gtkgui_helpers.decode_filechooser_file_paths( (path_to_snd_file,))[0] @@ -3633,8 +3755,10 @@ class SoundChooserDialog(FileChooserDialog): class ImageChooserDialog(FileChooserDialog): def __init__(self, path_to_file='', on_response_ok=None, - on_response_cancel=None): - '''optionally accepts path_to_snd_file so it has that as selected''' + on_response_cancel=None): + """ + Optionally accepts path_to_snd_file so it has that as selected + """ def on_ok(widget, callback): '''check if file exists and call callback''' path_to_file = self.get_filename() @@ -3725,8 +3849,10 @@ class AvatarChooserDialog(ImageChooserDialog): class AddSpecialNotificationDialog: def __init__(self, jid): - '''jid is the jid for which we want to add special notification - (sound and notification popups)''' + """ + jid is the jid for which we want to add special notification (sound and + notification popups) + """ self.xml = gtkgui_helpers.get_glade('add_special_notification_window.glade') self.window = self.xml.get_widget('add_special_notification_window') self.condition_combobox = self.xml.get_widget('condition_combobox') @@ -3846,7 +3972,9 @@ class AdvancedNotificationsWindow: self.window.show_all() def initiate_rule_state(self): - '''Set values for all widgets''' + """ + Set values for all widgets + """ if self.active_num < 0: return # event @@ -4237,8 +4365,10 @@ class TransformChatToMUC: # Keep a reference on windows so garbage collector don't restroy them instances = [] def __init__(self, account, jids, preselected=None): - '''This window is used to trasform a one-to-one chat to a MUC. - We do 2 things: first select the server and then make a guests list.''' + """ + This window is used to trasform a one-to-one chat to a MUC. We do 2 + things: first select the server and then make a guests list + """ self.instances.append(self) self.account = account @@ -4389,7 +4519,9 @@ class DataFormWindow(Dialog): self.destroy() class ESessionInfoWindow: - '''Class for displaying information about a XEP-0116 encrypted session''' + """ + Class for displaying information about a XEP-0116 encrypted session + """ def __init__(self, session): self.session = session @@ -4468,7 +4600,9 @@ class ESessionInfoWindow: YesNoDialog(pritext, sectext, on_response_yes=on_yes, on_response_no=on_no) class GPGInfoWindow: - '''Class for displaying information about a XEP-0116 encrypted session''' + """ + Class for displaying information about a XEP-0116 encrypted session + """ def __init__(self, control): xml = gtkgui_helpers.get_glade('esession_info_window.glade') security_image = xml.get_widget('security_image') From 1c137dd6c4253947ac50cc73c6370877c2877b25 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 25 Nov 2009 16:49:13 +0100 Subject: [PATCH 70/73] remove debug print --- src/common/zeroconf/client_zeroconf.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/common/zeroconf/client_zeroconf.py b/src/common/zeroconf/client_zeroconf.py index 23834be28..ab863709d 100644 --- a/src/common/zeroconf/client_zeroconf.py +++ b/src/common/zeroconf/client_zeroconf.py @@ -710,8 +710,6 @@ class ClientZeroconf: try: item = self.roster[to] except KeyError: - raise KeyError - print 'ret', to, self.roster.keys() # Contact offline return -1 From 351ddb471814eedf3756f645f1e76cb08dacca1d Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 25 Nov 2009 16:50:15 +0100 Subject: [PATCH 71/73] allow space in profile name when running with launch.sh. Fixes #5453 --- launch.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launch.sh b/launch.sh index bcb968553..0c73c667c 100755 --- a/launch.sh +++ b/launch.sh @@ -1,3 +1,3 @@ #!/bin/sh cd "$(dirname $0)/src" -exec python -OOt gajim.py $@ +exec python -OOt gajim.py "$@" From 98e27253b63cd04acbbefbe30ee02df98135950a Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 25 Nov 2009 16:50:49 +0100 Subject: [PATCH 72/73] prevent showing error message when we receive a PEP error message and really ignore it --- src/common/connection_handlers.py | 7 +++++-- src/common/pep.py | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 089fbd69b..2dbff985b 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -2733,8 +2733,11 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, con.RegisterHandler('message', self._messageCB) con.RegisterHandler('presence', self._presenceCB) con.RegisterHandler('presence', self._capsPresenceCB) - con.RegisterHandler('message', self._pubsubEventCB, - ns=common.xmpp.NS_PUBSUB_EVENT) + # We use makefirst so that this handler is called before _messageCB, and + # can prevent calling it when it's not needed. + # We also don't check for namespace, else it cannot stop _messageCB to be + # called + con.RegisterHandler('message', self._pubsubEventCB, makefirst=True) con.RegisterHandler('iq', self._vCardCB, 'result', common.xmpp.NS_VCARD) con.RegisterHandler('iq', self._rosterSetCB, 'set', diff --git a/src/common/pep.py b/src/common/pep.py index 34f3bda46..5ba476c0c 100644 --- a/src/common/pep.py +++ b/src/common/pep.py @@ -454,6 +454,8 @@ class ConnectionPEP(object): def _pubsubEventCB(self, xmpp_dispatcher, msg): ''' Called when we receive with pubsub event. ''' + if not msg.getTag('event'): + return if msg.getTag('error'): log.debug('PubsubEventCB received error stanza. Ignoring') raise xmpp.NodeProcessed From 7316d0076667bf0b8c20f97be46e2fe815632683 Mon Sep 17 00:00:00 2001 From: Alexander Cherniuk Date: Wed, 25 Nov 2009 19:52:56 +0200 Subject: [PATCH 73/73] New portion of doc-string refactoring --- src/disco.py | 388 +++++++++++++++++++++++++----------- src/features_window.py | 4 +- src/filetransfers_window.py | 85 +++++--- src/gajim-remote.py | 36 +++- src/gajim_themes_window.py | 31 ++- src/groups.py | 13 +- src/gtkgui_helpers.py | 196 ++++++++++++------ src/gui_interface.py | 83 +++++--- src/gui_menu_builder.py | 22 +- src/history_manager.py | 49 +++-- 10 files changed, 614 insertions(+), 293 deletions(-) diff --git a/src/disco.py b/src/disco.py index 0d3621f13..c38c7ae7f 100644 --- a/src/disco.py +++ b/src/disco.py @@ -124,16 +124,21 @@ _cat_to_descr = { class CacheDictionary: - '''A dictionary that keeps items around for only a specific time. - Lifetime is in minutes. Getrefresh specifies whether to refresh when - an item is merely accessed instead of set aswell.''' + """ + A dictionary that keeps items around for only a specific time. Lifetime is + in minutes. Getrefresh specifies whether to refresh when an item is merely + accessed instead of set aswell + """ + def __init__(self, lifetime, getrefresh = True): self.lifetime = lifetime * 1000 * 60 self.getrefresh = getrefresh self.cache = {} class CacheItem: - '''An object to store cache items and their timeouts.''' + """ + An object to store cache items and their timeouts + """ def __init__(self, value): self.value = value self.source = None @@ -149,13 +154,17 @@ class CacheDictionary: del self.cache[key] def _expire_timeout(self, key): - '''The timeout has expired, remove the object.''' + """ + The timeout has expired, remove the object + """ if key in self.cache: del self.cache[key] return False def _refresh_timeout(self, key): - '''The object was accessed, refresh the timeout.''' + """ + The object was accessed, refresh the timeout + """ item = self.cache[key] if item.source: gobject.source_remove(item.source) @@ -187,20 +196,25 @@ class CacheDictionary: _icon_cache = CacheDictionary(15) def get_agent_address(jid, node = None): - '''Returns an agent's address for displaying in the GUI.''' + """ + Get an agent's address for displaying in the GUI + """ if node: return '%s@%s' % (node, str(jid)) else: return str(jid) class Closure(object): - '''A weak reference to a callback with arguments as an object. + """ + A weak reference to a callback with arguments as an object Weak references to methods immediatly die, even if the object is still alive. Besides a handy way to store a callback, this provides a workaround that keeps a reference to the object instead. - Userargs and removeargs must be tuples.''' + Userargs and removeargs must be tuples. + """ + def __init__(self, cb, userargs = (), remove = None, removeargs = ()): self.userargs = userargs self.remove = remove @@ -229,8 +243,11 @@ class Closure(object): class ServicesCache: - '''Class that caches our query results. Each connection will have it's own - ServiceCache instance.''' + """ + Class that caches our query results. Each connection will have it's own + ServiceCache instance + """ + def __init__(self, account): self.account = account self._items = CacheDictionary(0, getrefresh = False) @@ -256,7 +273,9 @@ class ServicesCache: del self._cbs[cbkey] def get_icon(self, identities = []): - '''Return the icon for an agent.''' + """ + Return the icon for an agent + """ # Grab the first identity with an icon for identity in identities: try: @@ -284,7 +303,9 @@ class ServicesCache: return pix def get_browser(self, identities=[], features=[]): - '''Return the browser class for an agent.''' + """ + Return the browser class for an agent + """ # First pass, we try to find a ToplevelAgentBrowser for identity in identities: try: @@ -316,7 +337,9 @@ class ServicesCache: return None def get_info(self, jid, node, cb, force = False, nofetch = False, args = ()): - '''Get info for an agent.''' + """ + Get info for an agent + """ addr = get_agent_address(jid, node) # Check the cache if addr in self._info: @@ -338,7 +361,9 @@ class ServicesCache: gajim.connections[self.account].discoverInfo(jid, node) def get_items(self, jid, node, cb, force = False, nofetch = False, args = ()): - '''Get a list of items in an agent.''' + """ + Get a list of items in an agent + """ addr = get_agent_address(jid, node) # Check the cache if addr in self._items: @@ -360,7 +385,9 @@ class ServicesCache: gajim.connections[self.account].discoverItems(jid, node) def agent_info(self, jid, node, identities, features, data): - '''Callback for when we receive an agent's info.''' + """ + Callback for when we receive an agent's info + """ addr = get_agent_address(jid, node) # Store in cache @@ -376,7 +403,9 @@ class ServicesCache: del self._cbs[cbkey] def agent_items(self, jid, node, items): - '''Callback for when we receive an agent's items.''' + """ + Callback for when we receive an agent's items + """ addr = get_agent_address(jid, node) # Store in cache @@ -392,8 +421,10 @@ class ServicesCache: del self._cbs[cbkey] def agent_info_error(self, jid): - '''Callback for when a query fails. (even after the browse and agents - namespaces)''' + """ + Callback for when a query fails. Even after the browse and agents + namespaces + """ addr = get_agent_address(jid) # Call callbacks @@ -406,8 +437,10 @@ class ServicesCache: del self._cbs[cbkey] def agent_items_error(self, jid): - '''Callback for when a query fails. (even after the browse and agents - namespaces)''' + """ + Callback for when a query fails. Even after the browse and agents + namespaces + """ addr = get_agent_address(jid) # Call callbacks @@ -421,7 +454,10 @@ class ServicesCache: # object is needed so that @property works class ServiceDiscoveryWindow(object): - '''Class that represents the Services Discovery window.''' + """ + Class that represents the Services Discovery window + """ + def __init__(self, account, jid = '', node = '', address_entry = False, parent = None): self.account = account @@ -510,8 +546,10 @@ _('Without a connection, you can not browse available services')) self.browser.account = value def _initial_state(self): - '''Set some initial state on the window. Separated in a method because - it's handy to use within browser's cleanup method.''' + """ + Set some initial state on the window. Separated in a method because it's + handy to use within browser's cleanup method + """ self.progressbar.hide() title_text = _('Service Discovery using account %s') % self.account self.window.set_title(title_text) @@ -550,7 +588,9 @@ _('Without a connection, you can not browse available services')) self.banner.set_markup(markup) def paint_banner(self): - '''Repaint the banner with theme color''' + """ + Repaint the banner with theme color + """ theme = gajim.config.get('roster_theme') bgcolor = gajim.config.get_per('themes', theme, 'bannerbgcolor') textcolor = gajim.config.get_per('themes', theme, 'bannertextcolor') @@ -584,10 +624,11 @@ _('Without a connection, you can not browse available services')) self._on_style_set_event, set_fg, set_bg) def _on_style_set_event(self, widget, style, *opts): - ''' set style of widget from style class *.Frame.Eventbox + """ + Set style of widget from style class *.Frame.Eventbox opts[0] == True -> set fg color - opts[1] == True -> set bg color ''' - + opts[1] == True -> set bg color + """ self.disconnect_style_event() if opts[1]: bg_color = widget.style.bg[gtk.STATE_SELECTED] @@ -599,9 +640,11 @@ _('Without a connection, you can not browse available services')) self.connect_style_event(opts[0], opts[1]) def destroy(self, chain = False): - '''Close the browser. This can optionally close its children and - propagate to the parent. This should happen on actions like register, - or join to kill off the entire browser chain.''' + """ + Close the browser. This can optionally close its children and propagate + to the parent. This should happen on actions like register, or join to + kill off the entire browser chain + """ if self.dying: return self.dying = True @@ -632,7 +675,9 @@ _('Without a connection, you can not browse available services')) self.cache.cleanup() def travel(self, jid, node): - '''Travel to an agent within the current services window.''' + """ + Travel to an agent within the current services window + """ if self.browser: self.browser.cleanup() self.browser = None @@ -649,7 +694,9 @@ _('Without a connection, you can not browse available services')) self.cache.get_info(jid, node, self._travel) def _travel(self, jid, node, identities, features, data): - '''Continuation of travel.''' + """ + Continuation of travel + """ if self.dying or jid != self.jid or node != self.node: return if not identities: @@ -671,7 +718,9 @@ _('This type of service does not contain any items to browse.')) self.browser.browse() def open(self, jid, node): - '''Open an agent. By default, this happens in a new window.''' + """ + Open an agent. By default, this happens in a new window + """ try: win = gajim.interface.instances[self.account]['disco']\ [get_agent_address(jid, node)] @@ -737,10 +786,13 @@ _('This type of service does not contain any items to browse.')) class AgentBrowser: - '''Class that deals with browsing agents and appearance of the browser - window. This class and subclasses should basically be treated as "part" - of the ServiceDiscoveryWindow class, but had to be separated because this part - is dynamic.''' + """ + Class that deals with browsing agents and appearance of the browser window. + This class and subclasses should basically be treated as "part" of the + ServiceDiscoveryWindow class, but had to be separated because this part is + dynamic + """ + def __init__(self, account, jid, node): self.account = account self.jid = jid @@ -751,20 +803,26 @@ class AgentBrowser: self.active = False def _get_agent_address(self): - '''Returns the agent's address for displaying in the GUI.''' + """ + Get the agent's address for displaying in the GUI + """ return get_agent_address(self.jid, self.node) def _set_initial_title(self): - '''Set the initial window title based on agent address.''' + """ + Set the initial window title based on agent address + """ self.window.window.set_title(_('Browsing %(address)s using account ' '%(account)s') % {'address': self._get_agent_address(), 'account': self.account}) self.window._set_window_banner_text(self._get_agent_address()) def _create_treemodel(self): - '''Create the treemodel for the services treeview. When subclassing, - note that the first two columns should ALWAYS be of type string and - contain the JID and node of the item respectively.''' + """ + Create the treemodel for the services treeview. When subclassing, note + that the first two columns should ALWAYS be of type string and contain + the JID and node of the item respectively + """ # JID, node, name, address self.model = gtk.ListStore(str, str, str, str) self.model.set_sort_column_id(3, gtk.SORT_ASCENDING) @@ -792,8 +850,10 @@ class AgentBrowser: self.window.services_treeview.set_headers_visible(False) def _add_actions(self): - '''Add the action buttons to the buttonbox for actions the browser can - perform.''' + """ + Add the action buttons to the buttonbox for actions the browser can + perform + """ self.browse_button = gtk.Button() image = gtk.image_new_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_BUTTON) label = gtk.Label(_('_Browse')) @@ -807,13 +867,17 @@ class AgentBrowser: self.browse_button.show_all() def _clean_actions(self): - '''Remove the action buttons specific to this browser.''' + """ + Remove the action buttons specific to this browser + """ if self.browse_button: self.browse_button.destroy() self.browse_button = None def _set_title(self, jid, node, identities, features, data): - '''Set the window title based on agent info.''' + """ + Set the window title based on agent info + """ # Set the banner and window title if 'name' in identities[0]: name = identities[0]['name'] @@ -830,8 +894,10 @@ class AgentBrowser: pass def prepare_window(self, window): - '''Prepare the service discovery window. Called when a browser is hooked - up with a ServiceDiscoveryWindow instance.''' + """ + Prepare the service discovery window. Called when a browser is hooked up + with a ServiceDiscoveryWindow instance + """ self.window = window self.cache = window.cache @@ -852,7 +918,9 @@ class AgentBrowser: self.cache.get_info(self.jid, self.node, self._set_title) def cleanup(self): - '''Cleanup when the window intends to switch browsers.''' + """ + Cleanup when the window intends to switch browsers + """ self.active = False self._clean_actions() @@ -862,12 +930,16 @@ class AgentBrowser: self.window._initial_state() def update_theme(self): - '''Called when the default theme is changed.''' + """ + Called when the default theme is changed + """ pass def on_browse_button_clicked(self, widget = None): - '''When we want to browse an agent: - Open a new services window with a browser for the agent type.''' + """ + When we want to browse an agent: open a new services window with a + browser for the agent type + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if not iter_: return @@ -877,8 +949,9 @@ class AgentBrowser: self.window.open(jid, node) def update_actions(self): - '''When we select a row: - activate action buttons based on the agent's info.''' + """ + When we select a row: activate action buttons based on the agent's info + """ if self.browse_button: self.browse_button.set_sensitive(False) model, iter_ = self.window.services_treeview.get_selection().get_selected() @@ -890,7 +963,9 @@ class AgentBrowser: self.cache.get_info(jid, node, self._update_actions, nofetch = True) def _update_actions(self, jid, node, identities, features, data): - '''Continuation of update_actions.''' + """ + Continuation of update_actions + """ if not identities or not self.browse_button: return klass = self.cache.get_browser(identities, features) @@ -898,8 +973,10 @@ class AgentBrowser: self.browse_button.set_sensitive(True) def default_action(self): - '''When we double-click a row: - perform the default action on the selected item.''' + """ + When we double-click a row: perform the default action on the selected + item + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if not iter_: return @@ -909,7 +986,9 @@ class AgentBrowser: self.cache.get_info(jid, node, self._default_action, nofetch = True) def _default_action(self, jid, node, identities, features, data): - '''Continuation of default_action.''' + """ + Continuation of default_action + """ if self.cache.get_browser(identities, features): # Browse if we can self.on_browse_button_clicked() @@ -917,7 +996,9 @@ class AgentBrowser: return False def browse(self, force = False): - '''Fill the treeview with agents, fetching the info if necessary.''' + """ + Fill the treeview with agents, fetching the info if necessary + """ self.model.clear() self._total_items = self._progress = 0 self.window.progressbar.show() @@ -926,15 +1007,19 @@ class AgentBrowser: force = force, args = (force,)) def _pulse_timeout_cb(self, *args): - '''Simple callback to keep the progressbar pulsing.''' + """ + Simple callback to keep the progressbar pulsing + """ if not self.active: return False self.window.progressbar.pulse() return True def _find_item(self, jid, node): - '''Check if an item is already in the treeview. Return an iter to it - if so, None otherwise.''' + """ + Check if an item is already in the treeview. Return an iter to it if so, + None otherwise + """ iter_ = self.model.get_iter_root() while iter_: cjid = self.model.get_value(iter_, 0).decode('utf-8') @@ -947,7 +1032,9 @@ class AgentBrowser: return None def _agent_items(self, jid, node, items, force): - '''Callback for when we receive a list of agent items.''' + """ + Callback for when we receive a list of agent items + """ self.model.clear() self._total_items = 0 gobject.source_remove(self._pulse_timeout) @@ -973,7 +1060,9 @@ _('This service does not contain any items to browse.')) self.window.services_treeview.set_model(self.model) def _agent_info(self, jid, node, identities, features, data): - '''Callback for when we receive info about an agent's item.''' + """ + Callback for when we receive info about an agent's item + """ iter_ = self._find_item(jid, node) if not iter_: # Not in the treeview, stop @@ -987,21 +1076,27 @@ _('This service does not contain any items to browse.')) self.update_actions() def _add_item(self, jid, node, parent_node, item, force): - '''Called when an item should be added to the model. The result of a - disco#items query.''' + """ + Called when an item should be added to the model. The result of a + disco#items query + """ self.model.append((jid, node, item.get('name', ''), get_agent_address(jid, node))) self.cache.get_info(jid, node, self._agent_info, force = force) def _update_item(self, iter_, jid, node, item): - '''Called when an item should be updated in the model. The result of a - disco#items query. (seldom)''' + """ + Called when an item should be updated in the model. The result of a + disco#items query + """ if 'name' in item: self.model[iter_][2] = item['name'] def _update_info(self, iter_, jid, node, identities, features, data): - '''Called when an item should be updated in the model with further info. - The result of a disco#info query.''' + """ + Called when an item should be updated in the model with further info. + The result of a disco#info query + """ name = identities[0].get('name', '') if name: self.model[iter_][2] = name @@ -1012,8 +1107,11 @@ _('This service does not contain any items to browse.')) class ToplevelAgentBrowser(AgentBrowser): - '''This browser is used at the top level of a jabber server to browse - services such as transports, conference servers, etc.''' + """ + This browser is used at the top level of a jabber server to browse services + such as transports, conference servers, etc + """ + def __init__(self, *args): AgentBrowser.__init__(self, *args) self._progressbar_sourceid = None @@ -1029,7 +1127,9 @@ class ToplevelAgentBrowser(AgentBrowser): self._scroll_signal = None def _pixbuf_renderer_data_func(self, col, cell, model, iter_): - '''Callback for setting the pixbuf renderer's properties.''' + """ + Callback for setting the pixbuf renderer's properties + """ jid = model.get_value(iter_, 0) if jid: pix = model.get_value(iter_, 2) @@ -1039,7 +1139,9 @@ class ToplevelAgentBrowser(AgentBrowser): cell.set_property('visible', False) def _text_renderer_data_func(self, col, cell, model, iter_): - '''Callback for setting the text renderer's properties.''' + """ + Callback for setting the text renderer's properties + """ jid = model.get_value(iter_, 0) markup = model.get_value(iter_, 3) state = model.get_value(iter_, 4) @@ -1060,7 +1162,9 @@ class ToplevelAgentBrowser(AgentBrowser): cell.set_property('foreground_set', False) def _treemodel_sort_func(self, model, iter1, iter2): - '''Sort function for our treemodel.''' + """ + Sort function for our treemode + """ # Compare state statecmp = cmp(model.get_value(iter1, 4), model.get_value(iter2, 4)) if statecmp == 0: @@ -1120,8 +1224,9 @@ class ToplevelAgentBrowser(AgentBrowser): self._show_tooltip, state) def on_treeview_event_hide_tooltip(self, widget, event): - ''' This happens on scroll_event, key_press_event - and button_press_event ''' + """ + This happens on scroll_event, key_press_event and button_press_event + """ self.tooltip.hide_tooltip() def _create_treemodel(self): @@ -1236,8 +1341,9 @@ class ToplevelAgentBrowser(AgentBrowser): AgentBrowser._clean_actions(self) def on_search_button_clicked(self, widget = None): - '''When we want to search something: - open search window''' + """ + When we want to search something: open search window + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if not iter_: return @@ -1261,8 +1367,9 @@ class ToplevelAgentBrowser(AgentBrowser): self.window.services_treeview.queue_draw() def on_execute_button_clicked(self, widget=None): - '''When we want to execute a command: - open adhoc command window''' + """ + When we want to execute a command: open adhoc command window + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if not iter_: return @@ -1271,9 +1378,10 @@ class ToplevelAgentBrowser(AgentBrowser): adhoc_commands.CommandWindow(self.account, service, commandnode=node) def on_register_button_clicked(self, widget = None): - '''When we want to register an agent: - request information about registering with the agent and close the - window.''' + """ + When we want to register an agent: request information about registering + with the agent and close the window + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if not iter_: return @@ -1283,8 +1391,10 @@ class ToplevelAgentBrowser(AgentBrowser): self.window.destroy(chain = True) def on_join_button_clicked(self, widget): - '''When we want to join an IRC room or create a new MUC room: - Opens the join_groupchat_window.''' + """ + When we want to join an IRC room or create a new MUC room: Opens the + join_groupchat_window + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if not iter_: return @@ -1375,7 +1485,9 @@ class ToplevelAgentBrowser(AgentBrowser): AgentBrowser.browse(self, force = force) def _expand_all(self): - '''Expand all items in the treeview''' + """ + Expand all items in the treeview + """ # GTK apparently screws up here occasionally. :/ #def expand_all(*args): # self.window.services_treeview.expand_all() @@ -1386,7 +1498,9 @@ class ToplevelAgentBrowser(AgentBrowser): self.window.services_treeview.expand_all() def _update_progressbar(self): - '''Update the progressbar.''' + """ + Update the progressbar + """ # Refresh this every update if self._progressbar_sourceid: gobject.source_remove(self._progressbar_sourceid) @@ -1408,13 +1522,17 @@ class ToplevelAgentBrowser(AgentBrowser): self.window.progressbar.set_fraction(fraction) def _hide_progressbar_cb(self, *args): - '''Simple callback to hide the progressbar a second after we finish.''' + """ + Simple callback to hide the progressbar a second after we finish + """ if self.active: self.window.progressbar.hide() return False def _friendly_category(self, category, type_=None): - '''Get the friendly category name and priority.''' + """ + Get the friendly category name and priority + """ cat = None if type_: # Try type-specific override @@ -1430,12 +1548,16 @@ class ToplevelAgentBrowser(AgentBrowser): return cat, prio def _create_category(self, cat, type_=None): - '''Creates a category row.''' + """ + Creates a category row + """ cat, prio = self._friendly_category(cat, type_) return self.model.append(None, ('', '', None, cat, prio)) def _find_category(self, cat, type_=None): - '''Looks up a category row and returns the iterator to it, or None.''' + """ + Looks up a category row and returns the iterator to it, or None + """ cat = self._friendly_category(cat, type_)[0] iter_ = self.model.get_iter_root() while iter_: @@ -1670,8 +1792,10 @@ class MucBrowser(AgentBrowser): _('You can manage your bookmarks via Actions menu in your roster.')) def on_join_button_clicked(self, *args): - '''When we want to join a conference: - Ask specific informations about the selected agent and close the window''' + """ + When we want to join a conference: ask specific informations about the + selected agent and close the window + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if not iter_: return @@ -1697,18 +1821,24 @@ class MucBrowser(AgentBrowser): self.on_join_button_clicked() def _start_info_query(self): - '''Idle callback to start checking for visible rows.''' + """ + Idle callback to start checking for visible rows + """ self._fetch_source = None self._query_visible() return False def on_scroll(self, *args): - '''Scrollwindow callback to trigger new queries on scolling.''' + """ + Scrollwindow callback to trigger new queries on scolling + """ # This apparently happens when inactive sometimes self._query_visible() def _query_visible(self): - '''Query the next visible row for info.''' + """ + Query the next visible row for info + """ if self._fetch_source: # We're already fetching return @@ -1751,9 +1881,10 @@ class MucBrowser(AgentBrowser): self._fetch_source = None def _channel_altinfo(self, jid, node, items, name = None): - '''Callback for the alternate disco#items query. We try to atleast get - the amount of users in the room if the service does not support MUC - dataforms.''' + """ + Callback for the alternate disco#items query. We try to atleast get the + amount of users in the room if the service does not support MUC dataforms + """ if items == 0: # The server returned an error self._broken += 1 @@ -1816,15 +1947,20 @@ class MucBrowser(AgentBrowser): self.cache.get_items(jid, node, self._channel_altinfo) def PubSubBrowser(account, jid, node): - ''' Returns an AgentBrowser subclass that will display service discovery - for particular pubsub service. Different pubsub services may need to - present different data during browsing. ''' + """ + Return an AgentBrowser subclass that will display service discovery for + particular pubsub service. Different pubsub services may need to present + different data during browsing + """ # for now, only discussion groups are supported... # TODO: check if it has appropriate features to be such kind of service return DiscussionGroupsBrowser(account, jid, node) class DiscussionGroupsBrowser(AgentBrowser): - ''' For browsing pubsub-based discussion groups service. ''' + """ + For browsing pubsub-based discussion groups service + """ + def __init__(self, account, jid, node): AgentBrowser.__init__(self, account, jid, node) @@ -1840,7 +1976,9 @@ class DiscussionGroupsBrowser(AgentBrowser): gajim.connections[account].send_pb_subscription_query(jid, self._subscriptionsCB) def _create_treemodel(self): - ''' Create treemodel for the window. ''' + """ + Create treemodel for the window + """ # JID, node, name (with description) - pango markup, dont have info?, subscribed? self.model = gtk.TreeStore(str, str, str, bool, bool) # sort by name @@ -1891,8 +2029,10 @@ class DiscussionGroupsBrowser(AgentBrowser): return self.in_list def _add_item(self, jid, node, parent_node, item, force): - ''' Called when we got basic information about new node from query. - Show the item. ''' + """ + Called when we got basic information about new node from query. Show the + item + """ name = item.get('name', '') if self.subscriptions is not None: @@ -1962,8 +2102,10 @@ class DiscussionGroupsBrowser(AgentBrowser): self.unsubscribe_button = None def update_actions(self): - '''Called when user selected a row. Make subscribe/unsubscribe buttons - sensitive appropriatelly.''' + """ + Called when user selected a row. Make subscribe/unsubscribe buttons + sensitive appropriatelly + """ # we have nothing to do if we don't have buttons... if self.subscribe_button is None: return @@ -1980,7 +2122,9 @@ class DiscussionGroupsBrowser(AgentBrowser): self.unsubscribe_button.set_sensitive(subscribed) def on_post_button_clicked(self, widget): - '''Called when 'post' button is pressed. Open window to create post''' + """ + Called when 'post' button is pressed. Open window to create post + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if iter_ is None: return @@ -1989,7 +2133,9 @@ class DiscussionGroupsBrowser(AgentBrowser): groups.GroupsPostWindow(self.account, self.jid, groupnode) def on_subscribe_button_clicked(self, widget): - '''Called when 'subscribe' button is pressed. Send subscribtion request.''' + """ + Called when 'subscribe' button is pressed. Send subscribtion request + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if iter_ is None: return @@ -1998,7 +2144,9 @@ class DiscussionGroupsBrowser(AgentBrowser): gajim.connections[self.account].send_pb_subscribe(self.jid, groupnode, self._subscribeCB, groupnode) def on_unsubscribe_button_clicked(self, widget): - '''Called when 'unsubscribe' button is pressed. Send unsubscription request.''' + """ + Called when 'unsubscribe' button is pressed. Send unsubscription request + """ model, iter_ = self.window.services_treeview.get_selection().get_selected() if iter_ is None: return @@ -2007,8 +2155,10 @@ class DiscussionGroupsBrowser(AgentBrowser): gajim.connections[self.account].send_pb_unsubscribe(self.jid, groupnode, self._unsubscribeCB, groupnode) def _subscriptionsCB(self, conn, request): - ''' We got the subscribed groups list stanza. Now, if we already - have items on the list, we should actualize them. ''' + """ + We got the subscribed groups list stanza. Now, if we already have items + on the list, we should actualize them + """ try: subscriptions = request.getTag('pubsub').getTag('subscriptions') except Exception: @@ -2036,7 +2186,9 @@ class DiscussionGroupsBrowser(AgentBrowser): raise xmpp.NodeProcessed def _subscribeCB(self, conn, request, groupnode): - '''We have just subscribed to a node. Update UI''' + """ + We have just subscribed to a node. Update UI + """ self.subscriptions.add(groupnode) model = self.window.services_treeview.get_model() @@ -2050,7 +2202,9 @@ class DiscussionGroupsBrowser(AgentBrowser): raise xmpp.NodeProcessed def _unsubscribeCB(self, conn, request, groupnode): - '''We have just unsubscribed from a node. Update UI''' + """ + We have just unsubscribed from a node. Update UI + """ self.subscriptions.remove(groupnode) model = self.window.services_treeview.get_model() diff --git a/src/features_window.py b/src/features_window.py index c89b597cb..d784fc50a 100644 --- a/src/features_window.py +++ b/src/features_window.py @@ -33,7 +33,9 @@ from common import helpers from common import kwalletbinding class FeaturesWindow: - '''Class for features window''' + """ + Class for features window + """ def __init__(self): self.xml = gtkgui_helpers.get_glade('features_window.glade') diff --git a/src/filetransfers_window.py b/src/filetransfers_window.py index cae4549e6..d3d23a156 100644 --- a/src/filetransfers_window.py +++ b/src/filetransfers_window.py @@ -134,7 +134,9 @@ class FileTransfersWindow: self.xml.signal_autoconnect(self) def find_transfer_by_jid(self, account, jid): - ''' find all transfers with peer 'jid' that belong to 'account' ''' + """ + Find all transfers with peer 'jid' that belong to 'account' + """ active_transfers = [[],[]] # ['senders', 'receivers'] # 'account' is the sender @@ -155,7 +157,9 @@ class FileTransfersWindow: return active_transfers def show_completed(self, jid, file_props): - ''' show a dialog saying that file (file_props) has been transferred''' + """ + Show a dialog saying that file (file_props) has been transferred + """ def on_open(widget, file_props): dialog.destroy() if 'file-name' not in file_props: @@ -207,14 +211,16 @@ class FileTransfersWindow: dialog.show_all() def show_request_error(self, file_props): - ''' show error dialog to the recipient saying that transfer - has been canceled''' + """ + Show error dialog to the recipient saying that transfer has been canceled + """ dialogs.InformationDialog(_('File transfer cancelled'), _('Connection with peer cannot be established.')) self.tree.get_selection().unselect_all() def show_send_error(self, file_props): - ''' show error dialog to the sender saying that transfer - has been canceled''' + """ + Show error dialog to the sender saying that transfer has been canceled + """ dialogs.InformationDialog(_('File transfer cancelled'), _('Connection with peer cannot be established.')) self.tree.get_selection().unselect_all() @@ -273,7 +279,9 @@ _('Connection with peer cannot be established.')) desc_hbox.show_all() def send_file(self, account, contact, file_path, file_desc=''): - ''' start the real transfer(upload) of the file ''' + """ + Start the real transfer(upload) of the file + """ if gtkgui_helpers.file_is_locked(file_path): pritext = _('Gajim cannot access this file') sextext = _('This file is being used by another process.') @@ -303,8 +311,10 @@ _('Connection with peer cannot be established.')) gajim.connections[account].send_file_approval(file_props) def show_file_request(self, account, contact, file_props): - ''' show dialog asking for comfirmation and store location of new - file requested by a contact''' + """ + Show dialog asking for comfirmation and store location of new file + requested by a contact + """ if file_props is None or 'name' not in file_props: return sec_text = '\t' + _('File: %s') % gobject.markup_escape_text( @@ -394,7 +404,9 @@ _('Connection with peer cannot be established.')) self.window.render_icon(self.icons[ident], gtk.ICON_SIZE_MENU)) def set_status(self, typ, sid, status): - ''' change the status of a transfer to state 'status' ''' + """ + Change the status of a transfer to state 'status' + """ iter_ = self.get_iter_by_sid(typ, sid) if iter_ is None: return @@ -409,8 +421,10 @@ _('Connection with peer cannot be established.')) self.select_func(path) def _format_percent(self, percent): - ''' add extra spaces from both sides of the percent, so that - progress string has always a fixed size''' + """ + Add extra spaces from both sides of the percent, so that progress string + has always a fixed size + """ _str = ' ' if percent != 100.: _str += ' ' @@ -481,7 +495,9 @@ _('Connection with peer cannot be established.')) del(file_props) def set_progress(self, typ, sid, transfered_size, iter_ = None): - ''' change the progress of a transfer with new transfered size''' + """ + Change the progress of a transfer with new transfered size + """ if sid not in self.files_props[typ]: return file_props = self.files_props[typ][sid] @@ -546,8 +562,10 @@ _('Connection with peer cannot be established.')) self.select_func(path) def get_iter_by_sid(self, typ, sid): - '''returns iter to the row, which holds file transfer, identified by the - session id''' + """ + Return iter to the row, which holds file transfer, identified by the + session id + """ iter_ = self.model.get_iter_root() while iter_: if typ + sid == self.model[iter_][C_SID].decode('utf-8'): @@ -555,9 +573,10 @@ _('Connection with peer cannot be established.')) iter_ = self.model.iter_next(iter_) def get_send_file_props(self, account, contact, file_path, file_name, - file_desc=''): - ''' create new file_props dict and set initial file transfer - properties in it''' + file_desc=''): + """ + Create new file_props dict and set initial file transfer properties in it + """ file_props = {'file-name' : file_path, 'name' : file_name, 'type' : 's', 'desc' : file_desc} if os.path.isfile(file_path): @@ -582,7 +601,9 @@ _('Connection with peer cannot be established.')) return file_props def add_transfer(self, account, contact, file_props): - ''' add new transfer to FT window and show the FT window ''' + """ + Add new transfer to FT window and show the FT window + """ self.on_transfers_list_leave_notify_event(None) if file_props is None: return @@ -686,15 +707,19 @@ _('Connection with peer cannot be established.')) return True def set_cleanup_sensitivity(self): - ''' check if there are transfer rows and set cleanup_button - sensitive, or insensitive if model is empty''' + """ + Check if there are transfer rows and set cleanup_button sensitive, or + insensitive if model is empty + """ if len(self.model) == 0: self.cleanup_button.set_sensitive(False) else: self.cleanup_button.set_sensitive(True) def set_all_insensitive(self): - ''' make all buttons/menuitems insensitive ''' + """ + Make all buttons/menuitems insensitive + """ self.pause_button.set_sensitive(False) self.pause_menuitem.set_sensitive(False) self.continue_menuitem.set_sensitive(False) @@ -705,8 +730,10 @@ _('Connection with peer cannot be established.')) self.set_cleanup_sensitivity() def set_buttons_sensitive(self, path, is_row_selected): - ''' make buttons/menuitems sensitive as appropriate to - the state of file transfer located at path 'path' ''' + """ + Make buttons/menuitems sensitive as appropriate to the state of file + transfer located at path 'path' + """ if path is None: self.set_all_insensitive() return @@ -743,8 +770,9 @@ _('Connection with peer cannot be established.')) return True def selection_changed(self, args): - ''' selection has changed - change the sensitivity of the - buttons/menuitems''' + """ + Selection has changed - change the sensitivity of the buttons/menuitems + """ selection = args selected = selection.get_selected_rows() if selected[1] != []: @@ -881,7 +909,9 @@ _('Connection with peer cannot be established.')) event_button, event.time) def on_transfers_list_key_press_event(self, widget, event): - '''when a key is pressed in the treeviews''' + """ + When a key is pressed in the treeviews + """ self.tooltip.hide_tooltip() iter_ = None try: @@ -963,5 +993,4 @@ _('Connection with peer cannot be established.')) if event.keyval == gtk.keysyms.Escape: # ESCAPE self.window.hide() - # vim: se ts=3: diff --git a/src/gajim-remote.py b/src/gajim-remote.py index 8926cf341..09db604d0 100644 --- a/src/gajim-remote.py +++ b/src/gajim-remote.py @@ -60,7 +60,6 @@ INTERFACE = 'org.gajim.dbus.RemoteInterface' SERVICE = 'org.gajim.dbus' BASENAME = 'gajim-remote' - class GajimRemote: def __init__(self): @@ -327,7 +326,9 @@ class GajimRemote: self.print_result(res) def print_result(self, res): - ''' Print retrieved result to the output ''' + """ + Print retrieved result to the output + """ if res is not None: if self.command in ('open_chat', 'send_chat_message', 'send_single_message', 'start_chat'): if self.command in ('send_message', 'send_single_message'): @@ -382,8 +383,9 @@ class GajimRemote: return test def init_connection(self): - ''' create the onnection to the session dbus, - or exit if it is not possible ''' + """ + Create the onnection to the session dbus, or exit if it is not possible + """ try: self.sbus = dbus.SessionBus() except Exception: @@ -398,8 +400,10 @@ class GajimRemote: self.method = interface.__getattr__(self.command) def make_arguments_row(self, args): - ''' return arguments list. Mandatory arguments are enclosed with: - '<', '>', optional arguments - with '[', ']' ''' + """ + Return arguments list. Mandatory arguments are enclosed with: + '<', '>', optional arguments - with '[', ']' + """ s = '' for arg in args: if arg[2]: @@ -409,7 +413,9 @@ class GajimRemote: return s def help_on_command(self, command): - ''' return help message for a given command ''' + """ + Return help message for a given command + """ if command in self.commands: command_props = self.commands[command] arguments_str = self.make_arguments_row(command_props[1]) @@ -424,7 +430,9 @@ class GajimRemote: send_error(_('%s not found') % command) def compose_help(self): - ''' print usage, and list available commands ''' + """ + Print usage, and list available commands + """ s = _('Usage: %s command [arguments]\nCommand is one of:\n' ) % BASENAME for command in sorted(self.commands): s += ' ' + command @@ -437,7 +445,9 @@ class GajimRemote: return s def print_info(self, level, prop_dict, encode_return = False): - ''' return formated string from data structure ''' + """ + Return formated string from data structure + """ if prop_dict is None or not isinstance(prop_dict, (dict, list, tuple)): return '' ret_str = '' @@ -486,7 +496,9 @@ class GajimRemote: return ret_str def check_arguments(self): - ''' Make check if all necessary arguments are given ''' + """ + Make check if all necessary arguments are given + """ argv_len = self.argv_len - 2 args = self.commands[self.command][1] if len(args) < argv_len: @@ -559,7 +571,9 @@ class GajimRemote: sys.exit(0) def call_remote_method(self): - ''' calls self.method with arguments from sys.argv[2:] ''' + """ + Calls self.method with arguments from sys.argv[2:] + """ args = [i.decode(PREFERRED_ENCODING) for i in self.arguments] args = [dbus.String(i) for i in args] try: diff --git a/src/gajim_themes_window.py b/src/gajim_themes_window.py index ebaebc8d6..e6c23929a 100644 --- a/src/gajim_themes_window.py +++ b/src/gajim_themes_window.py @@ -275,7 +275,9 @@ class GajimThemesWindow: self._set_font() def _set_color(self, state, widget, option): - ''' set color value in prefs and update the UI ''' + """ + Set color value in prefs and update the UI + """ if state: color = widget.get_color() color_string = gtkgui_helpers.make_color_string(color) @@ -297,7 +299,9 @@ class GajimThemesWindow: gajim.interface.save_config() def _set_font(self): - ''' set font value in prefs and update the UI ''' + """ + Set font value in prefs and update the UI + """ state = self.textfont_checkbutton.get_active() if state: font_string = self.text_fontbutton.get_font_name() @@ -317,13 +321,16 @@ class GajimThemesWindow: gajim.interface.save_config() def _toggle_font_widgets(self, font_props): - ''' toggle font buttons with the bool values of font_props tuple''' + """ + Toggle font buttons with the bool values of font_props tuple + """ self.bold_togglebutton.set_active(font_props[0]) self.italic_togglebutton.set_active(font_props[1]) def _get_font_description(self): - ''' return a FontDescription from togglebuttons - states''' + """ + Return a FontDescription from togglebuttons states + """ fd = pango.FontDescription() if self.bold_togglebutton.get_active(): fd.set_weight(pango.WEIGHT_BOLD) @@ -332,8 +339,10 @@ class GajimThemesWindow: return fd def _set_font_widgets(self, font_attrs): - ''' set the correct toggle state of font style buttons by - a font string of type 'BI' ''' + """ + Set the correct toggle state of font style buttons by a font string of + type 'BI' + """ font_props = [False, False, False] if font_attrs: if font_attrs.find('B') != -1: @@ -343,7 +352,9 @@ class GajimThemesWindow: self._toggle_font_widgets(font_props) def _get_font_attrs(self): - ''' get a string with letters of font attribures: 'BI' ''' + """ + Get a string with letters of font attribures: 'BI' + """ attrs = '' if self.bold_togglebutton.get_active(): attrs += 'B' @@ -353,7 +364,9 @@ class GajimThemesWindow: def _get_font_props(self, font_name): - ''' get tuple of font properties: Weight, Style ''' + """ + Get tuple of font properties: weight, style + """ font_props = [False, False, False] font_description = pango.FontDescription(font_name) if font_description.get_weight() != pango.WEIGHT_NORMAL: diff --git a/src/groups.py b/src/groups.py index 1b78f5708..2588b11a4 100644 --- a/src/groups.py +++ b/src/groups.py @@ -26,7 +26,10 @@ import gtkgui_helpers class GroupsPostWindow: def __init__(self, account, servicejid, groupid): - '''Open new 'create post' window to create message for groupid on servicejid service.''' + """ + Open new 'create post' window to create message for groupid on servicejid + service + """ assert isinstance(servicejid, basestring) assert isinstance(groupid, basestring) @@ -43,11 +46,15 @@ class GroupsPostWindow: self.window.show_all() def on_cancel_button_clicked(self, w): - '''Close window.''' + """ + Close window + """ self.window.destroy() def on_send_button_clicked(self, w): - '''Gather info from widgets and send it as a message.''' + """ + Gather info from widgets and send it as a message + """ # constructing item to publish... that's atom:entry element item = xmpp.Node('entry', {'xmlns':'http://www.w3.org/2005/Atom'}) author = item.addChild('author') diff --git a/src/gtkgui_helpers.py b/src/gtkgui_helpers.py index 6a8b1b1cf..273dc65b7 100644 --- a/src/gtkgui_helpers.py +++ b/src/gtkgui_helpers.py @@ -67,8 +67,10 @@ def get_glade(file_name, root = None): return gtk.glade.XML(file_path, root=root, domain=i18n.APP) def get_completion_liststore(entry): - ''' create a completion model for entry widget - completion list consists of (Pixbuf, Text) rows''' + """ + Create a completion model for entry widget completion list consists of + (Pixbuf, Text) rows + """ completion = gtk.EntryCompletion() liststore = gtk.ListStore(gtk.gdk.Pixbuf, str) @@ -86,7 +88,9 @@ def get_completion_liststore(entry): def popup_emoticons_under_button(menu, button, parent_win): - ''' pops emoticons menu under button, which is in parent_win''' + """ + Popup the emoticons menu under button, which is in parent_win + """ window_x1, window_y1 = parent_win.get_origin() def position_menu_under_button(menu): # inline function, which will not keep refs, when used as CB @@ -115,8 +119,9 @@ def popup_emoticons_under_button(menu, button, parent_win): menu.popup(None, None, position_menu_under_button, 1, 0) def get_theme_font_for_option(theme, option): - '''return string description of the font, stored in - theme preferences''' + """ + Return string description of the font, stored in theme preferences + """ font_name = gajim.config.get_per('themes', theme, option) font_desc = pango.FontDescription() font_prop_str = gajim.config.get_per('themes', theme, option + 'attrs') @@ -130,10 +135,10 @@ def get_theme_font_for_option(theme, option): return fd.to_string() def get_default_font(): - '''Get the desktop setting for application font - first check for GNOME, then Xfce and last KDE - it returns None on failure or else a string 'Font Size' ''' - + """ + Get the desktop setting for application font first check for GNOME, then + Xfce and last KDE it returns None on failure or else a string 'Font Size' + """ try: import gconf # in try because daemon may not be there @@ -206,7 +211,9 @@ def user_runs_xfce(): return False def get_running_processes(): - '''returns running processes or None (if not /proc exists)''' + """ + Return running processes or None (if /proc does not exist) + """ if os.path.isdir('/proc'): # under Linux: checking if 'gnome-session' or # 'startkde' programs were run before gajim, by @@ -241,7 +248,9 @@ def get_running_processes(): return [] def move_window(window, x, y): - '''moves the window but also checks if out of screen''' + """ + Move the window, but also check if out of screen + """ if x < 0: x = 0 if y < 0: @@ -254,7 +263,9 @@ def move_window(window, x, y): window.move(x, y) def resize_window(window, w, h): - '''resizes window but also checks if huge window or negative values''' + """ + Resize window, but also checks if huge window or negative values + """ if not w or not h: return if w > screen_w: @@ -354,8 +365,10 @@ def parse_server_xml(path_to_file): print >> sys.stderr, _('Error parsing file:'), message def set_unset_urgency_hint(window, unread_messages_no): - '''sets/unsets urgency hint in window argument - depending if we have unread messages or not''' + """ + Sets/unset urgency hint in window argument depending if we have unread + messages or not + """ if gajim.config.get('use_urgency_hint'): if unread_messages_no > 0: window.props.urgency_hint = True @@ -363,8 +376,10 @@ def set_unset_urgency_hint(window, unread_messages_no): window.props.urgency_hint = False def get_abspath_for_script(scriptname, want_type = False): - '''checks if we are svn or normal user and returns abspath to asked script - if want_type is True we return 'svn' or 'install' ''' + """ + Check if we are svn or normal user and return abspath to asked script if + want_type is True we return 'svn' or 'install' + """ if os.path.isdir('.svn'): # we are svn user type_ = 'svn' cwd = os.getcwd() # it's always ending with src @@ -403,8 +418,10 @@ def get_abspath_for_script(scriptname, want_type = False): return path_to_script def get_pixbuf_from_data(file_data, want_type = False): - '''Gets image data and returns gtk.gdk.Pixbuf - if want_type is True it also returns 'jpeg', 'png' etc''' + """ + Get image data and returns gtk.gdk.Pixbuf if want_type is True it also + returns 'jpeg', 'png' etc + """ pixbufloader = gtk.gdk.PixbufLoader() try: pixbufloader.write(file_data) @@ -431,8 +448,11 @@ def get_invisible_cursor(): return cursor def get_current_desktop(window): - '''returns the current virtual desktop for given window - NOTE: window is GDK window''' + """ + Return the current virtual desktop for given window + + NOTE: Window is a GDK window. + """ prop = window.property_get('_NET_CURRENT_DESKTOP') if prop is None: # it means it's normal window (not root window) # so we look for it's current virtual desktop in another property @@ -444,9 +464,12 @@ def get_current_desktop(window): return current_virtual_desktop_no def possibly_move_window_in_current_desktop(window): - '''moves GTK window to current virtual desktop if it is not in the - current virtual desktop - window is GTK window''' + """ + Moves GTK window to current virtual desktop if it is not in the current + virtual desktop + + NOTE: Window is a GDK window. + """ if os.name == 'nt': return False @@ -468,7 +491,11 @@ def possibly_move_window_in_current_desktop(window): return False def file_is_locked(path_to_file): - '''returns True if file is locked (WINDOWS ONLY)''' + """ + Return True if file is locked + + NOTE: Windows only. + """ if os.name != 'nt': # just in case return @@ -496,8 +523,10 @@ def file_is_locked(path_to_file): return False def _get_fade_color(treeview, selected, focused): - '''get a gdk color that is between foreground and background in 0.3 - 0.7 respectively colors of the cell for the given treeview''' + """ + Get a gdk color that is between foreground and background in 0.3 + 0.7 respectively colors of the cell for the given treeview + """ style = treeview.style if selected: if focused: # is the window focused? @@ -516,9 +545,10 @@ def _get_fade_color(treeview, selected, focused): int(bg.blue*p + fg.blue*q)) def get_scaled_pixbuf(pixbuf, kind): - '''returns scaled pixbuf, keeping ratio etc or None - kind is either "chat", "roster", "notification", "tooltip", "vcard"''' - + """ + Return scaled pixbuf, keeping ratio etc or None kind is either "chat", + "roster", "notification", "tooltip", "vcard" + """ # resize to a width / height for the avatar not to have distortion # (keep aspect ratio) width = gajim.config.get(kind + '_avatar_width') @@ -544,12 +574,14 @@ def get_scaled_pixbuf(pixbuf, kind): return scaled_buf def get_avatar_pixbuf_from_cache(fjid, is_fake_jid = False, use_local = True): - '''checks if jid has cached avatar and if that avatar is valid image - (can be shown) - returns None if there is no image in vcard - returns 'ask' if cached vcard should not be used (user changed his vcard, - so we have new sha) or if we don't have the vcard''' + """ + Check if jid has cached avatar and if that avatar is valid image (can be + shown) + Returns None if there is no image in vcard/ + Returns 'ask' if cached vcard should not be used (user changed his vcard, so + we have new sha) or if we don't have the vcard + """ jid, nick = gajim.get_room_and_nick_from_fjid(fjid) if gajim.config.get('hide_avatar_of_transport') and\ gajim.jid_is_transport(jid): @@ -588,16 +620,21 @@ def get_avatar_pixbuf_from_cache(fjid, is_fake_jid = False, use_local = True): return pixbuf def make_gtk_month_python_month(month): - '''gtk start counting months from 0, so January is 0 - but python's time start from 1, so align to python - month MUST be integer''' + """ + GTK starts counting months from 0, so January is 0 but Python's time start + from 1, so align to Python + + NOTE: Month MUST be an integer. + """ return month + 1 def make_python_month_gtk_month(month): return month - 1 def make_color_string(color): - '''create #aabbcc color string from gtk color''' + """ + Create #aabbcc color string from gtk color + """ col = '#' for i in ('red', 'green', 'blue'): h = hex(getattr(color, i) / (16*16)).split('x')[1] @@ -612,10 +649,12 @@ def make_pixbuf_grayscale(pixbuf): return pixbuf2 def get_path_to_generic_or_avatar(generic, jid = None, suffix = None): - '''Chooses between avatar image and default image. - Returns full path to the avatar image if it exists, - otherwise returns full path to the image. - generic must be with extension and suffix without''' + """ + Choose between avatar image and default image + + Returns full path to the avatar image if it exists, otherwise returns full + path to the image. generic must be with extension and suffix without + """ if jid: # we want an avatar puny_jid = helpers.sanitize_filename(jid) @@ -632,9 +671,10 @@ def get_path_to_generic_or_avatar(generic, jid = None, suffix = None): return os.path.abspath(generic) def decode_filechooser_file_paths(file_paths): - '''decode as UTF-8 under Windows and - ask sys.getfilesystemencoding() in POSIX - file_paths MUST be LIST''' + """ + Decode as UTF-8 under Windows and ask sys.getfilesystemencoding() in POSIX + file_paths MUST be LIST + """ file_paths_list = list() if os.name == 'nt': # decode as UTF-8 under Windows @@ -655,7 +695,9 @@ def decode_filechooser_file_paths(file_paths): return file_paths_list def possibly_set_gajim_as_xmpp_handler(): - '''registers (by default only the first time) xmmp: to Gajim.''' + """ + Register (by default only the first time) 'xmmp:' to Gajim + """ path_to_dot_kde = os.path.expanduser('~/.kde') if os.path.exists(path_to_dot_kde): path_to_kde_file = os.path.join(path_to_dot_kde, @@ -737,8 +779,10 @@ Description=xmpp dlg.checkbutton.set_active(True) def escape_underscore(s): - '''Escape underlines to prevent them from being interpreted - as keyboard accelerators''' + """ + Escape underlines to prevent them from being interpreted as keyboard + accelerators + """ return s.replace('_', '__') def get_state_image_from_file_path_show(file_path, show): @@ -756,7 +800,9 @@ def get_state_image_from_file_path_show(file_path, show): return image def get_possible_button_event(event): - '''mouse or keyboard caused the event?''' + """ + Mouse or keyboard caused the event? + """ if event.type == gtk.gdk.KEY_PRESS: return 0 # no event.button so pass 0 # BUTTON_PRESS event, so pass event.button @@ -847,7 +893,9 @@ def on_bm_header_changed_state(widget, event): widget.set_state(gtk.STATE_NORMAL) #do not allow selected_state def create_combobox(value_list, selected_value = None): - '''Value_list is [(label1, value1), ]''' + """ + Value_list is [(label1, value1)] + """ liststore = gtk.ListStore(str, str) combobox = gtk.ComboBox(liststore) cell = gtk.CellRendererText() @@ -864,7 +912,9 @@ def create_combobox(value_list, selected_value = None): return combobox def create_list_multi(value_list, selected_values=None): - '''Value_list is [(label1, value1), ]''' + """ + Value_list is [(label1, value1)] + """ liststore = gtk.ListStore(str, str) treeview = gtk.TreeView(liststore) treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE) @@ -882,8 +932,10 @@ def create_list_multi(value_list, selected_values=None): return treeview def load_iconset(path, pixbuf2=None, transport=False): - '''load full iconset from the given path, and add - pixbuf2 on top left of each static images''' + """ + Load full iconset from the given path, and add pixbuf2 on top left of each + static images + """ path += '/' if transport: list_ = ('online', 'chat', 'away', 'xa', 'dnd', 'offline', @@ -898,21 +950,27 @@ def load_iconset(path, pixbuf2=None, transport=False): return _load_icon_list(list_, path, pixbuf2) def load_icon(icon_name): - '''load an icon from the iconset in 16x16''' + """ + Load an icon from the iconset in 16x16 + """ iconset = gajim.config.get('iconset') path = os.path.join(helpers.get_iconset_path(iconset), '16x16', '') icon_list = _load_icon_list([icon_name], path) return icon_list[icon_name] def load_mood_icon(icon_name): - '''load an icon from the mood iconset in 16x16''' + """ + Load an icon from the mood iconset in 16x16 + """ iconset = gajim.config.get('mood_iconset') path = os.path.join(helpers.get_mood_iconset_path(iconset), '') icon_list = _load_icon_list([icon_name], path) return icon_list[icon_name] def load_activity_icon(category, activity = None): - '''load an icon from the activity iconset in 16x16''' + """ + Load an icon from the activity iconset in 16x16 + """ iconset = gajim.config.get('activity_iconset') path = os.path.join(helpers.get_activity_iconset_path(iconset), category, '') @@ -922,8 +980,10 @@ def load_activity_icon(category, activity = None): return icon_list[activity] def load_icons_meta(): - '''load and return - AND + small icons to put on top left of an icon - for meta contacts.''' + """ + Load and return - AND + small icons to put on top left of an icon for meta + contacts + """ iconset = gajim.config.get('iconset') path = os.path.join(helpers.get_iconset_path(iconset), '16x16') # try to find opened_meta.png file, else opened.png else nopixbuf merge @@ -945,8 +1005,10 @@ def load_icons_meta(): return pixo, pixc def _load_icon_list(icons_list, path, pixbuf2 = None): - '''load icons in icons_list from the given path, - and add pixbuf2 on top left of each static images''' + """ + Load icons in icons_list from the given path, and add pixbuf2 on top left of + each static images + """ imgs = {} for icon in icons_list: # try to open a pixfile with the correct method @@ -972,7 +1034,9 @@ def _load_icon_list(icons_list, path, pixbuf2 = None): return imgs def make_jabber_state_images(): - '''initialise jabber_state_images dict''' + """ + Initialize jabber_state_images dictionary + """ iconset = gajim.config.get('iconset') if iconset: if helpers.get_iconset_path(iconset): @@ -1002,8 +1066,10 @@ def reload_jabber_state_images(): gajim.interface.roster.update_jabber_state_images() def label_set_autowrap(widget): - '''Make labels automatically re-wrap if their containers are resized. - Accepts label or container widgets.''' + """ + Make labels automatically re-wrap if their containers are resized. + Accepts label or container widgets + """ if isinstance (widget, gtk.Container): children = widget.get_children() for i in xrange (len (children)): @@ -1013,7 +1079,9 @@ def label_set_autowrap(widget): widget.connect_after('size-allocate', __label_size_allocate) def __label_size_allocate(widget, allocation): - '''Callback which re-allocates the size of a label.''' + """ + Callback which re-allocates the size of a label + """ layout = widget.get_layout() lw_old, lh_old = layout.get_size() diff --git a/src/gui_interface.py b/src/gui_interface.py index 7a948d5c1..6f5f06e4f 100644 --- a/src/gui_interface.py +++ b/src/gui_interface.py @@ -54,7 +54,6 @@ if dbus_support.supported: import gtkgui_helpers - import dialogs import notify import message_control @@ -92,7 +91,6 @@ config_filename = gajimpaths['CONFIG_FILE'] from common import optparser parser = optparser.OptionsParser(config_filename) - import logging log = logging.getLogger('gajim.interface') @@ -265,10 +263,10 @@ class Interface: def handle_event_new_jid(self, account, data): #('NEW_JID', account, (old_jid, new_jid)) - ''' + """ This event is raised when our JID changed (most probably because we use - anonymous account. We update contact and roster entry in this case. - ''' + anonymous account. We update contact and roster entry in this case + """ self.roster.rename_self_contact(data[0], data[1], account) def edit_own_details(self, account): @@ -1521,7 +1519,9 @@ class Interface: contact.resource) def handle_event_signed_in(self, account, empty): - '''SIGNED_IN event is emitted when we sign in, so handle it''' + """ + SIGNED_IN event is emitted when we sign in, so handle it + """ # ('SIGNED_IN', account, ()) # block signed in notifications for 30 seconds gajim.block_signed_in_notifications[account] = True @@ -1828,7 +1828,9 @@ class Interface: dialogs.RosterItemExchangeWindow(account, data[0], data[1], data[2]) def handle_event_unique_room_id_supported(self, account, data): - '''Receive confirmation that unique_room_id are supported''' + """ + Receive confirmation that unique_room_id are supported + """ # ('UNIQUE_ROOM_ID_SUPPORTED', server, instance, room_id) instance = data[1] instance.unique_room_id_supported(data[0], data[2]) @@ -2114,12 +2116,11 @@ class Interface: 'PEP_RECEIVED': [self.handle_event_pep_received] } - def dispatch(self, event, account, data): - ''' - Dispatches an network event to the event handlers of this class. - - Return true if it could be dispatched to alteast one handler. - ''' + def dispatch(self, event, account, data): + """ + Dispatch an network event to the event handlers of this class. Return + true if it could be dispatched to alteast one handler + """ if event not in self.handlers: log.warning('Unknown event %s dispatched to GUI: %s' % (event, data)) return False @@ -2135,7 +2136,9 @@ class Interface: ################################################################################ def add_event(self, account, jid, type_, event_args): - '''add an event to the gajim.events var''' + """ + Add an event to the gajim.events var + """ # We add it to the gajim.events queue # Do we have a queue? jid = gajim.get_jid_without_resource(jid) @@ -2464,7 +2467,9 @@ class Interface: self.invalid_XML_chars = u'[\x00-\x08]|[\x0b-\x0c]|[\x0e-\x19]|[\ud800-\udfff]|[\ufffe-\uffff]' def popup_emoticons_under_button(self, button, parent_win): - ''' pops emoticons menu under button, located in parent_win''' + """ + Popup the emoticons menu under button, located in parent_win + """ gtkgui_helpers.popup_emoticons_under_button(self.emoticons_menu, button, parent_win) @@ -2572,8 +2577,10 @@ class Interface: ################################################################################ def join_gc_room(self, account, room_jid, nick, password, minimize=False, - is_continued=False): - '''joins the room immediately''' + is_continued=False): + """ + Join the room immediately + """ if not nick: nick = gajim.nicks[account] @@ -2841,7 +2848,9 @@ class Interface: return (bg_str, fg_str) def read_sleepy(self): - '''Check idle status and change that status if needed''' + """ + Check idle status and change that status if needed + """ if not self.sleeper.poll(): # idle detection is not supported in that OS return False # stop looping in vain @@ -2895,7 +2904,9 @@ class Interface: return True # renew timeout (loop for ever) def autoconnect(self): - '''auto connect at startup''' + """ + Auto connect at startup + """ # dict of account that want to connect sorted by status shows = {} for a in gajim.connections: @@ -2934,7 +2945,9 @@ class Interface: helpers.launch_browser_mailer(kind, url) def process_connections(self): - ''' Called each foo (200) miliseconds. Check for idlequeue timeouts. ''' + """ + Called each foo (200) miliseconds. Check for idlequeue timeouts + """ try: gajim.idlequeue.process() except Exception: @@ -2958,7 +2971,11 @@ class Interface: sys.exit() def save_avatar_files(self, jid, photo, puny_nick = None, local = False): - '''Saves an avatar to a separate file, and generate files for dbus notifications. An avatar can be given as a pixmap directly or as an decoded image.''' + """ + Save an avatar to a separate file, and generate files for dbus + notifications. An avatar can be given as a pixmap directly or as an + decoded image + """ puny_jid = helpers.sanitize_filename(jid) path_to_file = os.path.join(gajim.AVATAR_PATH, puny_jid) if puny_nick: @@ -3011,7 +3028,9 @@ class Interface: (path_to_original_file, str(e))) def remove_avatar_files(self, jid, puny_nick = None, local = False): - '''remove avatar files of a jid''' + """ + Remove avatar files of a jid + """ puny_jid = helpers.sanitize_filename(jid) path_to_file = os.path.join(gajim.AVATAR_PATH, puny_jid) if puny_nick: @@ -3028,7 +3047,9 @@ class Interface: os.remove(path_to_file + '_notif_size_bw' + ext) def auto_join_bookmarks(self, account): - '''autojoin bookmarked GCs that have 'auto join' on for this account''' + """ + Autojoin bookmarked GCs that have 'auto join' on for this account + """ for bm in gajim.connections[account].bookmarks: if bm['autojoin'] in ('1', 'true'): jid = bm['jid'] @@ -3046,8 +3067,10 @@ class Interface: self.roster.add_groupchat(jid, account) def add_gc_bookmark(self, account, name, jid, autojoin, minimize, password, - nick): - '''add a bookmark for this account, sorted in bookmark list''' + nick): + """ + Add a bookmark for this account, sorted in bookmark list + """ bm = { 'name': name, 'jid': jid, @@ -3475,13 +3498,9 @@ class PassphraseRequest: class ThreadInterface: def __init__(self, func, func_args, callback, callback_args): - '''Call a function in a thread - - :param func: the function to call in the thread - :param func_args: list or arguments for this function - :param callback: callback to call once function is finished - :param callback_args: list of arguments for this callback - ''' + """ + Call a function in a thread + """ def thread_function(func, func_args, callback, callback_args): output = func(*func_args) gobject.idle_add(callback, output, *callback_args) diff --git a/src/gui_menu_builder.py b/src/gui_menu_builder.py index 80575d1ad..453677aa2 100644 --- a/src/gui_menu_builder.py +++ b/src/gui_menu_builder.py @@ -28,9 +28,11 @@ from common import helpers from common.xmpp.protocol import NS_COMMANDS, NS_FILE, NS_MUC, NS_ESESSION def build_resources_submenu(contacts, account, action, room_jid=None, -room_account=None, cap=None): - ''' Build a submenu with contact's resources. - room_jid and room_account are for action self.on_invite_to_room ''' + room_account=None, cap=None): + """ + Build a submenu with contact's resources. room_jid and room_account are for + action self.on_invite_to_room + """ roster = gajim.interface.roster sub_menu = gtk.Menu() @@ -61,7 +63,9 @@ room_account=None, cap=None): return sub_menu def build_invite_submenu(invite_menuitem, list_): - '''list_ in a list of (contact, account)''' + """ + list_ in a list of (contact, account) + """ roster = gajim.interface.roster # used if we invite only one contact with several resources contact_list = [] @@ -145,10 +149,12 @@ def build_invite_submenu(invite_menuitem, list_): invite_to_submenu.append(menuitem) def get_contact_menu(contact, account, use_multiple_contacts=True, -show_start_chat=True, show_encryption=False, show_buttonbar_items=True, -control=None): - ''' Build contact popup menu for roster and chat window. - If control is not set, we hide invite_contacts_menuitem''' + show_start_chat=True, show_encryption=False, show_buttonbar_items=True, + control=None): + """ + Build contact popup menu for roster and chat window. If control is not set, + we hide invite_contacts_menuitem + """ if not contact: return diff --git a/src/history_manager.py b/src/history_manager.py index 5cdac0369..5d171b75c 100644 --- a/src/history_manager.py +++ b/src/history_manager.py @@ -290,11 +290,13 @@ class HistoryManager: self._fill_logs_listview(jid) def _get_jid_id(self, jid): - '''jids table has jid and jid_id + """ + jids table has jid and jid_id logs table has log_id, jid_id, contact_name, time, kind, show, message - so to ask logs we need jid_id that matches our jid in jids table - this method wants jid and returns the jid_id for later sql-ing on logs - ''' + + So to ask logs we need jid_id that matches our jid in jids table this + method wants jid and returns the jid_id for later sql-ing on logs + """ if jid.find('/') != -1: # if it has a / jid_is_from_pm = self._jid_is_from_pm(jid) if not jid_is_from_pm: # it's normal jid with resource @@ -304,22 +306,24 @@ class HistoryManager: return str(jid_id) def _get_jid_from_jid_id(self, jid_id): - '''jids table has jid and jid_id - this method accepts jid_id and returns the jid for later sql-ing on logs - ''' + """ + jids table has jid and jid_id + + This method accepts jid_id and returns the jid for later sql-ing on logs + """ self.cur.execute('SELECT jid FROM jids WHERE jid_id = ?', (jid_id,)) jid = self.cur.fetchone()[0] return jid def _jid_is_from_pm(self, jid): - '''if jid is gajim@conf/nkour it's likely a pm one, how we know - gajim@conf is not a normal guy and nkour is not his resource? - we ask if gajim@conf is already in jids (with type room jid) - this fails if user disables logging for room and only enables for - pm (so higly unlikely) and if we fail we do not go chaos - (user will see the first pm as if it was message in room's public chat) - and after that all okay''' - + """ + If jid is gajim@conf/nkour it's likely a pm one, how we know gajim@conf + is not a normal guy and nkour is not his resource? We ask if gajim@conf + is already in jids (with type room jid). This fails if user disables + logging for room and only enables for pm (so higly unlikely) and if we + fail we do not go chaos (user will see the first pm as if it was message + in room's public chat) and after that everything is ok + """ possible_room_jid = jid.split('/', 1)[0] self.cur.execute('SELECT jid_id FROM jids WHERE jid = ? AND type = ?', @@ -331,8 +335,9 @@ class HistoryManager: return True def _jid_is_room_type(self, jid): - '''returns True/False if given id is room type or not - eg. if it is room''' + """ + Return True/False if given id is room type or not eg. if it is room + """ self.cur.execute('SELECT type FROM jids WHERE jid = ?', (jid,)) row = self.cur.fetchone() if row is None: @@ -343,8 +348,10 @@ class HistoryManager: return False def _fill_logs_listview(self, jid): - '''fill the listview with all messages that user sent to or - received from JID''' + """ + Fill the listview with all messages that user sent to or received from + JID + """ # no need to lower jid in this context as jid is already lowered # as we use those jids from db jid_id = self._get_jid_id(jid) @@ -403,7 +410,9 @@ class HistoryManager: subject, nickname)) def _fill_search_results_listview(self, text): - '''ask db and fill listview with results that match text''' + """ + Ask db and fill listview with results that match text + """ self.search_results_liststore.clear() like_sql = '%' + text + '%' self.cur.execute('''