diff --git a/data/glade/accounts_window.glade b/data/glade/accounts_window.glade index 3ac6f9e7b..39b637bae 100644 --- a/data/glade/accounts_window.glade +++ b/data/glade/accounts_window.glade @@ -2,6 +2,7 @@ + 12 Accounts @@ -64,7 +65,7 @@ True If you have 2 or more accounts and it is checked, Gajim will list all contacts as if you had one account True - _Merge accounts + Mer_ge accounts True GTK_RELIEF_NORMAL True @@ -80,6 +81,38 @@ + + + True + + + 0 + False + True + + + + + + True + If checked, all local contacts that use a Bonjour compatible chat client (like iChat, Trillian or Gaim) will be shown in roster. You don't need to be connected to a jabber server for it to work. +This is only available if python-avahi is installed and avahi-daemon is running. + True + _Enable link-local messaging + True + GTK_RELIEF_NORMAL + True + False + False + True + + + 0 + False + False + + + True @@ -267,4 +300,5 @@ + diff --git a/data/glade/zeroconf_contact_context_menu.glade b/data/glade/zeroconf_contact_context_menu.glade new file mode 100644 index 000000000..edbb0f064 --- /dev/null +++ b/data/glade/zeroconf_contact_context_menu.glade @@ -0,0 +1,153 @@ + + + + + + + + + + True + Start _Chat + True + + + + True + gtk-jump-to + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + + _Rename + True + + + + True + gtk-refresh + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + + Edit _Groups + True + + + + + + True + + + + + + True + Send _File + True + + + + True + gtk-file + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + + Assign Open_PGP Key + True + + + + + True + gtk-dialog-authentication + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + + True + Add Special _Notification + True + + + + True + gtk-info + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + + True + + + + + + gtk-info + True + + + + + + _History + True + + + + True + gtk-justify-fill + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + diff --git a/data/glade/zeroconf_context_menu.glade b/data/glade/zeroconf_context_menu.glade new file mode 100644 index 000000000..f2928cf04 --- /dev/null +++ b/data/glade/zeroconf_context_menu.glade @@ -0,0 +1,49 @@ + + + + + + + + + + True + _Status + True + + + + True + gtk-network + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + + True + _Modify Account... + True + + + + True + gtk-preferences + 1 + 0.5 + 0.5 + 0 + 0 + + + + + + + diff --git a/data/glade/zeroconf_information_window.glade b/data/glade/zeroconf_information_window.glade new file mode 100644 index 000000000..8cdf7e891 --- /dev/null +++ b/data/glade/zeroconf_information_window.glade @@ -0,0 +1,666 @@ + + + + + + + 12 + Contact Information + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + False + False + True + False + False + GDK_WINDOW_TYPE_HINT_NORMAL + GDK_GRAVITY_NORTH_WEST + True + + + + + + True + False + 12 + + + + True + True + + False + False + GTK_JUSTIFY_LEFT + False + True + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + False + False + + + + + + True + True + True + GTK_POS_TOP + False + False + + + + 12 + True + False + 12 + + + + True + 4 + 2 + False + 6 + 12 + + + + True + Local jid: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 0 + 1 + fill + + + + + + + True + Resource: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 1 + 2 + fill + + + + + + + True + Status: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 2 + 3 + fill + + + + + + + True + True + + False + False + GTK_JUSTIFY_LEFT + False + True + 0 + 0 + 5 + 5 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 1 + 2 + 0 + 1 + + + + + + + True + False + False + + + + True + True + + False + False + GTK_JUSTIFY_LEFT + False + True + 0 + 0 + 5 + 5 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + + + 1 + 2 + 1 + 2 + + + + + + + True + True + False + + + + True + True + + False + False + GTK_JUSTIFY_LEFT + False + True + 0 + 0 + 5 + 5 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + + + 1 + 2 + 2 + 3 + fill + fill + + + + + + True + True + _Log conversation history + True + GTK_RELIEF_NORMAL + True + True + False + True + + + 0 + 2 + 3 + 4 + fill + + + + + + 0 + True + True + + + + + + True + False + 0 + + + + True + False + False + + + + + True + 0.5 + 0 + 0 + 0 + + + + + 0 + False + False + + + + + + + + + 0 + True + True + + + + + False + True + + + + + + True + Contact + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + tab + + + + + + 16 + True + 4 + 2 + False + 6 + 12 + + + + True + Jabber ID: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 2 + 3 + fill + + + + + + + True + True + + False + False + GTK_JUSTIFY_LEFT + False + True + 0 + 0 + 5 + 5 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 1 + 2 + 2 + 3 + fill + + + + + + + True + E-Mail: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 3 + 4 + fill + + + + + + + True + True + + False + False + GTK_JUSTIFY_LEFT + True + True + 0 + 0 + 5 + 5 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 1 + 2 + 3 + 4 + fill + + + + + + + True + Last Name: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 1 + 2 + fill + + + + + + + True + First Name: + False + False + GTK_JUSTIFY_LEFT + False + False + 0 + 0 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 0 + 1 + fill + + + + + + + True + True + + False + False + GTK_JUSTIFY_LEFT + False + True + 0 + 0 + 5 + 5 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 1 + 2 + 0 + 1 + expand + + + + + + True + True + + False + False + GTK_JUSTIFY_LEFT + False + True + 0 + 0 + 5 + 5 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 1 + 2 + 1 + 2 + fill + + + + + + True + True + + + + + + True + Personal + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + tab + + + + + 0 + True + True + + + + + + True + GTK_BUTTONBOX_END + 0 + + + + True + True + True + gtk-close + True + GTK_RELIEF_NORMAL + True + + + + + + 0 + True + True + + + + + + + diff --git a/data/glade/zeroconf_properties_window.glade b/data/glade/zeroconf_properties_window.glade new file mode 100644 index 000000000..c296fe4d5 --- /dev/null +++ b/data/glade/zeroconf_properties_window.glade @@ -0,0 +1,689 @@ + + + + + + + 12 + Modify Account + GTK_WINDOW_TOPLEVEL + GTK_WIN_POS_NONE + False + True + False + True + False + False + GDK_WINDOW_TYPE_HINT_NORMAL + GDK_GRAVITY_NORTH_WEST + True + + + + + True + False + 6 + + + + True + False + 5 + + + + True + True + True + True + GTK_POS_TOP + False + False + + + + 6 + True + False + 6 + + + + True + If checked, Gajim, when launched, will automatically connect to jabber using this account + True + C_onnect on Gajim startup + True + GTK_RELIEF_NORMAL + True + False + False + True + + + 0 + False + False + + + + + + True + True + Save conversation _logs for all contacts + True + GTK_RELIEF_NORMAL + True + False + False + True + + + 0 + False + False + + + + + + True + If checked, any change to the global status (handled by the combobox at the bottom of the roster window) will change the status of this account accordingly + True + Synch_ronize account status with global status + True + GTK_RELIEF_NORMAL + True + False + False + True + + + 0 + False + False + + + + + + True + False + 0 + + + + True + If the default port that is used for incoming messages is unfitting for your setup you can select another one here. +You might consider to change possible firewall settings. + True + Use custom port: + True + GTK_RELIEF_NORMAL + True + False + False + True + + + + 0 + False + False + + + + + + True + True + True + True + 0 + + True + + False + 6 + + + 0 + False + False + + + + + 10 + False + True + + + + + False + True + + + + + + True + General + False + True + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + tab + + + + + + 6 + True + 6 + 2 + False + 5 + 2 + + + + True + True + True + True + 0 + + True + + False + + + 1 + 2 + 4 + 5 + + + + + + + True + Jabber ID: + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 4 + 5 + fill + + + + + + + True + E-Mail: + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 5 + 6 + fill + + + + + + + True + True + True + True + 0 + + True + + False + + + 1 + 2 + 5 + 6 + + + + + + + True + Last Name: + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 3 + 4 + fill + + + + + + + True + True + True + True + 0 + + True + + False + + + 1 + 2 + 3 + 4 + + + + + + + True + First Name: + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 1 + 2 + 3 + fill + + + + + + + True + True + True + True + 0 + + True + + False + + + 1 + 2 + 2 + 3 + + + + + + + True + <b>Personal Information</b> + False + True + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + 2 + 1 + 2 + fill + + + + + + + True + False + 5 + + + + True + <b>OpenPGP</b> + False + True + GTK_JUSTIFY_LEFT + False + False + 0 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + False + False + + + + + + True + False + 5 + + + + True + No key selected + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + False + False + + + + + + True + + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + 0 + True + True + + + + + + True + True + Choose _Key... + True + GTK_RELIEF_NORMAL + True + + + + 0 + False + False + + + + + 0 + True + True + + + + + + True + False + 0 + + + + True + If checked, Gajim will store the password in ~/.gajim/config with 'read' permission only for you + True + Save _passphrase (insecure) + True + GTK_RELIEF_NORMAL + True + False + False + True + + + + 0 + False + False + + + + + + True + False + True + True + False + 0 + + True + * + False + + + 0 + True + True + + + + + 0 + True + True + + + + + 0 + 2 + 0 + 1 + fill + + + + + False + True + + + + + + True + Personal Information + False + False + GTK_JUSTIFY_LEFT + False + False + 0.5 + 0.5 + 0 + 0 + PANGO_ELLIPSIZE_NONE + -1 + False + 0 + + + tab + + + + + 0 + True + True + + + + + 2 + False + False + + + + + + True + GTK_BUTTONBOX_END + 12 + + + + True + True + True + gtk-cancel + True + GTK_RELIEF_NORMAL + True + + + + + + + + True + True + True + True + gtk-save + True + GTK_RELIEF_NORMAL + True + + + + + + 0 + False + True + + + + + + + diff --git a/src/common/config.py b/src/common/config.py index fd575affa..960b39516 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -6,7 +6,8 @@ ## Copyright (C) 2005 Dimitur Kirov ## Copyright (C) 2005 Travis Shirk ## Copyright (C) 2005 Norman Rasmussen -## +## Copyright (C) 2006 Stefan Bethge +## ## 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. @@ -82,6 +83,7 @@ class Config: 'saveposition': [ opt_bool, True ], 'mergeaccounts': [ opt_bool, False, '', True ], 'sort_by_show': [ opt_bool, True, '', True ], + 'enable_zeroconf': [opt_bool, False, _('Enable link-local/zeroconf messaging')], 'use_speller': [ opt_bool, False, ], 'speller_language': [ opt_str, '', _('Language used by speller')], 'print_time': [ opt_str, 'always', _('\'always\' - print time for every message.\n\'sometimes\' - print time every print_ichat_every_foo_minutes minute.\n\'never\' - never print time.')], @@ -259,6 +261,11 @@ class Config: 'msgwin-y-position': [opt_int, -1], # Default is to let the wm decide 'msgwin-width': [opt_int, 480], 'msgwin-height': [opt_int, 440], + 'is_zeroconf': [opt_bool, False], + 'zeroconf_first_name': [ opt_str, '', '', True ], + 'zeroconf_last_name': [ opt_str, '', '', True ], + 'zeroconf_jabber_id': [ opt_str, '', '', True ], + 'zeroconf_email': [ opt_str, '', '', True ], }, {}), 'statusmsg': ({ 'message': [ opt_str, '' ], diff --git a/src/common/connection.py b/src/common/connection.py index 095cf2d4f..27a7521ea 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -46,6 +46,7 @@ class Connection(ConnectionHandlers): self.connection = None # xmpppy ClientCommon instance # this property is used to prevent double connections self.last_connection = None # last ClientCommon instance + self.is_zeroconf = False self.gpg = None self.status = '' self.priority = gajim.get_priority(name, 'offline') diff --git a/src/common/gajim.py b/src/common/gajim.py index 2fcc3d0a5..e353f284c 100644 --- a/src/common/gajim.py +++ b/src/common/gajim.py @@ -120,6 +120,12 @@ status_before_autoaway = {} SHOW_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', 'invisible'] +# zeroconf account name +ZEROCONF_ACC_NAME = 'Local' +priority_dict = {} +for status in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'): + priority_dict[status] = config.get('autopriority' + status) + def get_nick_from_jid(jid): pos = jid.find('@') return jid[:pos] diff --git a/src/common/xmpp/dispatcher_nb.py b/src/common/xmpp/dispatcher_nb.py index ca13af184..3079ca6f2 100644 --- a/src/common/xmpp/dispatcher_nb.py +++ b/src/common/xmpp/dispatcher_nb.py @@ -74,7 +74,6 @@ class Dispatcher(PlugIn): self.RegisterProtocol('presence', Presence) self.RegisterProtocol('message', Message) self.RegisterDefaultHandler(self.returnStanzaHandler) - # Register Gajim's event handler as soon as dispatcher begins self.RegisterEventHandler(self._owner._caller._event_dispatcher) self.on_responses = {} @@ -84,7 +83,10 @@ class Dispatcher(PlugIn): self._owner.lastErrNode = None self._owner.lastErr = None self._owner.lastErrCode = None - self.StreamInit() + if hasattr(self._owner, 'StreamInit'): + self._owner.StreamInit() + else: + self.StreamInit() def plugout(self): ''' Prepares instance to be destructed. ''' @@ -134,7 +136,7 @@ class Dispatcher(PlugIn): return 0 except ExpatError: sys.exc_clear() - self.DEBUG('Invalid XML received from server. Forcing disconnect.') + self.DEBUG('Invalid XML received from server. Forcing disconnect.', 'error') self._owner.Connection.pollend() return 0 if len(self._pendingExceptions) > 0: diff --git a/src/common/xmpp/session.py b/src/common/xmpp/session.py index 3921937ed..b61e4f6de 100644 --- a/src/common/xmpp/session.py +++ b/src/common/xmpp/session.py @@ -183,7 +183,7 @@ class Session: if self.sendbuffer: try: # LOCK_QUEUE - sent=self._send(self.sendbuffer) # ! + sent=self._send(self.sendbuffer) # blocking socket except: # UNLOCK_QUEUE self.set_socket_state(SOCKET_DEAD) diff --git a/src/common/zeroconf/__init__.py b/src/common/zeroconf/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/common/zeroconf/client_zeroconf.py b/src/common/zeroconf/client_zeroconf.py new file mode 100644 index 000000000..6f53b7071 --- /dev/null +++ b/src/common/zeroconf/client_zeroconf.py @@ -0,0 +1,598 @@ +## common/zeroconf/client_zeroconf.py +## +## Copyright (C) 2006 Stefan Bethge +## 2006 Dimitur Kirov +## +## 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. +## +from common import gajim +import common.xmpp +from common.xmpp.idlequeue import IdleObject +from common.xmpp import dispatcher_nb, simplexml +from common.xmpp.client import * +from common.xmpp.simplexml import ustr +from common.zeroconf import zeroconf + +from common.xmpp.protocol import * +import socket +import errno +import sys + +from common.zeroconf import roster_zeroconf + +MAX_BUFF_LEN = 65536 +DATA_RECEIVED='DATA RECEIVED' +DATA_SENT='DATA SENT' +TYPE_SERVER, TYPE_CLIENT = range(2) + +# wait XX sec to establish a connection +CONNECT_TIMEOUT_SECONDS = 30 + +# after XX sec with no activity, close the stream +ACTIVITY_TIMEOUT_SECONDS = 180 + +class ZeroconfListener(IdleObject): + def __init__(self, port, conn_holder): + ''' handle all incomming connections on ('0.0.0.0', port)''' + self.port = port + self.queue_idx = -1 + #~ self.queue = None + self.started = False + self._sock = None + self.fd = -1 + self.caller = conn_holder.caller + self.conn_holder = conn_holder + + def bind(self): + self._serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._serv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._serv.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + self._serv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + # will fail when port is busy, or we don't have rights to bind + try: + self._serv.bind(('0.0.0.0', self.port)) + except Exception, e: + # unable to bind, show error dialog + return None + self._serv.listen(socket.SOMAXCONN) + self._serv.setblocking(False) + self.fd = self._serv.fileno() + gajim.idlequeue.plug_idle(self, False, True) + self.started = True + + def pollend(self): + ''' called when we stop listening on (host, port) ''' + self.disconnect() + + def pollin(self): + ''' accept a new incomming connection and notify queue''' + sock = self.accept_conn() + P2PClient(sock[0], sock[1][0], sock[1][1], self.conn_holder) + + def disconnect(self): + ''' free all resources, we are not listening anymore ''' + gajim.idlequeue.remove_timeout(self.fd) + gajim.idlequeue.unplug_idle(self.fd) + self.fd = -1 + self.started = False + try: + self._serv.close() + except: + pass + self.conn_holder.kill_all_connections() + + def accept_conn(self): + ''' accepts a new incomming connection ''' + _sock = self._serv.accept() + _sock[0].setblocking(False) + return _sock + +class P2PClient(IdleObject): + def __init__(self, _sock, host, port, conn_holder, messagequeue = [], to = None): + self._owner = self + self.Namespace = 'jabber:client' + self.defaultNamespace = self.Namespace + self._component = 0 + self._registered_name = None + self._caller = conn_holder.caller + self.conn_holder = conn_holder + self.messagequeue = messagequeue + self.to = to + self.Server = host + self.DBG = 'client' + self.Connection = None + if gajim.verbose: + debug = ['always', 'nodebuilder'] + else: + debug = [] + self._DEBUG = Debug.Debug(debug) + self.DEBUG = self._DEBUG.Show + self.debug_flags = self._DEBUG.debug_flags + self.debug_flags.append(self.DBG) + self.sock_hash = None + if _sock: + self.sock_type = TYPE_SERVER + else: + self.sock_type = TYPE_CLIENT + conn = P2PConnection('', _sock, host, port, self._caller, self.on_connect) + self.sock_hash = conn._sock.__hash__ + self.conn_holder.add_connection(self, self.Server, self.to) + + def add_message(self, message): + if self.Connection: + if self.Connection.state == -1: + return False + self.send(message) + else: + self.messagequeue.append(message) + return True + + def on_connect(self, conn): + self.Connection = conn + self.Connection.PlugIn(self) + dispatcher_nb.Dispatcher().PlugIn(self) + self._register_handlers() + if self.sock_type == TYPE_CLIENT: + while self.messagequeue: + message = self.messagequeue.pop(0) + self.send(message) + + def StreamInit(self): + ''' Send an initial stream header. ''' + self.Dispatcher.Stream = simplexml.NodeBuilder() + self.Dispatcher.Stream._dispatch_depth = 2 + self.Dispatcher.Stream.dispatch = self.Dispatcher.dispatch + self.Dispatcher.Stream.stream_header_received = self._check_stream_start + self.debug_flags.append(simplexml.DBG_NODEBUILDER) + self.Dispatcher.Stream.DEBUG = self.DEBUG + self.Dispatcher.Stream.features = None + if self.sock_type == TYPE_CLIENT: + self.send_stream_header() + + def send_stream_header(self): + self.Dispatcher._metastream = Node('stream:stream') + self.Dispatcher._metastream.setNamespace(self.Namespace) + # XXX TLS support + #~ self._metastream.setAttr('version', '1.0') + self.Dispatcher._metastream.setAttr('xmlns:stream', NS_STREAMS) + self.Dispatcher.send("%s>" % str(self.Dispatcher._metastream)[:-2]) + + def _check_stream_start(self, ns, tag, attrs): + if ns<>NS_STREAMS or tag<>'stream': + self.Connection.DEBUG('Incorrect stream start: (%s,%s).Terminating! ' % (tag, ns), 'error') + self.Connection.disconnect() + return + if self.sock_type == TYPE_SERVER: + self.send_stream_header() + while self.messagequeue: + message = self.messagequeue.pop(0) + self.send(message) + + + def on_disconnect(self): + if self.conn_holder: + self.conn_holder.remove_connection(self.sock_hash) + if self.__dict__.has_key('Dispatcher'): + self.Dispatcher.PlugOut() + if self.__dict__.has_key('P2PConnection'): + self.P2PConnection.PlugOut() + self.Connection = None + self._caller = None + self.conn_holder = None + + def force_disconnect(self): + if self.Connection: + self.disconnect() + else: + self.on_disconnect() + + def _on_receive_document_attrs(self, data): + if data: + self.Dispatcher.ProcessNonBlocking(data) + if not hasattr(self, 'Dispatcher') or \ + self.Dispatcher.Stream._document_attrs is None: + return + self.onreceive(None) + if self.Dispatcher.Stream._document_attrs.has_key('version') and \ + self.Dispatcher.Stream._document_attrs['version'] == '1.0': + #~ self.onreceive(self._on_receive_stream_features) + #XXX continue with TLS + return + self.onreceive(None) + return True + + + + def _register_handlers(self): + self.RegisterHandler('message', lambda conn, data:self._caller._messageCB(self.Server, conn, data)) + self.RegisterHandler('iq', self._caller._siSetCB, 'set', + common.xmpp.NS_SI) + self.RegisterHandler('iq', self._caller._siErrorCB, 'error', + common.xmpp.NS_SI) + self.RegisterHandler('iq', self._caller._siResultCB, 'result', + common.xmpp.NS_SI) + self.RegisterHandler('iq', self._caller._bytestreamSetCB, 'set', + common.xmpp.NS_BYTESTREAM) + self.RegisterHandler('iq', self._caller._bytestreamResultCB, 'result', + common.xmpp.NS_BYTESTREAM) + self.RegisterHandler('iq', self._caller._bytestreamErrorCB, 'error', + common.xmpp.NS_BYTESTREAM) + +class P2PConnection(IdleObject, PlugIn): + ''' class for sending file to socket over socks5 ''' + def __init__(self, sock_hash, _sock, host = None, port = None, caller = None, on_connect = None): + IdleObject.__init__(self) + self._owner = None + PlugIn.__init__(self) + self.DBG_LINE='socket' + self.sendqueue = [] + self.sendbuff = None + self._sock = _sock + self.host, self.port = host, port + self.on_connect = on_connect + self.writable = False + self.readable = False + self._exported_methods=[self.send, self.disconnect, self.onreceive] + self.on_receive = None + if _sock: + self._sock = _sock + self.state = 1 + self._sock.setblocking(False) + self.fd = self._sock.fileno() + self.on_connect(self) + else: + self.state = 0 + self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._sock.setblocking(False) + self.fd = self._sock.fileno() + gajim.idlequeue.plug_idle(self, True, False) + self.set_timeout(CONNECT_TIMEOUT_SECONDS) + self.do_connect() + + def set_timeout(self, timeout): + gajim.idlequeue.remove_timeout(self.fd) + if self.state >= 0: + gajim.idlequeue.set_read_timeout(self.fd, timeout) + + def plugin(self, owner): + self.onreceive(owner._on_receive_document_attrs) + self._plug_idle() + return True + + def plugout(self): + ''' Disconnect from the remote server and unregister self.disconnected method from + the owner's dispatcher. ''' + self.disconnect() + self._owner = None + + def onreceive(self, recv_handler): + if not recv_handler: + if hasattr(self._owner, 'Dispatcher'): + self.on_receive = self._owner.Dispatcher.ProcessNonBlocking + else: + self.on_receive = None + return + _tmp = self.on_receive + # make sure this cb is not overriden by recursive calls + if not recv_handler(None) and _tmp == self.on_receive: + self.on_receive = recv_handler + + + + def send(self, stanza): + '''Append stanza to the queue of messages to be send. + If supplied data is unicode string, encode it to utf-8. + ''' + if self.state <= 0: + return + r = stanza + if isinstance(r, unicode): + r = r.encode('utf-8') + elif not isinstance(r, str): + r = ustr(r).encode('utf-8') + self.sendqueue.append(r) + self._plug_idle() + + def read_timeout(self): + self.pollend() + + + def do_connect(self): + errnum = 0 + try: + self._sock.connect((self.host, self.port)) + self._sock.setblocking(False) + except Exception, ee: + (errnum, errstr) = ee + if errnum in (errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK): + return + # win32 needs this + elif errnum not in (0, 10056, errno.EISCONN) or self.state != 0: + self.disconnect() + return None + else: # socket is already connected + self._sock.setblocking(False) + self.state = 1 # connected + self.on_connect(self) + return 1 # we are connected + + + def pollout(self): + if self.state == 0: + self.do_connect() + return + gajim.idlequeue.remove_timeout(self.fd) + self._do_send() + + def pollend(self): + self.state = -1 + self.disconnect() + + def pollin(self): + ''' Reads all pending incoming data. Calls owner's disconnected() method if appropriate.''' + received = '' + errnum = 0 + try: + # get as many bites, as possible, but not more than RECV_BUFSIZE + received = self._sock.recv(MAX_BUFF_LEN) + except Exception, e: + if len(e.args) > 0 and isinstance(e.args[0], int): + errnum = e[0] + sys.exc_clear() + # "received" will be empty anyhow + if errnum == socket.SSL_ERROR_WANT_READ: + pass + elif errnum in [errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN]: + self.pollend() + # don't proccess result, cas it will raise error + return + elif not received : + if errnum != socket.SSL_ERROR_EOF: + # 8 EOF occurred in violation of protocol + self.pollend() + if self.state >= 0: + self.disconnect() + return + + if self.state < 0: + return + if self.on_receive: + if self._owner.sock_type == TYPE_CLIENT: + self.set_timeout(ACTIVITY_TIMEOUT_SECONDS) + if received.strip(): + self.DEBUG(received, 'got') + if hasattr(self._owner, 'Dispatcher'): + self._owner.Dispatcher.Event('', DATA_RECEIVED, received) + self.on_receive(received) + else: + # This should never happed, so we need the debug + self.DEBUG('Unhandled data received: %s' % received,'error') + self.disconnect() + return True + + def onreceive(self, recv_handler): + if not recv_handler: + if hasattr(self._owner, 'Dispatcher'): + self.on_receive = self._owner.Dispatcher.ProcessNonBlocking + else: + self.on_receive = None + return + _tmp = self.on_receive + # make sure this cb is not overriden by recursive calls + if not recv_handler(None) and _tmp == self.on_receive: + self.on_receive = recv_handler + + def disconnect(self): + ''' Closes the socket. ''' + gajim.idlequeue.remove_timeout(self.fd) + gajim.idlequeue.unplug_idle(self.fd) + try: + self._sock.shutdown(socket.SHUT_RDWR) + self._sock.close() + except: + # socket is already closed + pass + self.fd = -1 + self.state = -1 + if self._owner: + self._owner.on_disconnect() + + def _do_send(self): + if not self.sendbuff: + if not self.sendqueue: + return None # nothing to send + self.sendbuff = self.sendqueue.pop(0) + self.sent_data = self.sendbuff + try: + send_count = self._sock.send(self.sendbuff) + if send_count: + self.sendbuff = self.sendbuff[send_count:] + if not self.sendbuff and not self.sendqueue: + if self.state < 0: + gajim.idlequeue.unplug_idle(self.fd) + self._on_send() + self.disconnect() + return + # we are not waiting for write + self._plug_idle() + self._on_send() + except socket.error, e: + sys.exc_clear() + if e[0] == socket.SSL_ERROR_WANT_WRITE: + return True + if self.state < 0: + self.disconnect() + return + self._on_send_failure() + return + if self._owner.sock_type == TYPE_CLIENT: + self.set_timeout(ACTIVITY_TIMEOUT_SECONDS) + return True + + def _plug_idle(self): + readable = self.state != 0 + if self.sendqueue or self.sendbuff: + writable = True + else: + writable = False + if self.writable != writable or self.readable != readable: + gajim.idlequeue.plug_idle(self, writable, readable) + + + def _on_send(self): + if self.sent_data and self.sent_data.strip(): + self.DEBUG(self.sent_data,'sent') + if hasattr(self._owner, 'Dispatcher'): + self._owner.Dispatcher.Event('', DATA_SENT, self.sent_data) + self.sent_data = None + + def _on_send_failure(self): + self.DEBUG("Socket error while sending data",'error') + self._owner.disconnected() + self.sent_data = None + + +class ClientZeroconf: + def __init__(self, caller): + self.caller = caller + self.zeroconf = None + self.roster = None + self.last_msg = '' + self.connections = {} + self.recipient_to_hash = {} + self.ip_to_hash = {} + + def test_avahi(self): + try: + import avahi + except ImportError: + return False + return True + + def connect(self, show, msg): + self.port = self.start_listener(self.caller.port) + if not self.port: + return + self.zeroconf_init(show, msg) + if not self.zeroconf.connect(): + self.disconnect() + return + self.roster = roster_zeroconf.Roster(self.zeroconf) + + def remove_announce(self): + if self.zeroconf: + return self.zeroconf.remove_announce() + + def announce(self): + if self.zeroconf: + return self.zeroconf.announce() + + def set_show_msg(self, show, msg): + if self.zeroconf: + self.zeroconf.txt['msg'] = msg + self.last_msg = msg + return self.zeroconf.update_txt(show) + + def resolve_all(self): + if self.zeroconf: + self.zeroconf.resolve_all() + + def reannounce(self, txt): + self.remove_announce() + self.zeroconf.txt = txt + self.zeroconf.port = self.port + self.zeroconf.username = self.caller.username + return self.announce() + + + def zeroconf_init(self, show, msg): + self.zeroconf = zeroconf.Zeroconf(self.caller._on_new_service, + self.caller._on_remove_service, self.caller._on_name_conflictCB, + self.caller._on_disconnected, self.caller.username, self.caller.host, + self.port) + self.zeroconf.txt['msg'] = msg + self.zeroconf.txt['status'] = show + self.zeroconf.txt['1st'] = self.caller.first + self.zeroconf.txt['last'] = self.caller.last + self.zeroconf.txt['jid'] = self.caller.jabber_id + self.zeroconf.txt['email'] = self.caller.email + self.zeroconf.username = self.caller.username + self.zeroconf.host = self.caller.host + self.zeroconf.port = self.port + self.last_msg = msg + + def disconnect(self): + if self.listener: + self.listener.disconnect() + self.listener = None + if self.zeroconf: + self.zeroconf.disconnect() + self.zeroconf = None + if self.roster: + self.roster.zeroconf = None + self.roster._data = None + self.roster = None + + def kill_all_connections(self): + for connection in self.connections.values(): + connection.force_disconnect() + + def add_connection(self, connection, ip, recipient): + sock_hash = connection.sock_hash + if sock_hash not in self.connections: + self.connections[sock_hash] = connection + self.ip_to_hash[ip] = sock_hash + if recipient: + self.recipient_to_hash[recipient] = sock_hash + + def remove_connection(self, sock_hash): + if sock_hash in self.connections: + del self.connections[sock_hash] + for i in self.recipient_to_hash: + if self.recipient_to_hash[i] == sock_hash: + del self.recipient_to_hash[i] + break + for i in self.ip_to_hash: + if self.ip_to_hash[i] == sock_hash: + del self.ip_to_hash[i] + break + + def start_listener(self, port): + for p in range(port, port + 5): + self.listener = ZeroconfListener(p, self) + self.listener.bind() + if self.listener.started: + return p + self.listener = None + return False + + def getRoster(self): + if self.roster: + return self.roster.getRoster() + return {} + + def send(self, msg_iq): + msg_iq.setFrom(self.roster.zeroconf.name) + to = msg_iq.getTo() + if to in self.recipient_to_hash: + conn = self.connections[self.recipient_to_hash[to]] + if conn.add_message(msg_iq): + return + try: + item = self.roster[to] + except KeyError: + #XXX invalid recipient, show some error maybe ? + return + if item['address'] in self.ip_to_hash: + conn = self.connections[self.ip_to_hash[item['address']]] + if conn.add_message(msg_iq): + return + P2PClient(None, item['address'], item['port'], self, [msg_iq], to) + diff --git a/src/common/zeroconf/connection_handlers_zeroconf.py b/src/common/zeroconf/connection_handlers_zeroconf.py new file mode 100644 index 000000000..a698107f6 --- /dev/null +++ b/src/common/zeroconf/connection_handlers_zeroconf.py @@ -0,0 +1,912 @@ +## +## Copyright (C) 2006 Gajim Team +## +## Contributors for this file: +## - Yann Le Boulanger +## - Nikos Kouremenos +## - Dimitur Kirov +## - Travis Shirk +## - Stefan Bethge +## +## 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. +## + +import os +import time +import base64 +import sha +import socket +import sys + +from calendar import timegm + +#import socks5 +import common.xmpp + +from common import GnuPG +from common import helpers +from common import gajim +from common.zeroconf import zeroconf +STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', + 'invisible'] +# kind of events we can wait for an answer +VCARD_PUBLISHED = 'vcard_published' +VCARD_ARRIVED = 'vcard_arrived' +AGENT_REMOVED = 'agent_removed' +HAS_IDLE = True +try: + import common.idle as idle # when we launch gajim from sources +except: + try: + import idle # when Gajim is installed + except: + gajim.log.debug(_('Unable to load idle module')) + HAS_IDLE = False + + +class ConnectionBytestream: + def __init__(self): + self.files_props = {} + + def is_transfer_stoped(self, file_props): + if file_props.has_key('error') and file_props['error'] != 0: + return True + if file_props.has_key('completed') and file_props['completed']: + return True + if file_props.has_key('connected') and file_props['connected'] == False: + return True + if not file_props.has_key('stopped') or not file_props['stopped']: + return False + return True + + def send_success_connect_reply(self, streamhost): + ''' send reply to the initiator of FT that we + made a connection + ''' + if streamhost is None: + return None + iq = common.xmpp.Iq(to = streamhost['initiator'], typ = 'result', + frm = streamhost['target']) + iq.setAttr('id', streamhost['id']) + query = iq.setTag('query') + query.setNamespace(common.xmpp.NS_BYTESTREAM) + stream_tag = query.setTag('streamhost-used') + stream_tag.setAttr('jid', streamhost['jid']) + self.connection.send(iq) + + def remove_transfers_for_contact(self, contact): + ''' stop all active transfer for contact ''' + for file_props in self.files_props.values(): + if self.is_transfer_stoped(file_props): + continue + receiver_jid = unicode(file_props['receiver']).split('/')[0] + if contact.jid == receiver_jid: + file_props['error'] = -5 + self.remove_transfer(file_props) + self.dispatch('FILE_REQUEST_ERROR', (contact.jid, file_props, '')) + sender_jid = unicode(file_props['sender']) + if contact.jid == sender_jid: + file_props['error'] = -3 + self.remove_transfer(file_props) + + def remove_all_transfers(self): + ''' stops and removes all active connections from the socks5 pool ''' + for file_props in self.files_props.values(): + self.remove_transfer(file_props, remove_from_list = False) + del(self.files_props) + self.files_props = {} + + def remove_transfer(self, file_props, remove_from_list = True): + if file_props is None: + return + self.disconnect_transfer(file_props) + sid = file_props['sid'] + gajim.socks5queue.remove_file_props(self.name, sid) + + if remove_from_list: + if self.files_props.has_key('sid'): + del(self.files_props['sid']) + + def disconnect_transfer(self, file_props): + if file_props is None: + return + if file_props.has_key('hash'): + gajim.socks5queue.remove_sender(file_props['hash']) + + if file_props.has_key('streamhosts'): + for host in file_props['streamhosts']: + if host.has_key('idx') and host['idx'] > 0: + gajim.socks5queue.remove_receiver(host['idx']) + gajim.socks5queue.remove_sender(host['idx']) + + def send_socks5_info(self, file_props, fast = True, receiver = None, + sender = None): + ''' send iq for the present streamhosts and proxies ''' + if type(self.peerhost) != tuple: + return + port = gajim.config.get('file_transfers_port') + ft_override_host_to_send = gajim.config.get('ft_override_host_to_send') + if receiver is None: + receiver = file_props['receiver'] + if sender is None: + sender = file_props['sender'] + proxyhosts = [] + sha_str = helpers.get_auth_sha(file_props['sid'], sender, + receiver) + file_props['sha_str'] = sha_str + if not ft_override_host_to_send: + ft_override_host_to_send = self.peerhost[0] + try: + ft_override_host_to_send = socket.gethostbyname( + ft_override_host_to_send) + except socket.gaierror: + self.dispatch('ERROR', (_('Wrong host'), _('The host you configured as the ft_override_host_to_send advanced option is not valid, so ignored.'))) + ft_override_host_to_send = self.peerhost[0] + listener = gajim.socks5queue.start_listener(port, + sha_str, self._result_socks5_sid, file_props['sid']) + if listener == 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 + + 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']) + streamhost = query.setTag('streamhost') + streamhost.setAttr('port', unicode(port)) + streamhost.setAttr('host', ft_override_host_to_send) + streamhost.setAttr('jid', sender) + self.connection.send(iq) + + def send_file_rejection(self, file_props): + ''' informs sender that we refuse to download the file ''' + # user response to ConfirmationDialog may come after we've disconneted + if not self.connection or self.connected < 2: + return + iq = common.xmpp.Protocol(name = 'iq', + to = unicode(file_props['sender']), typ = 'error') + iq.setAttr('id', file_props['request-id']) + err = common.xmpp.ErrorNode(code = '403', typ = 'cancel', name = + 'forbidden', text = 'Offer Declined') + iq.addChild(node=err) + self.connection.send(iq) + + def send_file_approval(self, file_props): + ''' send iq, confirming that we want to download the file ''' + # user response to ConfirmationDialog may come after we've disconneted + if not self.connection or self.connected < 2: + return + iq = common.xmpp.Protocol(name = 'iq', + to = unicode(file_props['sender']), typ = 'result') + iq.setAttr('id', file_props['request-id']) + si = iq.setTag('si') + si.setNamespace(common.xmpp.NS_SI) + if file_props.has_key('offset') and file_props['offset']: + file_tag = si.setTag('file') + file_tag.setNamespace(common.xmpp.NS_FILE) + range_tag = file_tag.setTag('range') + range_tag.setAttr('offset', file_props['offset']) + feature = si.setTag('feature') + feature.setNamespace(common.xmpp.NS_FEATURE) + _feature = common.xmpp.DataForm(typ='submit') + feature.addChild(node=_feature) + field = _feature.setField('stream-method') + field.delAttr('type') + field.setValue(common.xmpp.NS_BYTESTREAM) + self.connection.send(iq) + + 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 file_props.has_key('desc'): + 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 _result_socks5_sid(self, sid, hash_id): + ''' store the result of sha message from auth. ''' + if not self.files_props.has_key(sid): + return + file_props = self.files_props[sid] + file_props['hash'] = hash_id + return + + def _connect_error(self, to, _id, sid, code = 404): + ''' cb, when there is an error establishing BS connection, or + when connection is rejected''' + msg_dict = { + 404: 'Could not connect to given hosts', + 405: 'Cancel', + 406: 'Not acceptable', + } + msg = msg_dict[code] + iq = None + iq = common.xmpp.Protocol(name = 'iq', to = to, + typ = 'error') + iq.setAttr('id', _id) + err = iq.setTag('error') + err.setAttr('code', unicode(code)) + err.setData(msg) + self.connection.send(iq) + if code == 404: + file_props = gajim.socks5queue.get_file_props(self.name, sid) + if file_props is not None: + self.disconnect_transfer(file_props) + file_props['error'] = -3 + self.dispatch('FILE_REQUEST_ERROR', (to, file_props, msg)) + + def _proxy_auth_ok(self, proxy): + '''cb, called after authentication to proxy server ''' + file_props = self.files_props[proxy['sid']] + iq = common.xmpp.Protocol(name = 'iq', to = proxy['initiator'], + typ = 'set') + auth_id = "au_" + proxy['sid'] + iq.setID(auth_id) + query = iq.setTag('query') + query.setNamespace(common.xmpp.NS_BYTESTREAM) + query.setAttr('sid', proxy['sid']) + activate = query.setTag('activate') + activate.setData(file_props['proxy_receiver']) + iq.setID(auth_id) + self.connection.send(iq) + + # register xmpppy handlers for bytestream and FT stanzas + def _bytestreamErrorCB(self, con, iq_obj): + gajim.log.debug('_bytestreamErrorCB') + id = unicode(iq_obj.getAttr('id')) + frm = unicode(iq_obj.getFrom()) + query = iq_obj.getTag('query') + gajim.proxy65_manager.error_cb(frm, query) + jid = unicode(iq_obj.getFrom()) + id = id[3:] + if not self.files_props.has_key(id): + return + file_props = self.files_props[id] + file_props['error'] = -4 + self.dispatch('FILE_REQUEST_ERROR', (jid, file_props, '')) + raise common.xmpp.NodeProcessed + + def _bytestreamSetCB(self, con, iq_obj): + gajim.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 self.files_props.has_key(sid): + file_props = self.files_props[sid] + file_props['fast'] = streamhosts + if file_props['type'] == 's': + if file_props.has_key('streamhosts'): + 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 + + 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): + gajim.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 real_id[:3] != 'au_': + return + frm = unicode(iq_obj.getFrom()) + id = real_id[3:] + if self.files_props.has_key(id): + file_props = self.files_props[id] + if file_props['streamhost-used']: + for host in file_props['proxyhosts']: + if host['initiator'] == frm and host.has_key('idx'): + gajim.socks5queue.activate_proxy(host['idx']) + raise common.xmpp.NodeProcessed + + def _bytestreamResultCB(self, con, iq_obj): + gajim.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: # this bytestream result is not what we need + pass + id = real_id[3:] + if self.files_props.has_key(id): + file_props = self.files_props[id] + else: + raise common.xmpp.NodeProcessed + if streamhost is None: + # proxy approves the activate query + if real_id[:3] == 'au_': + id = real_id[3:] + if not file_props.has_key('streamhost-used') or \ + file_props['streamhost-used'] is False: + raise common.xmpp.NodeProcessed + if not file_props.has_key('proxyhosts'): + 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 file_props.has_key('streamhost-used') and \ + file_props['streamhost-used'] is True: + raise common.xmpp.NodeProcessed + + if real_id[:3] == 'au_': + gajim.socks5queue.send_file(file_props, self.name) + raise common.xmpp.NodeProcessed + + proxy = None + if file_props.has_key('proxyhosts'): + for proxyhost in file_props['proxyhosts']: + if proxyhost['jid'] == jid: + proxy = proxyhost + + if proxy != None: + file_props['streamhost-used'] = True + if not file_props.has_key('streamhosts'): + 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 file_props.has_key('fast'): + 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): + gajim.log.debug('_siResultCB') + self.peerhost = con._owner.Connection._sock.getsockname() + id = iq_obj.getAttr('id') + if not self.files_props.has_key(id): + # no such jid + return + file_props = self.files_props[id] + if file_props is None: + # file properties for jid is none + return + if file_props.has_key('request-id'): + # 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): + gajim.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')) + 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): + gajim.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 not self.files_props.has_key(id): + # 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 ConnectionVcard: + def __init__(self): + self.vcard_sha = None + self.vcard_shas = {} # sha of contacts + self.room_jids = [] # list of gc jids so that vcard are saved in a folder + + def add_sha(self, p, send_caps = True): + ''' + c = p.setTag('x', namespace = common.xmpp.NS_VCARD_UPDATE) + if self.vcard_sha is not None: + c.setTagData('photo', self.vcard_sha) + if send_caps: + return self.add_caps(p) + return p + ''' + pass + + def add_caps(self, p): + ''' + # advertise our capabilities in presence stanza (jep-0115) + c = p.setTag('c', namespace = common.xmpp.NS_CAPS) + c.setAttr('node', 'http://gajim.org/caps') + c.setAttr('ext', 'ftrans') + c.setAttr('ver', gajim.version) + return p + ''' + pass + + def node_to_dict(self, node): + dict = {} + + for info in node.getChildren(): + name = info.getName() + if name in ('ADR', 'TEL', 'EMAIL'): # we can have several + if not dict.has_key(name): + dict[name] = [] + entry = {} + for c in info.getChildren(): + entry[c.getName()] = c.getData() + dict[name].append(entry) + elif info.getChildren() == []: + dict[name] = info.getData() + else: + dict[name] = {} + for c in info.getChildren(): + dict[name][c.getName()] = c.getData() + + return dict + + def save_vcard_to_hd(self, full_jid, card): + jid, nick = gajim.get_room_and_nick_from_fjid(full_jid) + puny_jid = helpers.sanitize_filename(jid) + path = os.path.join(gajim.VCARD_PATH, puny_jid) + if jid in self.room_jids or os.path.isdir(path): + # remove room_jid file if needed + if os.path.isfile(path): + os.remove(path) + # create folder if needed + if not os.path.isdir(path): + os.mkdir(path, 0700) + puny_nick = helpers.sanitize_filename(nick) + path_to_file = os.path.join(gajim.VCARD_PATH, puny_jid, puny_nick) + else: + path_to_file = path + fil = open(path_to_file, 'w') + fil.write(str(card)) + fil.close() + + def get_cached_vcard(self, fjid, is_fake_jid = False): + '''return the vcard as a dict + return {} if vcard was too old + return None if we don't have cached vcard''' + jid, nick = gajim.get_room_and_nick_from_fjid(fjid) + puny_jid = helpers.sanitize_filename(jid) + if is_fake_jid: + puny_nick = helpers.sanitize_filename(nick) + path_to_file = os.path.join(gajim.VCARD_PATH, puny_jid, puny_nick) + else: + path_to_file = os.path.join(gajim.VCARD_PATH, puny_jid) + if not os.path.isfile(path_to_file): + return None + # We have the vcard cached + f = open(path_to_file) + c = f.read() + f.close() + card = common.xmpp.Node(node = c) + vcard = self.node_to_dict(card) + if vcard.has_key('PHOTO'): + if not isinstance(vcard['PHOTO'], dict): + del vcard['PHOTO'] + elif vcard['PHOTO'].has_key('SHA'): + cached_sha = vcard['PHOTO']['SHA'] + if self.vcard_shas.has_key(jid) and self.vcard_shas[jid] != \ + cached_sha: + # user change his vcard so don't use the cached one + return {} + vcard['jid'] = jid + vcard['resource'] = gajim.get_resource_from_jid(fjid) + return vcard + + def request_vcard(self, jid = None, is_fake_jid = False): + '''request the VCARD. If is_fake_jid is True, it means we request a vcard + to a fake jid, like in private messages in groupchat''' + if not self.connection: + return + ''' + iq = common.xmpp.Iq(typ = 'get') + if jid: + iq.setTo(jid) + iq.setTag(common.xmpp.NS_VCARD + ' vCard') + + id = self.connection.getAnID() + iq.setID(id) + self.awaiting_answers[id] = (VCARD_ARRIVED, jid) + if is_fake_jid: + room_jid, nick = gajim.get_room_and_nick_from_fjid(jid) + if not room_jid in self.room_jids: + self.room_jids.append(room_jid) + self.connection.send(iq) + #('VCARD', {entry1: data, entry2: {entry21: data, ...}, ...}) + ''' + pass + + def send_vcard(self, vcard): + if not self.connection: + return + ''' + iq = common.xmpp.Iq(typ = 'set') + iq2 = iq.setTag(common.xmpp.NS_VCARD + ' vCard') + for i in vcard: + if i == 'jid': + continue + if isinstance(vcard[i], dict): + iq3 = iq2.addChild(i) + for j in vcard[i]: + iq3.addChild(j).setData(vcard[i][j]) + elif type(vcard[i]) == type([]): + for j in vcard[i]: + iq3 = iq2.addChild(i) + for k in j: + iq3.addChild(k).setData(j[k]) + else: + iq2.addChild(i).setData(vcard[i]) + + id = self.connection.getAnID() + iq.setID(id) + self.connection.send(iq) + + # Add the sha of the avatar + if vcard.has_key('PHOTO') and isinstance(vcard['PHOTO'], dict) and \ + vcard['PHOTO'].has_key('BINVAL'): + photo = vcard['PHOTO']['BINVAL'] + photo_decoded = base64.decodestring(photo) + our_jid = gajim.get_jid_from_account(self.name) + gajim.interface.save_avatar_files(our_jid, photo_decoded) + avatar_sha = sha.sha(photo_decoded).hexdigest() + iq2.getTag('PHOTO').setTagData('SHA', avatar_sha) + + self.awaiting_answers[id] = (VCARD_PUBLISHED, iq2) + ''' + pass + +class ConnectionHandlersZeroconf(ConnectionVcard, ConnectionBytestream): + def __init__(self): + ConnectionVcard.__init__(self) + ConnectionBytestream.__init__(self) + # List of IDs we are waiting answers for {id: (type_of_request, data), } + self.awaiting_answers = {} + # List of IDs that will produce a timeout is answer doesn't arrive + # {time_of_the_timeout: (id, message to send to gui), } + self.awaiting_timeouts = {} + # keep the jids we auto added (transports contacts) to not send the + # SUBSCRIBED event to gui + self.automatically_added = [] + try: + idle.init() + except: + HAS_IDLE = False + + def _messageCB(self, ip, con, msg): + '''Called when we receive a message''' + msgtxt = msg.getBody() + msghtml = msg.getXHTML() + mtype = msg.getType() + subject = msg.getSubject() # if not there, it's None + tim = msg.getTimestamp() + tim = time.strptime(tim, '%Y%m%dT%H:%M:%S') + tim = time.localtime(timegm(tim)) + frm = msg.getFrom() + if frm == None: + for key in self.connection.zeroconf.contacts: + if ip == self.connection.zeroconf.contacts[key][zeroconf.C_ADDRESS]: + frm = key + frm = str(frm) + jid = frm + no_log_for = gajim.config.get_per('accounts', self.name, + 'no_log_for').split() + encrypted = False + chatstate = None + encTag = msg.getTag('x', namespace = common.xmpp.NS_ENCRYPTED) + decmsg = '' + # invitations + invite = None + if not encTag: + invite = msg.getTag('x', namespace = common.xmpp.NS_MUC_USER) + if invite and not invite.getTag('invite'): + invite = None + delayed = msg.getTag('x', namespace = common.xmpp.NS_DELAY) != None + msg_id = None + composing_jep = None + # FIXME: Msn transport (CMSN1.2.1 and PyMSN0.10) do NOT RECOMMENDED + # invitation + # stanza (MUC JEP) remove in 2007, as we do not do NOT RECOMMENDED + xtags = msg.getTags('x') + # chatstates - look for chatstate tags in a message if not delayed + if not delayed: + composing_jep = False + children = msg.getChildren() + for child in children: + if child.getNamespace() == 'http://jabber.org/protocol/chatstates': + chatstate = child.getName() + composing_jep = 'JEP-0085' + break + # No JEP-0085 support, fallback to JEP-0022 + if not chatstate: + chatstate_child = msg.getTag('x', namespace = common.xmpp.NS_EVENT) + if chatstate_child: + chatstate = 'active' + composing_jep = 'JEP-0022' + if not msgtxt and chatstate_child.getTag('composing'): + chatstate = 'composing' + # JEP-0172 User Nickname + user_nick = msg.getTagData('nick') + if not user_nick: + user_nick = '' + + if encTag and GnuPG.USE_GPG: + #decrypt + encmsg = encTag.getData() + + keyID = gajim.config.get_per('accounts', self.name, 'keyid') + if keyID: + decmsg = self.gpg.decrypt(encmsg, keyID) + if decmsg: + msgtxt = decmsg + encrypted = True + if mtype == 'error': + error_msg = msg.getError() + if not error_msg: + error_msg = msgtxt + msgtxt = None + if self.name not in no_log_for: + gajim.logger.write('error', frm, error_msg, tim = tim, + subject = subject) + self.dispatch('MSGERROR', (frm, msg.getErrorCode(), error_msg, msgtxt, + tim)) + elif mtype == 'chat': # it's type 'chat' + if not msg.getTag('body') and chatstate is None: #no + return + if msg.getTag('body') and self.name not in no_log_for and jid not in\ + no_log_for and msgtxt: + msg_id = gajim.logger.write('chat_msg_recv', frm, msgtxt, tim = tim, + subject = subject) + self.dispatch('MSG', (frm, msgtxt, tim, encrypted, mtype, subject, + chatstate, msg_id, composing_jep, user_nick, msghtml)) + elif mtype == 'normal': # it's single message + if self.name not in no_log_for and jid not in no_log_for and msgtxt: + gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim, + subject = subject) + if invite: + self.dispatch('MSG', (frm, msgtxt, tim, encrypted, 'normal', + subject, chatstate, msg_id, composing_jep, user_nick)) + # END messageCB + ''' + def build_http_auth_answer(self, iq_obj, answer): + if answer == 'yes': + iq = iq_obj.buildReply('result') + elif answer == 'no': + iq = iq_obj.buildReply('error') + iq.setError('not-authorized', 401) + self.connection.send(iq) + ''' + + def parse_data_form(self, node): + dic = {} + tag = node.getTag('title') + if tag: + dic['title'] = tag.getData() + tag = node.getTag('instructions') + if tag: + dic['instructions'] = tag.getData() + i = 0 + for child in node.getChildren(): + if child.getName() != 'field': + continue + var = child.getAttr('var') + ctype = child.getAttr('type') + label = child.getAttr('label') + if not var and ctype != 'fixed': # We must have var if type != fixed + continue + dic[i] = {} + if var: + dic[i]['var'] = var + if ctype: + dic[i]['type'] = ctype + if label: + dic[i]['label'] = label + tags = child.getTags('value') + if len(tags): + dic[i]['values'] = [] + for tag in tags: + data = tag.getData() + if ctype == 'boolean': + if data in ('yes', 'true', 'assent', '1'): + data = True + else: + data = False + dic[i]['values'].append(data) + tag = child.getTag('desc') + if tag: + dic[i]['desc'] = tag.getData() + option_tags = child.getTags('option') + if len(option_tags): + dic[i]['options'] = {} + j = 0 + for option_tag in option_tags: + dic[i]['options'][j] = {} + label = option_tag.getAttr('label') + tags = option_tag.getTags('value') + dic[i]['options'][j]['values'] = [] + for tag in tags: + dic[i]['options'][j]['values'].append(tag.getData()) + if not label: + label = dic[i]['options'][j]['values'][0] + dic[i]['options'][j]['label'] = label + j += 1 + if not dic[i].has_key('values'): + dic[i]['values'] = [dic[i]['options'][0]['values'][0]] + i += 1 + return dic + + def store_metacontacts(self, tags): + ''' fake empty method ''' + # serverside metacontacts are not supported with zeroconf + # (there is no server) + pass + def remove_transfers_for_contact(self, contact): + ''' stop all active transfer for contact ''' + '''for file_props in self.files_props.values(): + if self.is_transfer_stoped(file_props): + continue + receiver_jid = unicode(file_props['receiver']).split('/')[0] + if contact.jid == receiver_jid: + file_props['error'] = -5 + self.remove_transfer(file_props) + self.dispatch('FILE_REQUEST_ERROR', (contact.jid, file_props)) + sender_jid = unicode(file_props['sender']).split('/')[0] + if contact.jid == sender_jid: + file_props['error'] = -3 + self.remove_transfer(file_props) + ''' + pass + + def remove_all_transfers(self): + ''' stops and removes all active connections from the socks5 pool ''' + ''' + for file_props in self.files_props.values(): + self.remove_transfer(file_props, remove_from_list = False) + del(self.files_props) + self.files_props = {} + ''' + pass + + def remove_transfer(self, file_props, remove_from_list = True): + ''' + if file_props is None: + return + self.disconnect_transfer(file_props) + sid = file_props['sid'] + gajim.socks5queue.remove_file_props(self.name, sid) + + if remove_from_list: + if self.files_props.has_key('sid'): + del(self.files_props['sid']) + ''' + pass + diff --git a/src/common/zeroconf/connection_zeroconf.py b/src/common/zeroconf/connection_zeroconf.py new file mode 100644 index 000000000..c75ea4331 --- /dev/null +++ b/src/common/zeroconf/connection_zeroconf.py @@ -0,0 +1,487 @@ +## common/zeroconf/connection_zeroconf.py +## +## Contributors for this file: +## - Yann Le Boulanger +## - Nikos Kouremenos +## - Dimitur Kirov +## - Travis Shirk +## - Stefan Bethge +## +## Copyright (C) 2003-2004 Yann Le Boulanger +## Vincent Hanquez +## Copyright (C) 2006 Yann Le Boulanger +## Vincent Hanquez +## Nikos Kouremenos +## Dimitur Kirov +## Travis Shirk +## Norman Rasmussen +## Stefan Bethge +## +## 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. +## + + +import os +import random +random.seed() + +import signal +if os.name != 'nt': + signal.signal(signal.SIGPIPE, signal.SIG_DFL) +import getpass +import gobject +import notify + +from common import helpers +from common import gajim +from common import GnuPG +from common.zeroconf import connection_handlers_zeroconf +from common.zeroconf import client_zeroconf +from connection_handlers_zeroconf import * + +USE_GPG = GnuPG.USE_GPG + +class ConnectionZeroconf(ConnectionHandlersZeroconf): + '''Connection class''' + def __init__(self, name): + ConnectionHandlersZeroconf.__init__(self) + # system username + self.username = None + self.name = name + self.connected = 0 # offline + self.connection = None + self.gpg = None + self.is_zeroconf = True + self.privacy_rules_supported = 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 + self.password = 'zeroconf' + + self.autoconnect = False + self.sync_with_global_status = True + self.no_log_for = False + + # Do we continue connection when we get roster (send presence,get vcard...) + self.continue_connect_info = None + if USE_GPG: + self.gpg = GnuPG.GnuPG() + gajim.config.set('usegpg', True) + else: + gajim.config.set('usegpg', False) + + self.get_config_values_or_default() + + self.muc_jid = {} # jid of muc server for each transport type + self.vcard_supported = False + + def _on_name_conflictCB(self, alt_name): + self.disconnect() + self.dispatch('STATUS', 'offline') + self.dispatch('ZC_NAME_CONFLICT', alt_name) + + def get_config_values_or_default(self): + ''' get name, host, port from config, or + create zeroconf account with default values''' + + if not self.username: + self.username = unicode(getpass.getuser()) + 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') + + if not gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'name'): + print '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) + + #XXX make sure host is US-ASCII + self.host = unicode(socket.gethostname()) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'hostname', self.host) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'custom_port', 5298) + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'is_zeroconf', True) + self.host = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'hostname') + 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') + # END __init__ + + def dispatch(self, event, data): + if gajim.handlers.has_key(event): + gajim.handlers[event](self.name, data) + + def _reconnect(self): + gajim.log.debug('reconnect') + + signed = self.get_signed_msg(self.status) + self.reconnect() + + 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 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)) + elif self.gpg.passphrase is not None or use_gpg_agent: + signed = self.gpg.sign(msg, keyID) + if signed == 'BAD_PASSPHRASE': + signed = '' + if self.connected < 2: + self.dispatch('BAD_PASSPHRASE', ()) + return signed + + def _on_resolve_timeout(self): + if self.connected: + self.connection.resolve_all() + diffs = self.roster.getDiffs() + for key in diffs: + self.roster.setItem(key) + self.dispatch('ROSTER_INFO', (key, self.roster.getName(key), + 'both', 'no', self.roster.getGroups(key))) + self.dispatch('NOTIFY', (key, self.roster.getStatus(key), + self.roster.getMessage(key), 'local', 0, None, 0)) + #XXX open chat windows don't get refreshed (full name), add that + return self.call_resolve_timeout + + # 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)) + + def _on_remove_service(self, jid): + self.roster.delItem(jid) + # 'NOTIFY' (account, (jid, status, status message, resource, priority, + # keyID, timestamp)) + self.dispatch('NOTIFY', (jid, 'offline', '', 'local', 0, None, 0)) + + 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 connect(self, show = 'online', msg = ''): + self.get_config_values_or_default() + if not self.connection: + self.connection = client_zeroconf.ClientZeroconf(self) + if not self.connection.test_avahi(): + self.dispatch('STATUS', 'offline') + self.status = 'offline' + self.dispatch('CONNECTION_LOST', + (_('Could not connect to "%s"') % self.name, + _('Please check if Avahi is installed.'))) + self.disconnect() + return + self.connection.connect(show, msg) + if not self.connection.listener: + self.dispatch('STATUS', 'offline') + self.status = 'offline' + self.dispatch('CONNECTION_LOST', + (_('Could not start local service'), + _('Please check if avahi-daemon is running.'))) + self.disconnect() + return + else: + self.connection.announce() + self.roster = self.connection.getRoster() + self.dispatch('ROSTER', self.roster) + + #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)) + + self.connected = STATUS_LIST.index(show) + + # refresh all contacts data every five seconds + self.call_resolve_timeout = True + gobject.timeout_add(5000, self._on_resolve_timeout) + return True + + def disconnect(self, on_purpose = False): + self.connected = 0 + self.time_to_reconnect = None + if self.connection: + self.connection.disconnect() + self.connection = None + # stop calling the timeout + self.call_resolve_timeout = False + + 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') + self.connection.reannounce(txt) + + def update_details(self): + if self.connection: + 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 + self.disconnect() + if not self.connect(self.status, last_msg): + return + if self.status != 'invisible': + self.connection.announce() + 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) + + # 'disconnect' + elif show == 'offline' and self.connected: + self.disconnect() + self.dispatch('STATUS', 'offline') + + # update status + elif show != 'offline' and self.connected: + was_invisible = self.connected == STATUS_LIST.index('invisible') + self.connected = STATUS_LIST.index(show) + if show == 'invisible': + check = check and self.connection.remove_announce() + elif was_invisible: + check = check and self.connection.announce() + if self.connection and not show == 'invisible': + check = check and self.connection.set_show_msg(show, msg) + + #stay offline when zeroconf does something wrong + if check: + 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 get_status(self): + return STATUS_LIST[self.connected] + + def send_message(self, jid, msg, keyID, type = 'chat', subject='', + chatstate = None, msg_id = None, composing_jep = None, resource = None, + user_nick = None): + fjid = jid + + if not self.connection: + return + if not msg and chatstate is None: + return + + msgtxt = msg + msgenc = '' + if keyID and USE_GPG: + #encrypt + msgenc = self.gpg.encrypt(msg, [keyID]) + if msgenc: + 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 + + + if type == 'chat': + msg_iq = common.xmpp.Message(to = fjid, body = msgtxt, typ = type) + + else: + if subject: + msg_iq = common.xmpp.Message(to = fjid, body = msgtxt, + typ = 'normal', subject = subject) + else: + msg_iq = common.xmpp.Message(to = fjid, body = msgtxt, + typ = 'normal') + + 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_jep == 'JEP-0085' or not composing_jep: + # JEP-0085 + msg_iq.setTag(chatstate, namespace = common.xmpp.NS_CHATSTATES) + if composing_jep == 'JEP-0022' or not composing_jep: + # 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') + + self.connection.send(msg_iq) + no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for') + ji = gajim.get_jid_without_resource(jid) + if self.name not in no_log_for and ji not in no_log_for: + log_msg = msg + if subject: + log_msg = _('Subject: %s\n%s') % (subject, msg) + if log_msg: + if type == 'chat': + kind = 'chat_msg_sent' + else: + kind = 'single_msg_sent' + gajim.logger.write(kind, jid, log_msg) + + self.dispatch('MSGSENT', (jid, msg, keyID)) + + def send_stanza(self, stanza): + # send a stanza untouched + print 'connection_zeroconf.py: send_stanza' + if not self.connection: + return + #self.connection.send(stanza) + pass + + 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 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 USE_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 USE_GPG: + keys = self.gpg.get_keys() + return keys + return None + + def ask_gpg_secrete_keys(self): + if USE_GPG: + keys = self.gpg.get_secret_keys() + return keys + return None + + def _event_dispatcher(self, realm, event, data): + if realm == '': + if event == common.xmpp.transports.DATA_RECEIVED: + self.dispatch('STANZA_ARRIVED', unicode(data, errors = 'ignore')) + elif event == common.xmpp.transports.DATA_SENT: + self.dispatch('STANZA_SENT', unicode(data)) + +# END ConnectionZeroconf diff --git a/src/common/zeroconf/roster_zeroconf.py b/src/common/zeroconf/roster_zeroconf.py new file mode 100644 index 000000000..7924e70f7 --- /dev/null +++ b/src/common/zeroconf/roster_zeroconf.py @@ -0,0 +1,152 @@ +## common/zeroconf/roster_zeroconf.py +## +## Copyright (C) 2006 Stefan Bethge +## +## 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. +## + + +from common.zeroconf import zeroconf + +class Roster: + def __init__(self, zeroconf): + self._data = None + self.zeroconf = zeroconf # our zeroconf instance + + def update_roster(self): + for val in self.zeroconf.contacts.values(): + self.setItem(val[zeroconf.C_NAME]) + + def getRoster(self): + #print 'roster_zeroconf.py: getRoster' + if self._data is None: + self._data = {} + self.update_roster() + return self + + def getDiffs(self): + ''' update the roster with new data and return dict with + jid -> new status pairs to do notifications and stuff ''' + + diffs = {} + old_data = self._data.copy() + self.update_roster() + for key in old_data.keys(): + if self._data.has_key(key): + if old_data[key] != self._data[key]: + diffs[key] = self._data[key]['status'] + #print 'roster_zeroconf.py: diffs:' + str(diffs) + return diffs + + def setItem(self, jid, name = '', groups = ''): + #print 'roster_zeroconf.py: setItem %s' % jid + (service_jid, domain, interface, protocol, host, address, port, bare_jid, txt) \ + = self.zeroconf.get_contact(jid) + + self._data[jid]={} + self._data[jid]['ask'] = 'no' #? + self._data[jid]['subscription'] = 'both' + self._data[jid]['groups'] = [] + self._data[jid]['resources'] = {} + self._data[jid]['address'] = address + self._data[jid]['host'] = host + self._data[jid]['port'] = port + txt_dict = self.zeroconf.txt_array_to_dict(txt) + if txt_dict.has_key('status'): + status = txt_dict['status'] + else: + status = '' + nm = '' + if txt_dict.has_key('1st'): + nm = txt_dict['1st'] + if txt_dict.has_key('last'): + if nm != '': + nm += ' ' + nm += txt_dict['last'] + if nm: + self._data[jid]['name'] = nm + else: + self._data[jid]['name'] = jid + if status == 'avail': + status = 'online' + self._data[jid]['txt_dict'] = txt_dict + if not self._data[jid]['txt_dict'].has_key('msg'): + self._data[jid]['txt_dict']['msg'] = '' + self._data[jid]['status'] = status + self._data[jid]['show'] = status + + def delItem(self, jid): + #print 'roster_zeroconf.py: delItem %s' % jid + if self._data.has_key(jid): + del self._data[jid] + + def getItem(self, jid): + #print 'roster_zeroconf.py: getItem: %s' % jid + if self._data.has_key(jid): + return self._data[jid] + + def __getitem__(self,jid): + #print 'roster_zeroconf.py: __getitem__' + return self._data[jid] + + def getItems(self): + #print 'roster_zeroconf.py: getItems' + # Return list of all [bare] JIDs that the roster currently tracks. + return self._data.keys() + + def keys(self): + #print 'roster_zeroconf.py: keys' + return self._data.keys() + + def getRaw(self): + #print 'roster_zeroconf.py: getRaw' + return self._data + + def getResources(self, jid): + #print 'roster_zeroconf.py: getResources(%s)' % jid + return {} + + def getGroups(self, jid): + return self._data[jid]['groups'] + + def getName(self, jid): + if self._data.has_key(jid): + return self._data[jid]['name'] + + def getStatus(self, jid): + if self._data.has_key(jid): + return self._data[jid]['status'] + + def getMessage(self, jid): + if self._data.has_key(jid): + return self._data[jid]['txt_dict']['msg'] + + def getShow(self, jid): + #print 'roster_zeroconf.py: getShow' + return getStatus(jid) + + def getPriority(jid): + return 5 + + def getSubscription(self,jid): + #print 'roster_zeroconf.py: getSubscription' + return 'both' + + def Subscribe(self,jid): + pass + + def Unsubscribe(self,jid): + pass + + def Authorize(self,jid): + pass + + def Unauthorize(self,jid): + pass diff --git a/src/common/zeroconf/zeroconf.py b/src/common/zeroconf/zeroconf.py new file mode 100755 index 000000000..b18fdb28c --- /dev/null +++ b/src/common/zeroconf/zeroconf.py @@ -0,0 +1,373 @@ +## common/zeroconf/zeroconf.py +## +## Copyright (C) 2006 Stefan Bethge +## +## 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. +## + +import os +import sys +import socket +from common import gajim +from common import xmpp + +try: + import avahi, gobject, dbus +except ImportError: + gajim.log.debug('Error: python-avahi and python-dbus need to be installed. No zeroconf support.') + +try: + import dbus.glib +except ImportError, e: + pass + + +C_NAME, C_DOMAIN, C_INTERFACE, C_PROTOCOL, C_HOST, \ +C_ADDRESS, C_PORT, C_BARE_NAME, C_TXT = range(9) + +class Zeroconf: + def __init__(self, new_serviceCB, remove_serviceCB, name_conflictCB, + disconnected_CB, name, host, port): + self.server = None + self.domain = None # specific domain to browse + self.stype = '_presence._tcp' + self.port = port # listening port that gets announced + self.username = name + self.host = host + self.txt = {} # service data + + #XXX these CBs should be set to None when we destroy the object + # (go offline), because they create a circular reference + self.new_serviceCB = new_serviceCB + self.remove_serviceCB = remove_serviceCB + self.name_conflictCB = name_conflictCB + self.disconnected_CB = disconnected_CB + + self.service_browser = None + self.domain_browser = None + self.server = None + self.contacts = {} # all current local contacts with data + self.entrygroup = None + self.connected = False + self.announced = False + self.invalid_self_contact = {} + + + ## handlers for dbus callbacks + def entrygroup_commit_error_CB(self, err): + # left for eventual later use + pass + + def error_callback1(self, err): + gajim.log.debug('RR' + str(err)) + + def error_callback(self, err): + gajim.log.debug(str(err)) + # timeouts are non-critical + if str(err) != 'Timeout reached': + self.disconnect() + self.disconnected_CB() + + def new_service_callback(self, interface, protocol, name, stype, domain, flags): + gajim.log.debug('Found service %s in domain %s on %i.%i.' % (name, domain, interface, protocol)) + # if not self.connected: + # return + + # synchronous resolving + self.server.ResolveService( int(interface), int(protocol), name, stype, \ + domain, avahi.PROTO_UNSPEC, dbus.UInt32(0), \ + reply_handler=self.service_resolved_callback, error_handler=self.error_callback1) + + def remove_service_callback(self, interface, protocol, name, stype, domain, flags): + gajim.log.debug('Service %s in domain %s on %i.%i disappeared.' % (name, domain, interface, protocol)) + # if not self.connected: + # return + if name != self.name: + for key in self.contacts.keys(): + if self.contacts[key][C_BARE_NAME] == name: + del self.contacts[key] + self.remove_serviceCB(key) + return + + def new_service_type(self, interface, protocol, stype, domain, flags): + # Are we already browsing this domain for this type? + if self.service_browser: + return + + object_path = self.server.ServiceBrowserNew(interface, protocol, \ + stype, domain, dbus.UInt32(0)) + + self.service_browser = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, \ + object_path) , avahi.DBUS_INTERFACE_SERVICE_BROWSER) + self.service_browser.connect_to_signal('ItemNew', self.new_service_callback) + self.service_browser.connect_to_signal('ItemRemove', self.remove_service_callback) + self.service_browser.connect_to_signal('Failure', self.error_callback) + + def new_domain_callback(self,interface, protocol, domain, flags): + if domain != "local": + self.browse_domain(interface, protocol, domain) + + def txt_array_to_dict(self,txt_array): + items = {} + + for byte_array in txt_array: + # 'str' is used for string type in python + value = avahi.byte_array_to_string(byte_array) + poseq = value.find('=') + items[value[:poseq]] = value[poseq+1:] + return items + + def service_resolved_callback(self, interface, protocol, name, stype, domain, host, aprotocol, address, port, txt, flags): + gajim.log.debug('Service data for service %s in domain %s on %i.%i:' % (name, domain, interface, protocol)) + gajim.log.debug('Host %s (%s), port %i, TXT data: %s' % (host, address, port, avahi.txt_array_to_string_array(txt))) + if not self.connected: + return + bare_name = name + if name.find('@') == -1: + name = name + '@' + name + + # we don't want to see ourselves in the list + if name != self.name: + self.contacts[name] = (name, domain, interface, protocol, host, address, port, + bare_name, txt) + self.new_serviceCB(name) + else: + # remember data + # In case this is not our own record but of another + # gajim instance on the same machine, + # it will be used when we get a new name. + self.invalid_self_contact[name] = (name, domain, interface, protocol, host, address, port, bare_name, txt) + + + # different handler when resolving all contacts + def service_resolved_all_callback(self, interface, protocol, name, stype, domain, host, aprotocol, address, port, txt, flags): + if not self.connected: + return + bare_name = name + if name.find('@') == -1: + name = name + '@' + name + self.contacts[name] = (name, domain, interface, protocol, host, address, port, bare_name, txt) + + def service_added_callback(self): + gajim.log.debug('Service successfully added') + + def service_committed_callback(self): + gajim.log.debug('Service successfully committed') + + def service_updated_callback(self): + gajim.log.debug('Service successfully updated') + + def service_add_fail_callback(self, err): + gajim.log.debug('Error while adding service. %s' % str(err)) + alternative_name = self.server.GetAlternativeServiceName(self.username) + self.disconnect() + self.name_conflictCB(alternative_name) + + def server_state_changed_callback(self, state, error): + print 'server.state %s' % state + if state == avahi.SERVER_RUNNING: + self.create_service() + elif state == avahi.SERVER_COLLISION: + self.entrygroup.Reset() + elif state == avahi.CLIENT_FAILURE: # TODO: add error handling (avahi daemon dies...?) + print 'CLIENT FAILURE' + + def entrygroup_state_changed_callback(self, state, error): + # the name is already present, so recreate + if state == avahi.ENTRY_GROUP_COLLISION: + self.service_add_fail_callback('Local name collision, recreating.') + elif state == avahi.ENTRY_GROUP_FAILURE: + print 'zeroconf.py: ENTRY_GROUP_FAILURE reached(that should not happen)' + + # make zeroconf-valid names + def replace_show(self, show): + if show in ['chat', 'online', '']: + return 'avail' + elif show == 'xa': + return 'away' + return show + + def create_service(self): + try: + if not self.entrygroup: + # create an EntryGroup for publishing + self.entrygroup = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, self.server.EntryGroupNew()), avahi.DBUS_INTERFACE_ENTRY_GROUP) + self.entrygroup.connect_to_signal('StateChanged', self.entrygroup_state_changed_callback) + + txt = {} + + #remove empty keys + for key,val in self.txt.iteritems(): + if val: + txt[key] = val + + txt['port.p2pj'] = self.port + txt['version'] = 1 + txt['txtvers'] = 1 + + # replace gajim's show messages with compatible ones + if self.txt.has_key('status'): + txt['status'] = self.replace_show(self.txt['status']) + else: + txt['status'] = 'avail' + + self.txt = txt + gajim.log.debug('Publishing service %s of type %s' % (self.name, self.stype)) + self.entrygroup.AddService(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, dbus.UInt32(0), self.name, self.stype, '', '', self.port, avahi.dict_to_txt_array(self.txt), reply_handler=self.service_added_callback, error_handler=self.service_add_fail_callback) + self.entrygroup.Commit(reply_handler=self.service_committed_callback, + error_handler=self.entrygroup_commit_error_CB) + + return True + + except dbus.dbus_bindings.DBusException, e: + gajim.log.debug(str(e)) + return False + + def announce(self): + if not self.connected: + return False + + state = self.server.GetState() + if state == avahi.SERVER_RUNNING: + self.create_service() + self.announced = True + return True + + def remove_announce(self): + if self.announced == False: + return False + try: + if self.entrygroup.GetState() != avahi.ENTRY_GROUP_FAILURE: + self.entrygroup.Reset() + self.entrygroup.Free() + self.entrygroup = None + self.announced = False + return True + else: + return False + except dbus.dbus_bindings.DBusException, e: + gajim.log.debug("Can't remove service. That should not happen") + + def browse_domain(self, interface, protocol, domain): + self.new_service_type(interface, protocol, self.stype, domain, '') + + def avahi_dbus_connect_cb(self, a, connect, disconnect): + if connect != "": + gajim.log.debug('Lost connection to avahi-daemon') + try: + self.connected = False + self.disconnect() + self.disconnected_CB() + except Exception, e: + print e + else: + gajim.log.debug('We are connected to avahi-daemon') + + + + # connect to dbus + def connect_dbus(self): + if self.server: + return True + try: + self.bus = dbus.SystemBus() + self.bus.add_signal_receiver(self.avahi_dbus_connect_cb, + "NameOwnerChanged", "org.freedesktop.DBus", + arg0="org.freedesktop.Avahi") + self.server = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, \ + avahi.DBUS_PATH_SERVER), avahi.DBUS_INTERFACE_SERVER) + self.server.connect_to_signal('StateChanged', + self.server_state_changed_callback) + except Exception, e: + # Avahi service is not present + self.server = None + gajim.log.debug(str(e)) + return False + else: + return True + + def connect(self): + self.name = self.username + '@' + self.host # service name + if not self.connect_dbus(): + return False + + self.connected = True + # start browsing + if self.domain is None: + # Explicitly browse .local + self.browse_domain(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, "local") + + # Browse for other browsable domains + self.domain_browser = dbus.Interface(self.bus.get_object(avahi.DBUS_NAME, \ + self.server.DomainBrowserNew(avahi.IF_UNSPEC, \ + avahi.PROTO_UNSPEC, '', avahi.DOMAIN_BROWSER_BROWSE,\ + dbus.UInt32(0))), avahi.DBUS_INTERFACE_DOMAIN_BROWSER) + self.domain_browser.connect_to_signal('ItemNew', self.new_domain_callback) + self.domain_browser.connect_to_signal('Failure', self.error_callback) + else: + self.browse_domain(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, domain) + + return True + + def disconnect(self): + if self.connected: + self.connected = False + if self.service_browser: + self.service_browser.Free() + if self.domain_browser: + self.domain_browser.Free() + self.remove_announce() + self.service_browser = None + self.domain_browser = None + self.server = None + + # refresh txt data of all contacts manually (no callback available) + def resolve_all(self): + for val in self.contacts.values(): + self.server.ResolveService(int(val[C_INTERFACE]), int(val[C_PROTOCOL]), val[C_BARE_NAME], \ + self.stype, val[C_DOMAIN], avahi.PROTO_UNSPEC, dbus.UInt32(0),\ + reply_handler=self.service_resolved_all_callback, error_handler=self.error_callback) + + def get_contacts(self): + return self.contacts + + def get_contact(self, jid): + return self.contacts[jid] + + def update_txt(self, show = None): + if show: + self.txt['status'] = self.replace_show(show) + + txt = avahi.dict_to_txt_array(self.txt) + if self.connected and self.entrygroup: + self.entrygroup.UpdateServiceTxt(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, dbus.UInt32(0), self.name, self.stype,'', txt, reply_handler=self.service_updated_callback, error_handler=self.error_callback) + return True + else: + return False + + +# END Zeroconf + +''' +# how to use + + zeroconf = Zeroconf() + zeroconf.connect() + zeroconf.txt['1st'] = 'foo' + zeroconf.txt['last'] = 'bar' + zeroconf.txt['email'] = foo@bar.org + zeroconf.announce() + + # updating after announcing + txt = {} + txt['status'] = 'avail' + txt['msg'] = 'Here I am' + zeroconf.update_txt(txt) +''' diff --git a/src/config.py b/src/config.py index 1c8cd49fd..e3aa82d61 100644 --- a/src/config.py +++ b/src/config.py @@ -4,6 +4,7 @@ ## Copyright (C) 2005-2006 Nikos Kouremenos ## Copyright (C) 2005 Dimitur Kirov ## Copyright (C) 2003-2005 Vincent Hanquez +## Copyright (C) 2006 Stefan Bethge ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published @@ -38,6 +39,7 @@ from common import helpers from common import gajim from common import connection from common import passwords +from common import zeroconf from common import dbus_support from common.exceptions import GajimGeneralException @@ -1374,6 +1376,9 @@ class AccountModificationWindow: config['custom_host'] = self.xml.get_widget( 'custom_host_entry').get_text().decode('utf-8') + # update in case the name was changed to local account's name + config['is_zeroconf'] = False + config['keyname'] = self.xml.get_widget('gpg_name_label').get_text().decode('utf-8') if config['keyname'] == '': #no key selected config['keyid'] = '' @@ -1808,6 +1813,22 @@ class AccountsWindow: st = gajim.config.get('mergeaccounts') self.xml.get_widget('merge_checkbutton').set_active(st) + import os + + avahi_error = False + try: + import avahi + except ImportError: + avahi_error = True + + # enable zeroconf + st = gajim.config.get('enable_zeroconf') + w = self.xml.get_widget('enable_zeroconf_checkbutton') + w.set_active(st) + if os.name == 'nt' or (avahi_error and not w.get_active()): + w.set_sensitive(False) + self.zeroconf_toggled_id = w.connect('toggled', self.on_enable_zeroconf_checkbutton_toggled) + def on_accounts_window_key_press_event(self, widget, event): if event.keyval == gtk.keysyms.Escape: self.window.destroy() @@ -1847,6 +1868,17 @@ class AccountsWindow: dialogs.ErrorDialog(_('Unread events'), _('Read all pending events before removing this account.')) return + + if gajim.config.get_per('accounts', account, 'is_zeroconf'): + w = self.xml.get_widget('enable_zeroconf_checkbutton') + w.set_active(False) + else: + if gajim.interface.instances[account].has_key('remove_account'): + gajim.interface.instances[account]['remove_account'].window.present() + else: + gajim.interface.instances[account]['remove_account'] = \ + RemoveAccountWindow(account) + win_opened = False if gajim.interface.msg_win_mgr.get_controls(acct = account): win_opened = True @@ -1891,21 +1923,107 @@ class AccountsWindow: self.show_modification_window(account) def show_modification_window(self, account): - if gajim.interface.instances[account].has_key('account_modification'): - gajim.interface.instances[account]['account_modification'].window.present() + if gajim.config.get_per('accounts', account, 'is_zeroconf'): + if gajim.interface.instances.has_key('zeroconf_properties'): + gajim.interface.instances['zeroconf_properties'].window.present() + else: + gajim.interface.instances['zeroconf_properties'] = \ + ZeroconfPropertiesWindow() else: - gajim.interface.instances[account]['account_modification'] = \ - AccountModificationWindow(account) + if gajim.interface.instances[account].has_key('account_modification'): + gajim.interface.instances[account]['account_modification'].window.present() + else: + gajim.interface.instances[account]['account_modification'] = \ + AccountModificationWindow(account) + + def on_checkbutton_toggled(self, widget, config_name, + change_sensitivity_widgets = None): + gajim.config.set(config_name, widget.get_active()) + if change_sensitivity_widgets: + for w in change_sensitivity_widgets: + w.set_sensitive(widget.get_active()) + gajim.interface.save_config() def on_merge_checkbutton_toggled(self, widget): - gajim.config.set('mergeaccounts', widget.get_active()) - gajim.interface.save_config() + self.on_checkbutton_toggled(widget, 'mergeaccounts') if len(gajim.connections) >= 2: # Do not merge accounts if only one exists gajim.interface.roster.regroup = gajim.config.get('mergeaccounts') else: gajim.interface.roster.regroup = False gajim.interface.roster.draw_roster() + + def on_enable_zeroconf_checkbutton_toggled(self, widget): + # don't do anything if there is an account with the local name but is a normal account + if gajim.connections.has_key(gajim.ZEROCONF_ACC_NAME) and not gajim.connections[gajim.ZEROCONF_ACC_NAME].is_zeroconf: + gajim.connections[gajim.ZEROCONF_ACC_NAME].dispatch('ERROR', (_('Account Local already exists.'),_('Please rename or remove it before enabling link-local messaging.'))) + widget.disconnect(self.zeroconf_toggled_id) + widget.set_active(False) + self.zeroconf_toggled_id = widget.connect('toggled', self.on_enable_zeroconf_checkbutton_toggled) + return + + if gajim.config.get('enable_zeroconf'): + #disable + gajim.interface.roster.close_all(gajim.ZEROCONF_ACC_NAME) + gajim.connections[gajim.ZEROCONF_ACC_NAME].disable_account() + del gajim.connections[gajim.ZEROCONF_ACC_NAME] + gajim.interface.save_config() + del gajim.interface.instances[gajim.ZEROCONF_ACC_NAME] + del gajim.nicks[gajim.ZEROCONF_ACC_NAME] + del gajim.block_signed_in_notifications[gajim.ZEROCONF_ACC_NAME] + del gajim.groups[gajim.ZEROCONF_ACC_NAME] + gajim.contacts.remove_account(gajim.ZEROCONF_ACC_NAME) + del gajim.gc_connected[gajim.ZEROCONF_ACC_NAME] + del gajim.automatic_rooms[gajim.ZEROCONF_ACC_NAME] + del gajim.to_be_removed[gajim.ZEROCONF_ACC_NAME] + del gajim.newly_added[gajim.ZEROCONF_ACC_NAME] + del gajim.sleeper_state[gajim.ZEROCONF_ACC_NAME] + del gajim.encrypted_chats[gajim.ZEROCONF_ACC_NAME] + del gajim.last_message_time[gajim.ZEROCONF_ACC_NAME] + del gajim.status_before_autoaway[gajim.ZEROCONF_ACC_NAME] + if len(gajim.connections) >= 2: # Do not merge accounts if only one exists + gajim.interface.roster.regroup = gajim.config.get('mergeaccounts') + else: + gajim.interface.roster.regroup = False + gajim.interface.roster.draw_roster() + gajim.interface.roster.actions_menu_needs_rebuild = True + if gajim.interface.instances.has_key('accounts'): + gajim.interface.instances['accounts'].init_accounts() + + else: + # enable (will create new account if not present) + gajim.connections[gajim.ZEROCONF_ACC_NAME] = common.zeroconf.connection_zeroconf.ConnectionZeroconf(gajim.ZEROCONF_ACC_NAME) + # update variables + gajim.interface.instances[gajim.ZEROCONF_ACC_NAME] = {'infos': {}, 'disco': {}, + 'gc_config': {}} + gajim.connections[gajim.ZEROCONF_ACC_NAME].connected = 0 + gajim.groups[gajim.ZEROCONF_ACC_NAME] = {} + gajim.contacts.add_account(gajim.ZEROCONF_ACC_NAME) + gajim.gc_connected[gajim.ZEROCONF_ACC_NAME] = {} + gajim.automatic_rooms[gajim.ZEROCONF_ACC_NAME] = {} + gajim.newly_added[gajim.ZEROCONF_ACC_NAME] = [] + gajim.to_be_removed[gajim.ZEROCONF_ACC_NAME] = [] + gajim.nicks[gajim.ZEROCONF_ACC_NAME] = gajim.ZEROCONF_ACC_NAME + gajim.block_signed_in_notifications[gajim.ZEROCONF_ACC_NAME] = True + gajim.sleeper_state[gajim.ZEROCONF_ACC_NAME] = 'off' + gajim.encrypted_chats[gajim.ZEROCONF_ACC_NAME] = [] + gajim.last_message_time[gajim.ZEROCONF_ACC_NAME] = {} + gajim.status_before_autoaway[gajim.ZEROCONF_ACC_NAME] = '' + # refresh accounts window + if gajim.interface.instances.has_key('accounts'): + gajim.interface.instances['accounts'].init_accounts() + # refresh roster + if len(gajim.connections) >= 2: # Do not merge accounts if only one exists + gajim.interface.roster.regroup = gajim.config.get('mergeaccounts') + else: + gajim.interface.roster.regroup = False + gajim.interface.roster.draw_roster() + gajim.interface.roster.actions_menu_needs_rebuild = True + gajim.interface.save_config() + gajim.connections[gajim.ZEROCONF_ACC_NAME].change_status('online', '') + + self.on_checkbutton_toggled(widget, 'enable_zeroconf') + class DataFormWindow: def __init__(self, account, config): self.account = account @@ -3034,3 +3152,203 @@ _('You can set advanced account options by pressing Advanced button, or later by gajim.interface.roster.draw_roster() gajim.interface.roster.actions_menu_needs_rebuild = True gajim.interface.save_config() + +#---------- ZeroconfPropertiesWindow class -------------# +class ZeroconfPropertiesWindow: + def __init__(self): + self.xml = gtkgui_helpers.get_glade('zeroconf_properties_window.glade') + self.window = self.xml.get_widget('zeroconf_properties_window') + self.window.set_transient_for(gajim.interface.roster.window) + self.xml.signal_autoconnect(self) + + self.init_account() + self.init_account_gpg() + + self.xml.get_widget('save_button').grab_focus() + self.window.show_all() + + def init_account(self): + st = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'autoconnect') + if st: + self.xml.get_widget('autoconnect_checkbutton').set_active(st) + + list_no_log_for = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME,'no_log_for').split() + if gajim.ZEROCONF_ACC_NAME in list_no_log_for: + self.xml.get_widget('log_history_checkbutton').set_active(0) + else: + self.xml.get_widget('log_history_checkbutton').set_active(1) + + + st = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'sync_with_global_status') + if st: + self.xml.get_widget('sync_with_global_status_checkbutton').set_active(st) + + st = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_first_name') + if st: + self.xml.get_widget('first_name_entry').set_text(st) + + st = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_last_name') + if st: + self.xml.get_widget('last_name_entry').set_text(st) + + st = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_jabber_id') + if st: + self.xml.get_widget('jabber_id_entry').set_text(st) + + st = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'zeroconf_email') + if st: + self.xml.get_widget('email_entry').set_text(st) + + st = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'custom_port') + if st: + self.xml.get_widget('custom_port_entry').set_text(str(st)) + + st = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'use_custom_host') + if st: + self.xml.get_widget('custom_port_checkbutton').set_active(st) + + self.xml.get_widget('custom_port_entry').set_sensitive(bool(st)) + + if not st: + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, 'custom_port', '5298') + + def init_account_gpg(self): + keyid = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'keyid') + keyname = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'keyname') + savegpgpass = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME,'savegpgpass') + + if not keyid or not gajim.config.get('usegpg'): + return + + self.xml.get_widget('gpg_key_label').set_text(keyid) + self.xml.get_widget('gpg_name_label').set_text(keyname) + gpg_save_password_checkbutton = \ + self.xml.get_widget('gpg_save_password_checkbutton') + gpg_save_password_checkbutton.set_sensitive(True) + gpg_save_password_checkbutton.set_active(savegpgpass) + + if savegpgpass: + entry = self.xml.get_widget('gpg_password_entry') + entry.set_sensitive(True) + gpgpassword = gajim.config.get_per('accounts', + gajim.ZEROCONF_ACC_NAME, 'gpgpassword') + entry.set_text(gpgpassword) + + def on_zeroconf_properties_window_destroy(self, widget): + #close window + if gajim.interface.instances.has_key('zeroconf_properties'): + del gajim.interface.instances['zeroconf_properties'] + + def on_custom_port_checkbutton_toggled(self, widget): + st = self.xml.get_widget('custom_port_checkbutton').get_active() + self.xml.get_widget('custom_port_entry').set_sensitive(bool(st)) + + def on_cancel_button_clicked(self, widget): + self.window.destroy() + + def on_save_button_clicked(self, widget): + config = {} + + st = self.xml.get_widget('autoconnect_checkbutton').get_active() + config['autoconnect'] = st + list_no_log_for = gajim.config.get_per('accounts', + gajim.ZEROCONF_ACC_NAME, 'no_log_for').split() + if gajim.ZEROCONF_ACC_NAME in list_no_log_for: + list_no_log_for.remove(gajim.ZEROCONF_ACC_NAME) + if not self.xml.get_widget('log_history_checkbutton').get_active(): + list_no_log_for.append(gajim.ZEROCONF_ACC_NAME) + config['no_log_for'] = ' '.join(list_no_log_for) + + st = self.xml.get_widget('sync_with_global_status_checkbutton').get_active() + config['sync_with_global_status'] = st + + st = self.xml.get_widget('first_name_entry').get_text() + config['zeroconf_first_name'] = st.decode('utf-8') + + st = self.xml.get_widget('last_name_entry').get_text() + config['zeroconf_last_name'] = st.decode('utf-8') + + st = self.xml.get_widget('jabber_id_entry').get_text() + config['zeroconf_jabber_id'] = st.decode('utf-8') + + st = self.xml.get_widget('email_entry').get_text() + config['zeroconf_email'] = st.decode('utf-8') + + use_custom_port = self.xml.get_widget('custom_port_checkbutton').get_active() + config['use_custom_host'] = use_custom_port + + old_port = gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, 'custom_port') + if use_custom_port: + port = self.xml.get_widget('custom_port_entry').get_text() + else: + port = 5298 + + config['custom_port'] = port + + config['keyname'] = self.xml.get_widget('gpg_name_label').get_text().decode('utf-8') + if config['keyname'] == '': #no key selected + config['keyid'] = '' + config['savegpgpass'] = False + config['gpgpassword'] = '' + else: + config['keyid'] = self.xml.get_widget('gpg_key_label').get_text().decode('utf-8') + config['savegpgpass'] = self.xml.get_widget( + 'gpg_save_password_checkbutton').get_active() + config['gpgpassword'] = self.xml.get_widget('gpg_password_entry' + ).get_text().decode('utf-8') + + reconnect = False + for opt in ('zeroconf_first_name','zeroconf_last_name', 'zeroconf_jabber_id', 'zeroconf_email', 'custom_port'): + if gajim.config.get_per('accounts', gajim.ZEROCONF_ACC_NAME, opt) != config[opt]: + reconnect = True + + for opt in config: + gajim.config.set_per('accounts', gajim.ZEROCONF_ACC_NAME, opt, config[opt]) + + if gajim.connections.has_key(gajim.ZEROCONF_ACC_NAME): + if port != old_port or reconnect: + gajim.connections[gajim.ZEROCONF_ACC_NAME].update_details() + + self.window.destroy() + + def on_gpg_choose_button_clicked(self, widget, data = None): + if gajim.connections.has_key(gajim.ZEROCONF_ACC_NAME): + secret_keys = gajim.connections[gajim.ZEROCONF_ACC_NAME].ask_gpg_secrete_keys() + + # self.account is None and/or gajim.connections is {} + else: + from common import GnuPG + if GnuPG.USE_GPG: + secret_keys = GnuPG.GnuPG().get_secret_keys() + else: + secret_keys = [] + if not secret_keys: + dialogs.ErrorDialog(_('Failed to get secret keys'), + _('There was a problem retrieving your OpenPGP secret keys.')) + return + secret_keys[_('None')] = _('None') + instance = dialogs.ChooseGPGKeyDialog(_('OpenPGP Key Selection'), + _('Choose your OpenPGP key'), secret_keys) + keyID = instance.run() + if keyID is None: + return + checkbutton = self.xml.get_widget('gpg_save_password_checkbutton') + gpg_key_label = self.xml.get_widget('gpg_key_label') + gpg_name_label = self.xml.get_widget('gpg_name_label') + if keyID[0] == _('None'): + gpg_key_label.set_text(_('No key selected')) + gpg_name_label.set_text('') + checkbutton.set_sensitive(False) + self.xml.get_widget('gpg_password_entry').set_sensitive(False) + else: + gpg_key_label.set_text(keyID[0]) + gpg_name_label.set_text(keyID[1]) + checkbutton.set_sensitive(True) + checkbutton.set_active(False) + self.xml.get_widget('gpg_password_entry').set_text('') + + def on_gpg_save_password_checkbutton_toggled(self, widget): + st = widget.get_active() + w = self.xml.get_widget('gpg_password_entry') + w.set_sensitive(bool(st)) +# w.set_text = '' diff --git a/src/gajim.py b/src/gajim.py index 5584b89ca..413f5705c 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -31,6 +31,7 @@ import message_control from chat_control import ChatControlBase from common import exceptions +from common.zeroconf import connection_zeroconf if os.name == 'posix': # dl module is Unix Only try: # rename the process name to gajim @@ -618,7 +619,9 @@ class Interface: jids = full_jid_with_resource.split('/', 1) jid = jids[0] gc_control = self.msg_win_mgr.get_control(jid, account) - if gc_control and gc_control.type_id == message_control.TYPE_GC: + if gc_control and gc_control.type_id != message_control.TYPE_GC: + gc_control = None + if gc_control: if len(jids) > 1: # it's a pm nick = jids[1] if not self.msg_win_mgr.get_control(full_jid_with_resource, @@ -1406,6 +1409,20 @@ class Interface: if win.startswith('privacy_list_'): self.instances[account][win].check_active_default(data) + def handle_event_zc_name_conflict(self, account, data): + dlg = dialogs.InputDialog(_('Username Conflict'), + _('Please type a new username for your local account'), + is_modal = True) + dlg.input_entry.set_text(data) + response = dlg.get_response() + if response == gtk.RESPONSE_OK: + new_name = dlg.input_entry.get_text() + gajim.config.set_per('accounts', account, 'name', new_name) + status = gajim.connections[account].status + gajim.connections[account].username = new_name + gajim.connections[account].change_status(status, '') + + def read_sleepy(self): '''Check idle status and change that status if needed''' if not self.sleeper.poll(): @@ -1711,6 +1728,7 @@ class Interface: 'PRIVACY_LIST_RECEIVED': self.handle_event_privacy_list_received, 'PRIVACY_LISTS_ACTIVE_DEFAULT': \ self.handle_event_privacy_lists_active_default, + 'ZC_NAME_CONFLICT': self.handle_event_zc_name_conflict, } gajim.handlers = self.handlers @@ -1867,9 +1885,13 @@ class Interface: self.handle_event_file_progress) gajim.proxy65_manager = proxy65_manager.Proxy65Manager(gajim.idlequeue) self.register_handlers() + if gajim.config.get('enable_zeroconf'): + gajim.connections[gajim.ZEROCONF_ACC_NAME] = common.zeroconf.connection_zeroconf.ConnectionZeroconf(gajim.ZEROCONF_ACC_NAME) for account in gajim.config.get_per('accounts'): - gajim.connections[account] = common.connection.Connection(account) - + if not gajim.config.get_per('accounts', account, 'is_zeroconf'): + gajim.connections[account] = common.connection.Connection(account) + + # gtk hooks # gtk hooks gtk.about_dialog_set_email_hook(self.on_launch_browser_mailer, 'mail') gtk.about_dialog_set_url_hook(self.on_launch_browser_mailer, 'url') diff --git a/src/roster_window.py b/src/roster_window.py index 881ce56aa..1edabc878 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1119,6 +1119,14 @@ class RosterWindow: else: info[contact.jid] = vcard.VcardWindow(contact, account) + def on_info_zeroconf(self, widget, contact, account): + info = gajim.interface.instances[account]['infos'] + if info.has_key(contact.jid): + info[contact.jid].window.present() + else: + info[contact.jid] = vcard.ZeroconfVcardWindow(contact, account) + + def show_tooltip(self, contact): pointer = self.tree.get_pointer() props = self.tree.get_path_at_pos(pointer[0], pointer[1]) @@ -1363,6 +1371,118 @@ class RosterWindow: if not contact: return + if gajim.config.get_per('accounts', account, 'is_zeroconf'): + xml = gtkgui_helpers.get_glade('zeroconf_contact_context_menu.glade') + zeroconf_contact_context_menu = xml.get_widget('zeroconf_contact_context_menu') + + start_chat_menuitem = xml.get_widget('start_chat_menuitem') + rename_menuitem = xml.get_widget('rename_menuitem') + edit_groups_menuitem = xml.get_widget('edit_groups_menuitem') + # separator has with send file, assign_openpgp_key_menuitem, etc.. + above_send_file_separator = xml.get_widget('above_send_file_separator') + send_file_menuitem = xml.get_widget('send_file_menuitem') + assign_openpgp_key_menuitem = xml.get_widget( + 'assign_openpgp_key_menuitem') + add_special_notification_menuitem = xml.get_widget( + 'add_special_notification_menuitem') + + add_special_notification_menuitem.hide() + add_special_notification_menuitem.set_no_show_all(True) + + if not our_jid: + # add a special img for rename menuitem + path_to_kbd_input_img = os.path.join(gajim.DATA_DIR, 'pixmaps', + 'kbd_input.png') + img = gtk.Image() + img.set_from_file(path_to_kbd_input_img) + rename_menuitem.set_image(img) + + above_information_separator = xml.get_widget( + 'above_information_separator') + + # skip a separator + information_menuitem = xml.get_widget('information_menuitem') + history_menuitem = xml.get_widget('history_menuitem') + + contacts = gajim.contacts.get_contact(account, jid) + if len(contacts) > 1: # sevral resources + sub_menu = gtk.Menu() + start_chat_menuitem.set_submenu(sub_menu) + + iconset = gajim.config.get('iconset') + path = os.path.join(gajim.DATA_DIR, 'iconsets', iconset, '16x16') + for c in contacts: + # icon MUST be different instance for every item + state_images = self.load_iconset(path) + item = gtk.ImageMenuItem(c.resource + ' (' + str(c.priority) + ')') + icon_name = helpers.get_icon_name_to_show(c, account) + icon = state_images[icon_name] + item.set_image(icon) + sub_menu.append(item) + item.connect('activate', self.on_open_chat_window, c, account, + c.resource) + + else: # one resource + start_chat_menuitem.connect('activate', + self.on_roster_treeview_row_activated, tree_path) + + if contact.resource: + send_file_menuitem.connect('activate', + self.on_send_file_menuitem_activate, account, contact) + else: # if we do not have resource we cannot send file + send_file_menuitem.hide() + send_file_menuitem.set_no_show_all(True) + + rename_menuitem.connect('activate', self.on_rename, iter, tree_path) + information_menuitem.connect('activate', self.on_info_zeroconf, contact, + account) + history_menuitem.connect('activate', self.on_history, contact, + account) + + if _('Not in Roster') not in contact.groups: + #contact is in normal group + edit_groups_menuitem.set_no_show_all(False) + assign_openpgp_key_menuitem.set_no_show_all(False) + edit_groups_menuitem.connect('activate', self.on_edit_groups, [( + contact,account)]) + + if gajim.config.get('usegpg'): + assign_openpgp_key_menuitem.connect('activate', + self.on_assign_pgp_key, contact, account) + + else: # contact is in group 'Not in Roster' + edit_groups_menuitem.hide() + edit_groups_menuitem.set_no_show_all(True) + # hide first of the two consecutive separators + above_send_file_separator.hide() + above_send_file_separator.set_no_show_all(True) + assign_openpgp_key_menuitem.hide() + assign_openpgp_key_menuitem.set_no_show_all(True) + + # Remove many items when it's self contact row + if our_jid: + for menuitem in (rename_menuitem, edit_groups_menuitem, + above_information_separator): + menuitem.set_no_show_all(True) + menuitem.hide() + + # Unsensitive many items when account is offline + if gajim.connections[account].connected < 2: + for widget in [start_chat_menuitem, rename_menuitem, edit_groups_menuitem, send_file_menuitem]: + widget.set_sensitive(False) + + event_button = gtkgui_helpers.get_possible_button_event(event) + + zeroconf_contact_context_menu.attach_to_widget(self.tree, None) + zeroconf_contact_context_menu.connect('selection-done', + gtkgui_helpers.destroy_widget) + zeroconf_contact_context_menu.show_all() + zeroconf_contact_context_menu.popup(None, None, None, event_button, + event.time) + return + + + # normal account xml = gtkgui_helpers.get_glade('roster_contact_context_menu.glade') roster_contact_context_menu = xml.get_widget( 'roster_contact_context_menu') @@ -1773,6 +1893,14 @@ class RosterWindow: gajim.interface.instances[account]['account_modification'] = \ config.AccountModificationWindow(account) + def on_zeroconf_properties(self, widget, account): + if gajim.interface.instances.has_key('zeroconf_properties'): + gajim.interface.instances['zeroconf_properties'].\ + window.present() + else: + gajim.interface.instances['zeroconf_properties'] = \ + config.ZeroconfPropertiesWindow() + def on_open_gmail_inbox(self, widget, account): url = 'http://mail.google.com/mail?account_id=%s' % urllib.quote( gajim.config.get_per('accounts', account, 'name')) @@ -1792,71 +1920,123 @@ class RosterWindow: path = os.path.join(gajim.DATA_DIR, 'iconsets', iconset, '16x16') state_images = self.load_iconset(path) - xml = gtkgui_helpers.get_glade('account_context_menu.glade') - account_context_menu = xml.get_widget('account_context_menu') + if not gajim.config.get_per('accounts', account, 'is_zeroconf'): + xml = gtkgui_helpers.get_glade('account_context_menu.glade') + account_context_menu = xml.get_widget('account_context_menu') - status_menuitem = xml.get_widget('status_menuitem') - join_group_chat_menuitem =xml.get_widget('join_group_chat_menuitem') - open_gmail_inbox_menuitem = xml.get_widget('open_gmail_inbox_menuitem') - new_message_menuitem = xml.get_widget('new_message_menuitem') - add_contact_menuitem = xml.get_widget('add_contact_menuitem') - service_discovery_menuitem = xml.get_widget('service_discovery_menuitem') - edit_account_menuitem = xml.get_widget('edit_account_menuitem') - sub_menu = gtk.Menu() - status_menuitem.set_submenu(sub_menu) + status_menuitem = xml.get_widget('status_menuitem') + join_group_chat_menuitem =xml.get_widget('join_group_chat_menuitem') + open_gmail_inbox_menuitem = xml.get_widget('open_gmail_inbox_menuitem') + new_message_menuitem = xml.get_widget('new_message_menuitem') + add_contact_menuitem = xml.get_widget('add_contact_menuitem') + service_discovery_menuitem = xml.get_widget('service_discovery_menuitem') + edit_account_menuitem = xml.get_widget('edit_account_menuitem') + sub_menu = gtk.Menu() + status_menuitem.set_submenu(sub_menu) - for show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'): - uf_show = helpers.get_uf_show(show, use_mnemonic = True) + for show in ('online', 'chat', 'away', 'xa', 'dnd', 'invisible'): + uf_show = helpers.get_uf_show(show, use_mnemonic = True) + item = gtk.ImageMenuItem(uf_show) + icon = state_images[show] + item.set_image(icon) + sub_menu.append(item) + item.connect('activate', self.change_status, account, 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, account) + if gajim.connections[account].connected < 2: + item.set_sensitive(False) + + uf_show = helpers.get_uf_show('offline', use_mnemonic = True) item = gtk.ImageMenuItem(uf_show) - icon = state_images[show] + icon = state_images['offline'] item.set_image(icon) sub_menu.append(item) - item.connect('activate', self.change_status, account, show) + item.connect('activate', self.change_status, account, 'offline') - item = gtk.SeparatorMenuItem() - sub_menu.append(item) + if gajim.config.get_per('accounts', account, 'hostname') not in gajim.gmail_domains: + open_gmail_inbox_menuitem.set_no_show_all(True) + open_gmail_inbox_menuitem.hide() + else: + open_gmail_inbox_menuitem.connect('activate', self.on_open_gmail_inbox, + account) - 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, account) - if gajim.connections[account].connected < 2: - item.set_sensitive(False) + edit_account_menuitem.connect('activate', self.on_edit_account, account) + add_contact_menuitem.connect('activate', self.on_add_new_contact, account) + service_discovery_menuitem.connect('activate', + self.on_service_disco_menuitem_activate, account) + + gc_sub_menu = gtk.Menu() # gc is always a submenu + join_group_chat_menuitem.set_submenu(gc_sub_menu) + self.add_bookmarks_list(gc_sub_menu, account) + new_message_menuitem.connect('activate', + self.on_new_message_menuitem_activate, account) - uf_show = helpers.get_uf_show('offline', use_mnemonic = True) - item = gtk.ImageMenuItem(uf_show) - icon = state_images['offline'] - item.set_image(icon) - sub_menu.append(item) - item.connect('activate', self.change_status, account, 'offline') - - if gajim.config.get_per('accounts', account, 'hostname') not in gajim.gmail_domains: - open_gmail_inbox_menuitem.set_no_show_all(True) - open_gmail_inbox_menuitem.hide() + # make some items insensitive if account is offline + if gajim.connections[account].connected < 2: + for widget in [add_contact_menuitem, service_discovery_menuitem, + join_group_chat_menuitem, new_message_menuitem]: + widget.set_sensitive(False) else: - open_gmail_inbox_menuitem.connect('activate', self.on_open_gmail_inbox, - account) + xml = gtkgui_helpers.get_glade('zeroconf_context_menu.glade') + account_context_menu = xml.get_widget('zeroconf_context_menu') - edit_account_menuitem.connect('activate', self.on_edit_account, account) - add_contact_menuitem.connect('activate', self.on_add_new_contact, account) - service_discovery_menuitem.connect('activate', - self.on_service_disco_menuitem_activate, account) - - gc_sub_menu = gtk.Menu() # gc is always a submenu - join_group_chat_menuitem.set_submenu(gc_sub_menu) - self.add_bookmarks_list(gc_sub_menu, account) - new_message_menuitem.connect('activate', - self.on_new_message_menuitem_activate, account) + status_menuitem = xml.get_widget('status_menuitem') + #join_group_chat_menuitem =xml.get_widget('join_group_chat_menuitem') + new_message_menuitem = xml.get_widget('new_message_menuitem') + zeroconf_properties_menuitem = xml.get_widget('zeroconf_properties_menuitem') + sub_menu = gtk.Menu() + status_menuitem.set_submenu(sub_menu) - # make some items insensitive if account is offline - if gajim.connections[account].connected < 2: - for widget in [add_contact_menuitem, service_discovery_menuitem, - join_group_chat_menuitem, new_message_menuitem]: - widget.set_sensitive(False) - + for show in ('online', 'away', 'dnd', 'invisible'): + uf_show = helpers.get_uf_show(show, use_mnemonic = True) + item = gtk.ImageMenuItem(uf_show) + icon = state_images[show] + item.set_image(icon) + sub_menu.append(item) + item.connect('activate', self.change_status, account, 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, account) + if gajim.connections[account].connected < 2: + item.set_sensitive(False) + + uf_show = helpers.get_uf_show('offline', use_mnemonic = True) + item = gtk.ImageMenuItem(uf_show) + icon = state_images['offline'] + item.set_image(icon) + sub_menu.append(item) + item.connect('activate', self.change_status, account, 'offline') + + zeroconf_properties_menuitem.connect('activate', self.on_zeroconf_properties, account) + #gc_sub_menu = gtk.Menu() # gc is always a submenu + #join_group_chat_menuitem.set_submenu(gc_sub_menu) + #self.add_bookmarks_list(gc_sub_menu, account) + #new_message_menuitem.connect('activate', + # self.on_new_message_menuitem_activate, account) + + # make some items insensitive if account is offline + #if gajim.connections[account].connected < 2: + # for widget in [join_group_chat_menuitem, new_message_menuitem]: + # widget.set_sensitive(False) + # new_message_menuitem.set_sensitive(False) + return account_context_menu def make_account_menu(self, event, iter): @@ -1967,6 +2147,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) if not len(list_of_paths): return type = model[list_of_paths[0]][C_TYPE] + account = model[list_of_paths[0]][C_ACCOUNT] list_ = [] for path in list_of_paths: if model[path][C_TYPE] != type: @@ -1976,7 +2157,7 @@ _('If "%s" accepts this request you will know his or her status.') % jid) contact = gajim.contacts.get_contact_with_highest_priority(account, jid) list_.append((contact, account)) - if type in ('account', 'group', 'self_contact'): + if type in ('account', 'group', 'self_contact') or account == gajim.ZEROCONF_ACC_NAME: return if type == 'contact': self.on_req_usub(widget, list_) @@ -3572,6 +3753,10 @@ _('If "%s" accepts this request you will know his or her status.') % jid) account_dest, c_dest, path) return + if gajim.config.get_per('accounts', account_dest, 'is_zeroconf'): + # drop on zeroconf account, no contact adds possible + return + if position == gtk.TREE_VIEW_DROP_BEFORE and len(path_dest) == 2: # dropped before a group : we drop it in the previous group path_dest = (path_dest[0], path_dest[1]-1) @@ -3583,6 +3768,8 @@ _('If "%s" accepts this request you will know his or her status.') % jid) return if type_dest == 'account' and account_source == account_dest: return + if gajim.config.get_per('accounts', account_source, 'is_zeroconf'): + return it = iter_source while model[it][C_TYPE] == 'contact': it = model.iter_parent(it) diff --git a/src/tooltips.py b/src/tooltips.py index 7787c2673..be393c623 100644 --- a/src/tooltips.py +++ b/src/tooltips.py @@ -307,7 +307,14 @@ class GCTooltip(BaseTooltip): properties.append((show, None)) if contact.jid.strip() != '': - properties.append((_('Jabber ID: '), contact.jid)) + jid_markup = '' + contact.jid + '' + else: + jid_markup = '' + \ + gtkgui_helpers.escape_for_pango_markup(contact.get_shown_name()) \ + + '' + properties.append((jid_markup, None)) + properties.append((_('Role: '), helpers.get_uf_role(contact.role))) + properties.append((_('Affiliation: '), contact.affiliation.capitalize())) if hasattr(contact, 'resource') and contact.resource.strip() != '': properties.append((_('Resource: '), gtkgui_helpers.escape_for_pango_markup(contact.resource) )) @@ -408,10 +415,25 @@ class RosterTooltip(NotificationAreaTooltip): vcard_table.set_homogeneous(False) vcard_current_row = 1 properties = [] - name_markup = '%s' % gtkgui_helpers.escape_for_pango_markup( - prim_contact.get_shown_name()) - properties.append((name_markup, None)) + jid_markup = '' + prim_contact.jid + '' + properties.append((jid_markup, None)) + + properties.append((_('Name: '), gtkgui_helpers.escape_for_pango_markup( + prim_contact.get_shown_name()))) + if prim_contact.sub: + properties.append(( _('Subscription: '), + gtkgui_helpers.escape_for_pango_markup(helpers.get_uf_sub(prim_contact.sub)))) + if prim_contact.keyID: + keyID = None + if len(prim_contact.keyID) == 8: + keyID = prim_contact.keyID + elif len(prim_contact.keyID) == 16: + keyID = prim_contact.keyID[8:] + if keyID: + properties.append((_('OpenPGP: '), + gtkgui_helpers.escape_for_pango_markup(keyID))) + num_resources = 0 # put contacts in dict, where key is priority contacts_dict = {} @@ -422,6 +444,11 @@ class RosterTooltip(NotificationAreaTooltip): contacts_dict[contact.priority].append(contact) else: contacts_dict[contact.priority] = [contact] + + if num_resources == 1 and contact.resource: + properties.append((_('Resource: '), + gtkgui_helpers.escape_for_pango_markup(contact.resource) + ' (' + \ + unicode(contact.priority) + ')')) if num_resources > 1: properties.append((_('Status: '), ' ')) transport = gajim.get_transport_name_from_jid( diff --git a/src/vcard.py b/src/vcard.py index c1d86afb1..52b686493 100644 --- a/src/vcard.py +++ b/src/vcard.py @@ -2,6 +2,7 @@ ## ## Copyright (C) 2003-2006 Yann Le Boulanger ## Copyright (C) 2005-2006 Nikos Kouremenos +## Copyright (C) 2006 Stefan Bethge ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published @@ -336,3 +337,150 @@ class VcardWindow: def on_close_button_clicked(self, widget): self.window.destroy() + + +class ZeroconfVcardWindow: + def __init__(self, contact, account, is_fake = False): + # the contact variable is the jid if vcard is true + self.xml = gtkgui_helpers.get_glade('zeroconf_information_window.glade') + self.window = self.xml.get_widget('zeroconf_information_window') + + self.contact = contact + self.account = account + self.is_fake = is_fake + + # self.avatar_mime_type = None + # self.avatar_encoded = None + + self.fill_contact_page() + self.fill_personal_page() + + self.xml.signal_autoconnect(self) + self.window.show_all() + + def on_zeroconf_information_window_destroy(self, widget): + del gajim.interface.instances[self.account]['infos'][self.contact.jid] + + def on_zeroconf_information_window_key_press_event(self, widget, event): + if event.keyval == gtk.keysyms.Escape: + self.window.destroy() + + def on_log_history_checkbutton_toggled(self, widget): + #log conversation history? + oldlog = True + no_log_for = gajim.config.get_per('accounts', self.account, + 'no_log_for').split() + if self.contact.jid in no_log_for: + oldlog = False + log = widget.get_active() + if not log and not self.contact.jid in no_log_for: + no_log_for.append(self.contact.jid) + if log and self.contact.jid in no_log_for: + no_log_for.remove(self.contact.jid) + if oldlog != log: + gajim.config.set_per('accounts', self.account, 'no_log_for', + ' '.join(no_log_for)) + + def on_PHOTO_eventbox_button_press_event(self, widget, event): + '''If right-clicked, show popup''' + if event.button == 3: # right click + menu = gtk.Menu() + menuitem = gtk.ImageMenuItem(gtk.STOCK_SAVE_AS) + menuitem.connect('activate', + gtkgui_helpers.on_avatar_save_as_menuitem_activate, + self.contact.jid, self.account, self.contact.name + '.jpeg') + menu.append(menuitem) + menu.connect('selection-done', lambda w:w.destroy()) + # show the menu + menu.show_all() + menu.popup(None, None, None, event.button, event.time) + + def set_value(self, entry_name, value): + try: + if value and entry_name == 'URL_label': + if gtk.pygtk_version >= (2, 10, 0) and gtk.gtk_version >= (2, 10, 0): + widget = gtk.LinkButton(value, value) + else: + widget = gtk.Label(value) + table = self.xml.get_widget('personal_info_table') + table.attach(widget, 1, 4, 3, 4, yoptions = 0) + else: + self.xml.get_widget(entry_name).set_text(value) + except AttributeError: + pass + + def fill_status_label(self): + if self.xml.get_widget('information_notebook').get_n_pages() < 2: + return + contact_list = gajim.contacts.get_contact(self.account, self.contact.jid) + # stats holds show and status message + stats = '' + one = True # Are we adding the first line ? + if contact_list: + for c in contact_list: + if not one: + stats += '\n' + stats += helpers.get_uf_show(c.show) + if c.status: + stats += ': ' + c.status + if c.last_status_time: + stats += '\n' + _('since %s') % time.strftime('%c', + c.last_status_time).decode(locale.getpreferredencoding()) + one = False + else: # Maybe gc_vcard ? + stats = helpers.get_uf_show(self.contact.show) + if self.contact.status: + stats += ': ' + self.contact.status + status_label = self.xml.get_widget('status_label') + status_label.set_max_width_chars(15) + status_label.set_text(stats) + + tip = gtk.Tooltips() + status_label_eventbox = self.xml.get_widget('status_label_eventbox') + tip.set_tip(status_label_eventbox, stats) + + def fill_contact_page(self): + tooltips = gtk.Tooltips() + self.xml.get_widget('nickname_label').set_markup( + '' + + self.contact.get_shown_name() + + '') + self.xml.get_widget('local_jid_label').set_text(self.contact.jid) + + log = True + if self.contact.jid in gajim.config.get_per('accounts', self.account, + 'no_log_for').split(' '): + log = False + checkbutton = self.xml.get_widget('log_history_checkbutton') + checkbutton.set_active(log) + checkbutton.connect('toggled', self.on_log_history_checkbutton_toggled) + + resources = '%s (%s)' % (self.contact.resource, unicode( + self.contact.priority)) + uf_resources = self.contact.resource + _(' resource with priority ')\ + + unicode(self.contact.priority) + if not self.contact.status: + self.contact.status = '' + + # Request list time status + # gajim.connections[self.account].request_last_status_time(self.contact.jid, + # self.contact.resource) + + self.xml.get_widget('resource_prio_label').set_text(resources) + resource_prio_label_eventbox = self.xml.get_widget( + 'resource_prio_label_eventbox') + tooltips.set_tip(resource_prio_label_eventbox, uf_resources) + + self.fill_status_label() + + # gajim.connections[self.account].request_vcard(self.contact.jid, self.is_fake) + + def fill_personal_page(self): + contact = gajim.connections[gajim.ZEROCONF_ACC_NAME].roster.getItem(self.contact.jid) + self.xml.get_widget('first_name_label').set_text(contact['txt_dict']['1st']) + self.xml.get_widget('last_name_label').set_text(contact['txt_dict']['last']) + self.xml.get_widget('jabber_id_label').set_text(contact['txt_dict']['jid']) + self.xml.get_widget('email_label').set_text(contact['txt_dict']['email']) + + def on_close_button_clicked(self, widget): + self.window.destroy()