From bad42979524c67da0f1054c643785c8ac3d7caf7 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 9 Sep 2009 10:13:03 +0200 Subject: [PATCH 01/39] when unregistering an account, wait server answer before closing connection. --- src/common/connection.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/common/connection.py b/src/common/connection.py index d58c6aae5..4f8fdf977 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -1953,8 +1953,15 @@ class Connection(ConnectionHandlers): hostname = gajim.config.get_per('accounts', self.name, 'hostname') iq = common.xmpp.Iq(typ = 'set', to = hostname) iq.setTag(common.xmpp.NS_REGISTER + ' query').setTag('remove') - con.send(iq) - on_remove_success(True) + def _on_answer(result): + if result.getType() == 'result': + on_remove_success(True) + return + self.dispatch('ERROR', (_('Unregister failed'), + _('Unregistration with server %(server)s failed: %(error)s') \ + % {'server': hostname, 'error': result.getErrorMsg()})) + on_remove_success(False) + con.SendAndCallForResponse(iq, _on_answer) return on_remove_success(False) if self.connected == 0: From 377a77782384cc078a5e59d4e31d24bdb9be9311 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 9 Sep 2009 10:38:26 +0200 Subject: [PATCH 02/39] always reply to incoming resource. see #5227 --- src/common/connection.py | 2 +- src/common/stanza_session.py | 9 ++++++++- src/session.py | 4 ++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/common/connection.py b/src/common/connection.py index 4f8fdf977..b49149a08 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -1199,7 +1199,7 @@ class Connection(ConnectionHandlers): msgenc = '' if session: - fjid = str(session.jid) + fjid = session.get_to() if keyID and self.USE_GPG: xhtml = None diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index d5a5ec9e7..51a12b488 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -54,6 +54,7 @@ class StanzaSession(object): self.conn = conn self.jid = jid self.type = type_ + self.resource = None if thread_id: self.received_thread_id = True @@ -75,6 +76,12 @@ class StanzaSession(object): def is_loggable(self): return self.loggable and gajim.config.should_log(self.conn.name, self.jid) + def get_to(self): + to = str(self.jid) + if self.resource: + to += '/' + self.resource + return to + def remove_events(self, types): ''' Remove events associated with this session from the queue. @@ -107,7 +114,7 @@ class StanzaSession(object): if self.thread_id: msg.NT.thread = self.thread_id - msg.setAttr('to', self.jid) + msg.setAttr('to', self.get_to()) self.conn.send_stanza(msg) if isinstance(msg, xmpp.Message): diff --git a/src/session.py b/src/session.py index 6f5e0627c..f68d7c2dc 100644 --- a/src/session.py +++ b/src/session.py @@ -86,6 +86,10 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession): '''dispatch a received stanza''' msg_type = msg.getType() subject = msg.getSubject() + if self.jid != full_jid_with_resource: + self.resource = gajim.get_nick_from_fjid(full_jid_with_resource) + if self.control: + self.control.resource = self.resource if not msg_type or msg_type not in ('chat', 'groupchat', 'error'): msg_type = 'normal' From 44e98585edfca4f7785dabf2db0b8384d533e210 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 9 Sep 2009 13:26:53 +0200 Subject: [PATCH 03/39] move music track listener things from roster_window.py to gajim.py. see #3643 --- src/common/connection.py | 2 +- src/common/connection_handlers.py | 4 +-- src/gajim.py | 50 ++++++++++++++++++++++++++++ src/roster_window.py | 55 ++----------------------------- 4 files changed, 55 insertions(+), 56 deletions(-) diff --git a/src/common/connection.py b/src/common/connection.py index b49149a08..27b840f7b 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -219,7 +219,7 @@ class Connection(ConnectionHandlers): # We are doing disconnect at so many places, better use one function in all def disconnect(self, on_purpose=False): - gajim.interface.roster.music_track_changed(None, None, self.name) + gajim.interface.music_track_changed(None, None, self.name) self.on_purpose = on_purpose self.connected = 0 self.time_to_reconnect = None diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index f4304d976..0cfab7a2b 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -899,8 +899,8 @@ class ConnectionDisco: track = listener.get_playing_track() if gajim.config.get_per('accounts', self.name, 'publish_tune'): - gajim.interface.roster.music_track_changed(listener, - track, self.name) + gajim.interface.music_track_changed(listener, track, + self.name) break if features.__contains__(common.xmpp.NS_VCARD): self.vcard_supported = True diff --git a/src/gajim.py b/src/gajim.py index 3645c1c1f..3e65fac01 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -156,6 +156,7 @@ except exceptions.DatabaseMalformed: else: from common import dbus_support if dbus_support.supported: + from music_track_listener import MusicTrackListener import dbus if os.name == 'posix': # dl module is Unix Only @@ -243,6 +244,7 @@ from common import helpers from common import optparser from common import dataforms from common import passwords +from common import pep gajimpaths = common.configpaths.gajimpaths @@ -2961,6 +2963,48 @@ class Interface: ### Other Methods ################################################################################ + def enable_music_listener(self): + if not self.music_track_changed_signal: + listener = MusicTrackListener.get() + self.music_track_changed_signal = listener.connect( + 'music-track-changed', self.music_track_changed) + track = listener.get_playing_track() + self.music_track_changed(listener, track) + + def disable_music_listener(self): + listener = MusicTrackListener.get() + listener.disconnect(self.music_track_changed_signal) + self.music_track_changed_signal = None + + def music_track_changed(self, unused_listener, music_track_info, account=''): + if account == '': + accounts = gajim.connections.keys() + else: + accounts = [account] + if music_track_info is None: + artist = '' + title = '' + source = '' + elif hasattr(music_track_info, 'paused') and music_track_info.paused == 0: + artist = '' + title = '' + source = '' + else: + artist = music_track_info.artist + title = music_track_info.title + source = music_track_info.album + for acct in accounts: + if acct not in gajim.connections: + continue + if not gajim.account_is_connected(acct): + continue + if not gajim.connections[acct].pep_supported: + continue + if gajim.connections[acct].music_track_info == music_track_info: + continue + pep.user_send_tune(acct, artist, title, source) + gajim.connections[acct].music_track_info = music_track_info + def read_sleepy(self): '''Check idle status and change that status if needed''' if not self.sleeper.poll(): @@ -3516,6 +3560,12 @@ class Interface: except Exception: pass gobject.timeout_add_seconds(5, remote_init) + self.music_track_changed_signal = None + for account in gajim.connections: + if gajim.config.get_per('accounts', account, 'publish_tune') and \ + dbus_support.supported: + self.enable_music_listener() + break if __name__ == '__main__': def sigint_cb(num, stack): diff --git a/src/roster_window.py b/src/roster_window.py index b9358f07b..37269b414 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -62,7 +62,6 @@ from message_window import MessageWindowMgr from common import dbus_support if dbus_support.supported: - from music_track_listener import MusicTrackListener import dbus from common.xmpp.protocol import NS_COMMANDS, NS_FILE, NS_MUC @@ -1762,38 +1761,6 @@ class RosterWindow: except Exception: pass - def music_track_changed(self, unused_listener, music_track_info, - account=''): - if account == '': - accounts = gajim.connections.keys() - if music_track_info is None: - artist = '' - title = '' - source = '' - elif hasattr(music_track_info, 'paused') and music_track_info.paused == 0: - artist = '' - title = '' - source = '' - else: - artist = music_track_info.artist - title = music_track_info.title - source = music_track_info.album - if account == '': - for account in accounts: - if not gajim.account_is_connected(account): - continue - if not gajim.connections[account].pep_supported: - continue - if gajim.connections[account].music_track_info == music_track_info: - continue - pep.user_send_tune(account, artist, title, source) - gajim.connections[account].music_track_info = music_track_info - elif account in gajim.connections and \ - gajim.connections[account].pep_supported: - if gajim.connections[account].music_track_info != music_track_info: - pep.user_send_tune(account, artist, title, source) - gajim.connections[account].music_track_info = music_track_info - def connected_rooms(self, account): if account in gajim.gc_connected[account].values(): return True @@ -3354,21 +3321,14 @@ class RosterWindow: act = widget.get_active() gajim.config.set_per('accounts', account, 'publish_tune', act) if act: - listener = MusicTrackListener.get() - if not self.music_track_changed_signal: - self.music_track_changed_signal = listener.connect( - 'music-track-changed', self.music_track_changed) - track = listener.get_playing_track() - self.music_track_changed(listener, track) + gajim.interface.enable_music_listener() else: # disable it only if no other account use it for acct in gajim.connections: if gajim.config.get_per('accounts', acct, 'publish_tune'): break else: - listener = MusicTrackListener.get() - listener.disconnect(self.music_track_changed_signal) - self.music_track_changed_signal = None + gajim.interface.disable_music_listener() if gajim.connections[account].pep_supported: # As many implementations don't support retracting items, we send a @@ -6190,7 +6150,6 @@ class RosterWindow: self.xml = gtkgui_helpers.get_glade('roster_window.glade') self.window = self.xml.get_widget('roster_window') self.hpaned = self.xml.get_widget('roster_hpaned') - self.music_track_changed_signal = None gajim.interface.msg_win_mgr = MessageWindowMgr(self.window, self.hpaned) gajim.interface.msg_win_mgr.connect('window-delete', self.on_message_window_delete) @@ -6411,16 +6370,6 @@ class RosterWindow: self._toggeling_row = False self.setup_and_draw_roster() - for account in gajim.connections: - if gajim.config.get_per('accounts', account, 'publish_tune') and \ - dbus_support.supported: - listener = MusicTrackListener.get() - self.music_track_changed_signal = listener.connect( - 'music-track-changed', self.music_track_changed) - track = listener.get_playing_track() - self.music_track_changed(listener, track) - break - if gajim.config.get('show_roster_on_startup'): self.window.show_all() else: From 8b528e512f5182e55ff88892340632628620b67a Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 9 Sep 2009 13:51:33 +0200 Subject: [PATCH 04/39] move change_awn_icon_status from roster_window.py to gajim.py. see #3643 --- src/gajim.py | 26 ++++++++++++++++++++++++++ src/roster_window.py | 28 +--------------------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/gajim.py b/src/gajim.py index 3e65fac01..377888e96 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -2963,6 +2963,32 @@ class Interface: ### Other Methods ################################################################################ + def _change_awn_icon_status(self, status): + if not dbus_support.supported: + # do nothing if user doesn't have D-Bus bindings + return + try: + bus = dbus.SessionBus() + if not 'com.google.code.Awn' in bus.list_names(): + # Awn is not installed + return + except Exception: + return + iconset = gajim.config.get('iconset') + prefix = os.path.join(helpers.get_iconset_path(iconset), '32x32') + if status in ('chat', 'away', 'xa', 'dnd', 'invisible', 'offline'): + status = status + '.png' + elif status == 'online': + prefix = os.path.join(gajim.DATA_DIR, 'pixmaps') + status = 'gajim.png' + path = os.path.join(prefix, status) + try: + obj = bus.get_object('com.google.code.Awn', '/com/google/code/Awn') + awn = dbus.Interface(obj, 'com.google.code.Awn') + awn.SetTaskIconByName('Gajim', os.path.abspath(path)) + except Exception: + pass + def enable_music_listener(self): if not self.music_track_changed_signal: listener = MusicTrackListener.get() diff --git a/src/roster_window.py b/src/roster_window.py index 37269b414..54cf257c8 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -1735,32 +1735,6 @@ class RosterWindow: if chat_control: chat_control.contact = contact1 - def _change_awn_icon_status(self, status): - if not dbus_support.supported: - # do nothing if user doesn't have D-Bus bindings - return - try: - bus = dbus.SessionBus() - if not 'com.google.code.Awn' in bus.list_names(): - # Awn is not installed - return - except Exception: - return - iconset = gajim.config.get('iconset') - prefix = os.path.join(helpers.get_iconset_path(iconset), '32x32') - if status in ('chat', 'away', 'xa', 'dnd', 'invisible', 'offline'): - status = status + '.png' - elif status == 'online': - prefix = os.path.join(gajim.DATA_DIR, 'pixmaps') - status = 'gajim.png' - path = os.path.join(prefix, status) - try: - obj = bus.get_object('com.google.code.Awn', '/com/google/code/Awn') - awn = dbus.Interface(obj, 'com.google.code.Awn') - awn.SetTaskIconByName('Gajim', os.path.abspath(path)) - except Exception: - pass - def connected_rooms(self, account): if account in gajim.gc_connected[account].values(): return True @@ -2156,7 +2130,7 @@ class RosterWindow: liststore.prepend([status_combobox_text, gajim.interface.jabber_state_images['16'][show], show, False]) self.status_combobox.set_active(0) - self._change_awn_icon_status(show) + gajim.interface._change_awn_icon_status(show) self.combobox_callback_active = True if gajim.interface.systray_enabled: gajim.interface.systray.change_status(show) From 7d7ca4d4ae118e17b0c87e898ce42b304d06112e Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Thu, 10 Sep 2009 13:54:44 +0200 Subject: [PATCH 05/39] always expand lines in disco window, increase default disco window width. Fixes #3877 --- data/glade/service_discovery_window.glade | 45 ++++++++++++----------- src/disco.py | 6 +-- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/data/glade/service_discovery_window.glade b/data/glade/service_discovery_window.glade index 5c0ff24d5..176a94a27 100644 --- a/data/glade/service_discovery_window.glade +++ b/data/glade/service_discovery_window.glade @@ -1,11 +1,11 @@ - - - + + + 6 Service Discovery - 450 + 550 420 @@ -28,6 +28,9 @@ Agent JID - node True + + 0 + @@ -55,22 +59,12 @@ Agent JID - node 3 3 6 - - - - - - True - - - - 1 @@ -86,7 +80,7 @@ Agent JID - node True True True - 0 + False @@ -105,6 +99,7 @@ Agent JID - node False False + 0 @@ -145,6 +140,12 @@ Agent JID - node + + + + + + False @@ -155,9 +156,9 @@ Agent JID - node True True - GTK_POLICY_AUTOMATIC - GTK_POLICY_AUTOMATIC - GTK_SHADOW_ETCHED_IN + automatic + automatic + etched-in True @@ -184,6 +185,7 @@ Agent JID - node False False + 0 @@ -200,19 +202,20 @@ Agent JID - node 6 + gtk-close True True True True - gtk-close + False True - 0 False False - GTK_PACK_END + end + 0 diff --git a/src/disco.py b/src/disco.py index 87df8ec1d..0d3621f13 100644 --- a/src/disco.py +++ b/src/disco.py @@ -1137,11 +1137,11 @@ class ToplevelAgentBrowser(AgentBrowser): # Icon Renderer renderer = gtk.CellRendererPixbuf() renderer.set_property('xpad', 6) - col.pack_start(renderer, expand = False) + col.pack_start(renderer, expand=False) col.set_cell_data_func(renderer, self._pixbuf_renderer_data_func) # Text Renderer renderer = gtk.CellRendererText() - col.pack_start(renderer, expand = True) + col.pack_start(renderer, expand=True) col.set_cell_data_func(renderer, self._text_renderer_data_func) renderer.set_property('foreground', 'dark gray') # Save this so we can go along with theme changes @@ -1487,7 +1487,7 @@ class ToplevelAgentBrowser(AgentBrowser): if not cat: cat = self._create_category(*cat_args) self.model.append(cat, (jid, node, pix, descr, 1)) - self._expand_all() + gobject.idle_add(self._expand_all) # Grab info on the service self.cache.get_info(jid, node, self._agent_info, force=force) self._update_progressbar() From 0ea6544c2626162f8913ff4a58cd3771e00441ba Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Thu, 10 Sep 2009 18:30:26 +0200 Subject: [PATCH 06/39] show notification for bad GPG pass instead of an error dialog --- src/common/connection_handlers.py | 4 +--- src/gajim.py | 9 +++++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 0cfab7a2b..b4576a455 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -2615,9 +2615,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if sign_msg and not signed: signed = self.get_signed_presence(msg) if signed is None: - self.dispatch('ERROR', (_('OpenPGP passphrase was not given'), - #%s is the account name here - _('You will be connected to %s without OpenPGP.') % self.name)) + self.dispatch('BAD_PASSPHRASE', ()) self.USE_GPG = False signed = '' self.connected = gajim.SHOW_LIST.index(show) diff --git a/src/gajim.py b/src/gajim.py index 377888e96..b8512ac43 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -1509,10 +1509,15 @@ class Interface: if use_gpg_agent: sectext = _('You configured Gajim to use GPG agent, but there is no ' 'GPG agent running or it returned a wrong passphrase.\n') - sectext += _('You are currently connected without your OpenPGP key.') + sectext += _('You are currently connected without your OpenPGP key.') + dialogs.WarningDialog(_('Your passphrase is incorrect'), sectext) + else: + path = os.path.join(gajim.DATA_DIR, 'pixmaps', 'warning.png') + notify.popup('warning', account, account, 'warning', path, + _('OpenGPG Passphrase Incorrect'), + _('You are currently connected without your OpenPGP key.')) keyID = gajim.config.get_per('accounts', account, 'keyid') self.forget_gpg_passphrase(keyID) - dialogs.WarningDialog(_('Your passphrase is incorrect'), sectext) def handle_event_gpg_password_required(self, account, array): #('GPG_PASSWORD_REQUIRED', account, (callback,)) From 17e03edfd7bba0e32921b48fc834eabc7d737a0f Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Thu, 10 Sep 2009 22:12:52 +0200 Subject: [PATCH 07/39] show confirmation dialog first time we send custom status. see #3646 --- src/roster_window.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/roster_window.py b/src/roster_window.py index 54cf257c8..f99006f4e 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -3184,8 +3184,26 @@ class RosterWindow: jid += '/' + contact.resource self.send_status(account, show, message, to=jid) - self.get_status_message(show, on_response, show_pep=False, - always_ask=True) + def send_it(is_checked=None): + if is_checked is not None: # dialog has been shown + if is_checked: # user does not want to be asked again + gajim.config.set('confirm_custom_status', 'no') + else: + gajim.config.set('confirm_custom_status', 'yes') + self.get_status_message(show, on_response, show_pep=False, + always_ask=True) + + confirm_custom_status = gajim.config.get('confirm_custom_status') + if confirm_custom_status == 'no': + send_it() + return + pritext = _('You are about to send a custom status. Are you sure you want' + ' to continue?') + sectext = _('This contact will temporarily see you as %(status)s, ' + 'but only until you change your status. Then he will see your global ' + 'status.') % {'status': show} + dlg = dialogs.ConfirmationDialogCheck(pritext, sectext, + _('Do _not ask me again'), on_response_ok=send_it) def on_status_combobox_changed(self, widget): '''When we change our status via the combobox''' From a7c3451e1def17e7c01a87e1cffa5735f7dc6eca Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Fri, 11 Sep 2009 00:01:56 +0200 Subject: [PATCH 08/39] Show a confirmation dialog first time we block a contact. Fixes #3646 --- src/common/config.py | 2 ++ src/roster_window.py | 23 ++++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/common/config.py b/src/common/config.py index 6482d7516..1f99ef702 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -249,6 +249,8 @@ class Config: 'gc_nicknames_colors': [ opt_str, '#a34526:#c000ff:#0012ff:#388a99:#045723:#7c7c7c:#ff8a00:#94452d:#244b5a:#32645a', _('List of colors, separated by ":", that will be used to color nicknames in group chats.'), True ], 'ctrl_tab_go_to_next_composing': [opt_bool, True, _('Ctrl-Tab go to next composing tab when none is unread.')], 'confirm_metacontacts': [ opt_str, '', _('Should we show the confirm metacontacts creation dialog or not? Empty string means we never show the dialog.')], + 'confirm_block': [ opt_str, '', _('Should we show the confirm block contact dialog or not? Empty string means we never show the dialog.')], + 'confirm_custom_status': [ opt_str, '', _('Should we show the confirm custom status dialog or not? Empty string means we never show the dialog.')], 'enable_negative_priority': [ opt_bool, False, _('If True, you will be able to set a negative priority to your account in account modification window. BE CAREFUL, when you are logged in with a negative priority, you will NOT receive any message from your server.')], 'use_gnomekeyring': [opt_bool, True, _('If True, Gajim will use Gnome Keyring (if available) to store account passwords.')], 'use_kwalletcli': [opt_bool, True, _('If True, Gajim will use KDE Wallet (if kwalletcli is available) to store account passwords.')], diff --git a/src/roster_window.py b/src/roster_window.py index f99006f4e..f7a0044bf 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -2575,7 +2575,24 @@ class RosterWindow: connection.set_default_list('block') connection.get_privacy_list('block') - self.get_status_message('offline', on_continue, show_pep=False) + def _block_it(is_checked=None): + if is_checked is not None: # dialog has been shown + if is_checked: # user does not want to be asked again + gajim.config.set('confirm_block', 'no') + else: + gajim.config.set('confirm_block', 'yes') + self.get_status_message('offline', on_continue, show_pep=False) + + confirm_block = gajim.config.get('confirm_block') + if confirm_block == 'no': + _block_it() + return + pritext = _('You are about to block a contact. Are you sure you want' + ' to continue?') + sectext = _('This contact will see you offline and you will not receive ' + 'messages he will send you.') + dlg = dialogs.ConfirmationDialogCheck(pritext, sectext, + _('Do _not ask me again'), on_response_ok=_block_it) def on_unblock(self, widget, list_, group=None): ''' When clicked on the 'unblock' button in context menu. ''' @@ -2828,7 +2845,7 @@ class RosterWindow: self.remove_groupchat(jid, account) def on_send_single_message_menuitem_activate(self, widget, account, - contact = None): + contact=None): if contact is None: dialogs.SingleMessageWindow(account, action='send') elif isinstance(contact, list): @@ -2878,7 +2895,7 @@ class RosterWindow: break def on_invite_to_room(self, widget, list_, room_jid, room_account, - resource = None): + resource=None): ''' resource parameter MUST NOT be used if more than one contact in list ''' for e in list_: From faf3a1fe6faee2871c7149f8e535425deb1c78bc Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Fri, 11 Sep 2009 00:12:36 +0200 Subject: [PATCH 09/39] add a reconnect menuitem in groupchat context menu when it's doconnected. Fixes #4004 --- src/roster_window.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/roster_window.py b/src/roster_window.py index f7a0044bf..5e1f33221 100644 --- a/src/roster_window.py +++ b/src/roster_window.py @@ -2844,6 +2844,13 @@ class RosterWindow: ctrl.got_disconnected() self.remove_groupchat(jid, account) + def on_reconnect(self, widget, jid, account): + '''When disconnect menuitem is activated: disconect from room''' + if jid in gajim.interface.minimized_controls[account]: + ctrl = gajim.interface.minimized_controls[account][jid] + gajim.interface.join_gc_room(account, jid, ctrl.nick, + gajim.gc_passwords.get(jid, '')) + def on_send_single_message_menuitem_activate(self, widget, account, contact=None): if contact is None: @@ -5888,6 +5895,13 @@ class RosterWindow: jid, account) menu.append(maximize_menuitem) + if not gajim.gc_connected[account].get(jid, False): + connect_menuitem = gtk.ImageMenuItem(_('_Reconnect')) + connect_icon = gtk.image_new_from_stock(gtk.STOCK_CONNECT, \ + gtk.ICON_SIZE_MENU) + connect_menuitem.set_image(connect_icon) + connect_menuitem.connect('activate', self.on_reconnect, jid, account) + menu.append(connect_menuitem) disconnect_menuitem = gtk.ImageMenuItem(_('_Disconnect')) disconnect_icon = gtk.image_new_from_stock(gtk.STOCK_DISCONNECT, \ gtk.ICON_SIZE_MENU) From c38e7050f5ea4ced05d70babb018723db0ef2276 Mon Sep 17 00:00:00 2001 From: red-agent Date: Fri, 11 Sep 2009 04:54:26 +0300 Subject: [PATCH 10/39] Swept everything related to commands --- src/chat_control.py | 116 ++---------- src/groupchat_control.py | 375 +-------------------------------------- 2 files changed, 17 insertions(+), 474 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 4d2a8806f..6bf451ff9 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -603,45 +603,24 @@ class ChatControlBase(MessageControl): self.drag_entered_conv = True self.conv_textview.tv.set_editable(True) - def _process_command(self, message): - if not message or message[0] != '/': - return False - - message = message[1:] - message_array = message.split(' ', 1) - command = message_array.pop(0).lower() - if message_array == ['']: - message_array = [] - - if command == 'clear' and not len(message_array): - self.conv_textview.clear() # clear conversation - self.clear(self.msg_textview) # clear message textview too - return True - elif message == 'compact' and not len(message_array): - self.chat_buttons_set_visible(not self.hide_chat_buttons) - self.clear(self.msg_textview) - return True - return False - def send_message(self, message, keyID='', type_='chat', chatstate=None, - msg_id=None, composing_xep=None, resource=None, process_command=True, + msg_id=None, composing_xep=None, resource=None, xhtml=None, callback=None, callback_args=[]): '''Send the given message to the active tab. Doesn't return None if error ''' if not message or message == '\n': return None - if not process_command or not self._process_command(message): - MessageControl.send_message(self, message, keyID, type_=type_, - chatstate=chatstate, msg_id=msg_id, composing_xep=composing_xep, - resource=resource, user_nick=self.user_nick, xhtml=xhtml, - callback=callback, callback_args=callback_args) + MessageControl.send_message(self, message, keyID, type_=type_, + chatstate=chatstate, msg_id=msg_id, composing_xep=composing_xep, + resource=resource, user_nick=self.user_nick, xhtml=xhtml, + callback=callback, callback_args=callback_args) - # Record message history - self.save_sent_message(message) + # Record message history + self.save_sent_message(message) - # Be sure to send user nickname only once according to JEP-0172 - self.user_nick = None + # Be sure to send user nickname only once according to JEP-0172 + self.user_nick = None # Clear msg input message_buffer = self.msg_textview.get_buffer() @@ -1129,7 +1108,6 @@ class ChatControl(ChatControlBase): '''A control for standard 1-1 chat''' TYPE_ID = message_control.TYPE_CHAT old_msg_kind = None # last kind of the printed message - CHAT_CMDS = ['clear', 'compact', 'help', 'me', 'ping', 'say'] def __init__(self, parent_win, contact, acct, session, resource = None): ChatControlBase.__init__(self, self.TYPE_ID, parent_win, @@ -1718,83 +1696,11 @@ class ChatControl(ChatControlBase): elif self.session and self.session.enable_encryption: dialogs.ESessionInfoWindow(self.session) - def _process_command(self, message): - if message[0] != '/': - return False - - # Handle common commands - if ChatControlBase._process_command(self, message): - return True - - message = message[1:] - message_array = message.split(' ', 1) - command = message_array.pop(0).lower() - if message_array == ['']: - message_array = [] - - if command == 'me': - if len(message_array): - return False # /me is not really a command - else: - self.get_command_help(command) - return True # do not send "/me" as message - - if command == 'help': - if len(message_array): - subcommand = message_array.pop(0) - self.get_command_help(subcommand) - else: - self.get_command_help(command) - self.clear(self.msg_textview) - return True - elif command == 'ping': - if not len(message_array): - if self.account == gajim.ZEROCONF_ACC_NAME: - self.print_conversation( - _('Command not supported for zeroconf account.'), 'info') - else: - gajim.connections[self.account].sendPing(self.contact) - else: - self.get_command_help(command) - self.clear(self.msg_textview) - return True - return False - - def get_command_help(self, command): - if command == 'help': - self.print_conversation(_('Commands: %s') % ChatControl.CHAT_CMDS, - 'info') - elif command == 'clear': - self.print_conversation(_('Usage: /%s, clears the text window.') % \ - command, 'info') - elif command == 'compact': - self.print_conversation(_('Usage: /%s, hide the chat buttons.') % \ - command, 'info') - elif command == 'me': - self.print_conversation(_('Usage: /%(command)s , sends action ' - 'to the current group chat. Use third person. (e.g. /%(command)s ' - 'explodes.)' - ) % {'command': command}, 'info') - elif command == 'ping': - self.print_conversation(_('Usage: /%s, sends a ping to the contact') %\ - command, 'info') - elif command == 'say': - self.print_conversation(_('Usage: /%s, send the message to the contact') %\ - command, 'info') - else: - self.print_conversation(_('No help info for /%s') % command, 'info') - def send_message(self, message, keyID='', chatstate=None, xhtml=None): '''Send a message to contact''' - if message in ('', None, '\n') or self._process_command(message): + if message in ('', None, '\n'): return None - # Do we need to process command for the message ? - process_command = True - if message.startswith('/say'): - message = message[5:] - process_command = False - # refresh timers self.reset_kbd_mouse_timeout_vars() @@ -1853,7 +1759,7 @@ class ChatControl(ChatControlBase): ChatControlBase.send_message(self, message, keyID, type_='chat', chatstate=chatstate_to_send, composing_xep=composing_xep, - process_command=process_command, xhtml=xhtml, callback=_on_sent, + xhtml=xhtml, callback=_on_sent, callback_args=[contact, message, encrypted, xhtml]) def check_for_possible_paused_chatstate(self, arg): diff --git a/src/groupchat_control.py b/src/groupchat_control.py index 754c20df9..72912953c 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -182,10 +182,6 @@ class PrivateChatControl(ChatControl): class GroupchatControl(ChatControlBase): TYPE_ID = message_control.TYPE_GC - # alphanum sorted - MUC_CMDS = ['ban', 'block', 'chat', 'query', 'clear', 'close', 'compact', - 'help', 'invite', 'join', 'kick', 'leave', 'me', 'msg', 'nick', - 'part', 'names', 'say', 'topic', 'unblock'] def __init__(self, parent_win, contact, acct, is_continued=False): ChatControlBase.__init__(self, self.TYPE_ID, parent_win, @@ -281,7 +277,6 @@ class GroupchatControl(ChatControlBase): self.attention_list = [] self.room_creation = int(time.time()) # Use int to reduce mem usage self.nick_hits = [] - self.cmd_hits = [] self.last_key_tabs = False self.subject = '' @@ -1510,262 +1505,6 @@ class GroupchatControl(ChatControlBase): if model.iter_n_children(parent_iter) == 0: model.remove(parent_iter) - def _process_command(self, message): - if message[0] != '/': - return False - - # Handle common commands - if ChatControlBase._process_command(self, message): - return True - - message = message[1:] - message_array = message.split(' ', 1) - command = message_array.pop(0).lower() - if message_array == ['']: - message_array = [] - - if command == 'me': - return False # This is not really a command - - if command == 'nick': - # example: /nick foo - if len(message_array) and message_array[0] != self.nick: - nick = message_array[0] - try: - nick = helpers.parse_resource(nick) - except Exception: - # Invalid Nickname - dialogs.ErrorDialog(_('Invalid nickname'), - _('The nickname has not allowed characters.')) - return True - gajim.connections[self.account].join_gc(nick, self.room_jid, None, - change_nick=True) - self.new_nick = nick - self.clear(self.msg_textview) - else: - self.get_command_help(command) - return True - elif command == 'query' or command == 'chat': - # Open a chat window to the specified nick - # example: /query foo - if len(message_array): - nick0 = message_array.pop(0) - if nick0[-1] == ' ': - nick1 = nick0[:-1] - else: - nick1 = nick0 - nicks = gajim.contacts.get_nick_list(self.account, self.room_jid) - for nick in (nick0, nick1): - if nick in nicks: - self.on_send_pm(nick=nick) - self.clear(self.msg_textview) - return True - self.print_conversation(_('Nickname not found: %s') % \ - nick0, 'info') - else: - self.get_command_help(command) - return True - elif command == 'msg': - # Send a message to a nick. Also opens a private message window. - # example: /msg foo Hey, what's up? - if len(message_array): - message_array = message_array[0].split() - nick = message_array.pop(0) - room_nicks = gajim.contacts.get_nick_list(self.account, - self.room_jid) - if nick in room_nicks: - privmsg = ' '.join(message_array) - self.on_send_pm(nick=nick, msg=privmsg) - self.clear(self.msg_textview) - else: - self.print_conversation(_('Nickname not found: %s') % nick, - 'info') - else: - self.get_command_help(command) - return True - elif command == 'topic': - # display or change the room topic - # example: /topic : print topic - # /topic foo : change topic to foo - if len(message_array): - new_topic = message_array.pop(0) - gajim.connections[self.account].send_gc_subject(self.room_jid, - new_topic) - elif self.subject is not '': - self.print_conversation(self.subject, 'info') - else: - self.print_conversation(_('This group chat has no subject'), 'info') - self.clear(self.msg_textview) - return True - elif command == 'invite': - # invite a user to a room for a reason - # example: /invite user@example.com reason - if len(message_array): - message_array = message_array[0].split() - invitee = message_array.pop(0) - reason = ' '.join(message_array) - gajim.connections[self.account].send_invite(self.room_jid, invitee, - reason) - s = _('Invited %(contact_jid)s to %(room_jid)s.') % { - 'contact_jid': invitee, - 'room_jid': self.room_jid} - self.print_conversation(s, 'info') - self.clear(self.msg_textview) - else: - self.get_command_help(command) - return True - elif command == 'join': - # example: /join room@conference.example.com/nick - if len(message_array): - room_jid = message_array[0] - if room_jid.find('@') < 0: - room_jid = room_jid + '@' + gajim.get_server_from_jid( - self.room_jid) - else: - room_jid = '@' + gajim.get_server_from_jid(self.room_jid) - if room_jid.find('/') >= 0: - room_jid, nick = room_jid.split('/', 1) - else: - nick = '' - # join_gc window is needed in order to provide for password entry. - if 'join_gc' in gajim.interface.instances[self.account]: - gajim.interface.instances[self.account]['join_gc'].\ - window.present() - else: - try: - dialogs.JoinGroupchatWindow(account=None, room_jid=room_jid, - nick=nick) - except GajimGeneralException: - pass - self.clear(self.msg_textview) - return True - elif command == 'leave' or command == 'part' or command == 'close': - # Leave the room and close the tab or window - reason = 'offline' - if len(message_array): - reason = message_array.pop(0) - self.parent_win.remove_tab(self, self.parent_win.CLOSE_COMMAND, reason) - self.clear(self.msg_textview) - return True - elif command == 'ban': - if len(message_array): - room_nicks = gajim.contacts.get_nick_list(self.account, - self.room_jid) - nb_match = 0 - nick_ban = '' - for nick in room_nicks: - if message_array[0].startswith(nick): - nb_match += 1 - nick_ban = nick - test_reason = message_array[0][len(nick) + 1:] - if len(test_reason) == 0: - reason = 'None' - else: - reason = test_reason - banned_jid = None - if nb_match == 1: - gc_contact = gajim.contacts.get_gc_contact(self.account, - self.room_jid, nick_ban) - banned_jid = gc_contact.jid - elif nb_match > 1: - self.print_conversation(_('There is an ambiguity: %d nicks ' - 'match.\n Please use graphical interface ') % nb_match, - 'info') - self.clear(self.msg_textview) - elif message_array[0].split()[0].find('@') > 0: - message_splited = message_array[0].split(' ', 1) - banned_jid = message_splited[0] - if len(message_splited) == 2: - reason = message_splited[1] - else: - reason = 'None' - if banned_jid: - gajim.connections[self.account].gc_set_affiliation(self.room_jid, - banned_jid, 'outcast', reason) - self.clear(self.msg_textview) - else: - self.print_conversation(_('Nickname not found'), 'info') - else: - self.get_command_help(command) - return True - elif command == 'kick': - if len(message_array): - nick_kick = '' - room_nicks = gajim.contacts.get_nick_list(self.account, - self.room_jid) - nb_match = 0 - for nick in room_nicks: - if message_array[0].startswith(nick): - nb_match += 1 - nick_kick = nick - test_reason = message_array[0][len(nick) + 1:] - if len(test_reason) == 0: - reason = 'None' - else: - reason = test_reason - if nb_match == 1: - gajim.connections[self.account].gc_set_role(self.room_jid, - nick_kick, 'none', reason) - self.clear(self.msg_textview) - elif nb_match > 1: - self.print_conversation(_('There is an ambiguity: %d nicks ' - 'match.\n Please use graphical interface') % nb_match , - 'info' ) - self.clear(self.msg_textview) - else: - # We can't do the difference between nick and reason - # So we don't say the nick - self.print_conversation(_('Nickname not found') , 'info') - else: - self.get_command_help(command) - return True - elif command == 'names': - # print the list of participants - nicklist='' - i=0 - for contact in self.iter_contact_rows(): - nicklist += '[ %-12.12s ] ' % (contact[C_NICK].decode('utf-8')) - i=i+1 - if i == 3: - i=0 - self.print_conversation(nicklist, 'info') - nicklist='' - if nicklist: - self.print_conversation(nicklist, 'info') - self.clear(self.msg_textview) - return True - elif command == 'help': - if len(message_array): - subcommand = message_array.pop(0) - self.get_command_help(subcommand) - else: - self.get_command_help(command) - self.clear(self.msg_textview) - return True - elif command == 'say': - gajim.connections[self.account].send_gc_message(self.room_jid, - message[4:]) - self.clear(self.msg_textview) - return True - elif command == 'block': - if len(message_array) == 0: - self.get_command_help(command) - return True - nick = message_array[0].strip() - self.on_block(None, nick) - self.clear(self.msg_textview) - return True - elif command == 'unblock': - if len(message_array) == 0: - self.get_command_help(command) - return True - nick = message_array[0].strip() - self.on_unblock(None, nick) - self.clear(self.msg_textview) - return True - - return False - def send_message(self, message, xhtml=None): '''call this function to send our message''' if not message: @@ -1778,79 +1517,12 @@ class GroupchatControl(ChatControlBase): if message != '' or message != '\n': self.save_sent_message(message) - - if not self._process_command(message): - # Send the message - gajim.connections[self.account].send_gc_message(self.room_jid, - message, xhtml=xhtml) - self.msg_textview.get_buffer().set_text('') - self.msg_textview.grab_focus() - - def get_command_help(self, command): - if command == 'help': - self.print_conversation(_('Commands: %s') % GroupchatControl.MUC_CMDS, - 'info') - elif command == 'ban': - s = _('Usage: /%s [reason], bans the JID from the group' - ' chat. The nickname of an occupant may be substituted, but not if ' - 'it contains "@". If the JID is currently in the group chat, ' - 'he/she/it will also be kicked.') % command - self.print_conversation(s, 'info') - elif command == 'chat' or command == 'query': - self.print_conversation(_('Usage: /%s , opens a private chat' - ' window with the specified occupant.') % command, 'info') - elif command == 'clear': - self.print_conversation( - _('Usage: /%s, clears the text window.') % command, 'info') - elif command == 'close' or command == 'leave' or command == 'part': - self.print_conversation(_('Usage: /%s [reason], closes the current ' - 'window or tab, displaying reason if specified.') % command, 'info') - elif command == 'compact': - self.print_conversation(_('Usage: /%s, hide the chat buttons.') % \ - command, 'info') - elif command == 'invite': - self.print_conversation(_('Usage: /%s [reason], invites JID to ' - 'the current group chat, optionally providing a reason.') % command, - 'info') - elif command == 'join': - self.print_conversation(_('Usage: /%s @[/nickname], ' - 'offers to join room@server optionally using specified nickname.') \ - % command, 'info') - elif command == 'kick': - self.print_conversation(_('Usage: /%s [reason], removes ' - 'the occupant specified by nickname from the group chat and ' - 'optionally displays a reason.') % command, 'info') - elif command == 'me': - self.print_conversation(_('Usage: /%(command)s , sends action ' - 'to the current group chat. Use third person. (e.g. /%(command)s ' - 'explodes.)') % {'command': command}, 'info') - elif command == 'msg': - s = _('Usage: /%s [message], opens a private message window' - ' and sends message to the occupant specified by nickname.') % \ - command - self.print_conversation(s, 'info') - elif command == 'nick': - s = _('Usage: /%s , changes your nickname in current group ' - 'chat.') % command - self.print_conversation(s, 'info') - elif command == 'names': - s = _('Usage: /%s , display the names of group chat occupants.')\ - % command - self.print_conversation(s, 'info') - elif command == 'topic': - self.print_conversation(_('Usage: /%s [topic], displays or updates the' - ' current group chat topic.') % command, 'info') - elif command == 'say': - self.print_conversation(_('Usage: /%s , sends a message ' - 'without looking for other commands.') % command, 'info') - elif command == 'block': - self.print_conversation(_('Usage: /%s , prevent ' - 'to send you messages or private messages.') % command, 'info') - elif command == 'unblock': - self.print_conversation(_('Usage: /%s , allow ' - 'to send you messages and private messages.') % command, 'info') - else: - self.print_conversation(_('No help info for /%s') % command, 'info') + + # Send the message + gajim.connections[self.account].send_gc_message(self.room_jid, + message, xhtml=xhtml) + self.msg_textview.get_buffer().set_text('') + self.msg_textview.grab_focus() def get_role(self, nick): gc_contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, @@ -2100,41 +1772,6 @@ class GroupchatControl(ChatControlBase): 'utf-8') splitted_text = text.split() - # topic completion - splitted_text2 = text.split(None, 1) - if text.startswith('/topic '): - if len(splitted_text2) == 2 and \ - self.subject.startswith(splitted_text2[1]) and\ - len(self.subject) > len(splitted_text2[1]): - message_buffer.insert_at_cursor( - self.subject[len(splitted_text2[1]):]) - return True - elif len(splitted_text2) == 1 and text.startswith('/topic '): - message_buffer.delete(start_iter, end_iter) - message_buffer.insert_at_cursor('/topic '+self.subject) - return True - - # command completion - if text.startswith('/') and len(splitted_text) == 1: - text = splitted_text[0] - if len(text) == 1: # user wants to cycle all commands - self.cmd_hits = GroupchatControl.MUC_CMDS - else: - # cycle possible commands depending on what the user typed - if self.last_key_tabs and len(self.cmd_hits) and \ - self.cmd_hits[0].startswith(text.lstrip('/')): - self.cmd_hits.append(self.cmd_hits[0]) - self.cmd_hits.pop(0) - else: # find possible commands - self.cmd_hits = [] - for cmd in GroupchatControl.MUC_CMDS: - if cmd.startswith(text.lstrip('/')): - self.cmd_hits.append(cmd) - if len(self.cmd_hits): - message_buffer.delete(start_iter, end_iter) - message_buffer.insert_at_cursor('/' + self.cmd_hits[0] + ' ') - self.last_key_tabs = True - return True # nick completion # check if tab is pressed with empty message From ae0f32d9225a5d4a578f1ca1e4e68242253ace86 Mon Sep 17 00:00:00 2001 From: red-agent Date: Sat, 12 Sep 2009 16:51:21 +0300 Subject: [PATCH 11/39] Dropped in the brand new and shiny command system --- launch.sh | 2 +- src/chat_control.py | 22 +- src/commands/__init__.py | 0 src/commands/custom.py | 88 +++++ src/commands/framework.py | 610 +++++++++++++++++++++++++++++++++ src/commands/implementation.py | 124 +++++++ src/commands/middleware.py | 97 ++++++ src/gajim.py | 9 + src/groupchat_control.py | 20 +- 9 files changed, 961 insertions(+), 11 deletions(-) create mode 100644 src/commands/__init__.py create mode 100644 src/commands/custom.py create mode 100644 src/commands/framework.py create mode 100644 src/commands/implementation.py create mode 100644 src/commands/middleware.py diff --git a/launch.sh b/launch.sh index bcb968553..c7c361f3d 100755 --- a/launch.sh +++ b/launch.sh @@ -1,3 +1,3 @@ #!/bin/sh cd "$(dirname $0)/src" -exec python -OOt gajim.py $@ +exec python -Ot gajim.py $@ diff --git a/src/chat_control.py b/src/chat_control.py index 6bf451ff9..dc259dfbe 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -51,6 +51,8 @@ from common.pep import MOODS, ACTIVITIES from common.xmpp.protocol import NS_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC from common.xmpp.protocol import NS_RECEIPTS, NS_ESESSION +from commands.implementation import CommonCommands, ChatCommands + try: import gtkspell HAS_GTK_SPELL = True @@ -76,9 +78,12 @@ if gajim.config.get('use_speller') and HAS_GTK_SPELL: del langs[lang] ################################################################################ -class ChatControlBase(MessageControl): +class ChatControlBase(MessageControl, CommonCommands): '''A base class containing a banner, ConversationTextview, MessageTextView ''' + + DISPATCHED_BY = CommonCommands + def make_href(self, match): url_color = gajim.config.get('urlmsgcolor') return '%s' % (match.group(), @@ -605,12 +610,15 @@ class ChatControlBase(MessageControl): def send_message(self, message, keyID='', type_='chat', chatstate=None, msg_id=None, composing_xep=None, resource=None, - xhtml=None, callback=None, callback_args=[]): + xhtml=None, callback=None, callback_args=[], process_commands=True): '''Send the given message to the active tab. Doesn't return None if error ''' if not message or message == '\n': return None + if process_commands and self.process_as_command(message): + return + MessageControl.send_message(self, message, keyID, type_=type_, chatstate=chatstate, msg_id=msg_id, composing_xep=composing_xep, resource=resource, user_nick=self.user_nick, xhtml=xhtml, @@ -1104,11 +1112,13 @@ class ChatControlBase(MessageControl): # FIXME: Set sensitivity for toolbar ################################################################################ -class ChatControl(ChatControlBase): +class ChatControl(ChatControlBase, ChatCommands): '''A control for standard 1-1 chat''' TYPE_ID = message_control.TYPE_CHAT old_msg_kind = None # last kind of the printed message + DISPATCHED_BY = ChatCommands + def __init__(self, parent_win, contact, acct, session, resource = None): ChatControlBase.__init__(self, self.TYPE_ID, parent_win, 'chat_child_vbox', contact, acct, resource) @@ -1696,7 +1706,8 @@ class ChatControl(ChatControlBase): elif self.session and self.session.enable_encryption: dialogs.ESessionInfoWindow(self.session) - def send_message(self, message, keyID='', chatstate=None, xhtml=None): + def send_message(self, message, keyID='', chatstate=None, xhtml=None, + process_commands=True): '''Send a message to contact''' if message in ('', None, '\n'): return None @@ -1760,7 +1771,8 @@ class ChatControl(ChatControlBase): ChatControlBase.send_message(self, message, keyID, type_='chat', chatstate=chatstate_to_send, composing_xep=composing_xep, xhtml=xhtml, callback=_on_sent, - callback_args=[contact, message, encrypted, xhtml]) + callback_args=[contact, message, encrypted, xhtml], + process_commands=process_commands) def check_for_possible_paused_chatstate(self, arg): ''' did we move mouse of that window or write something in message diff --git a/src/commands/__init__.py b/src/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/commands/custom.py b/src/commands/custom.py new file mode 100644 index 000000000..84827c5b0 --- /dev/null +++ b/src/commands/custom.py @@ -0,0 +1,88 @@ +# Copyright (C) 2009 red-agent +# +# 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, either version 3 of the License, or +# (at your option) any later version. +# +# 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. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +This module contains examples of how to create your own commands by creating an +adhoc command processor. Each adhoc command processor should be hosted by one or +more which dispatch the real deal and droppped in to where it belongs. +""" + +from framework import command +from implementation import ChatCommands, PrivateChatCommands, GroupChatCommands + +class CustomCommonCommands(ChatCommands, PrivateChatCommands, GroupChatCommands): + """ + This adhoc processor will be hosted by a multiple processors which dispatch + commands from all, chat, private chat and group chat. So commands defined + here will be available to all of them. + """ + + IS_COMMAND_PROCESSOR = True + HOSTED_BY = ChatCommands, PrivateChatCommands, GroupChatCommands + + @command + def dance(self): + """ + First line of the doc string is called a description and will be + programmatically extracted. + + After that you can give more help, like explanation of the options. This + one will be programatically extracted and formatted too. After this one + there will be autogenerated (based on the method signature) usage + information appended. You can turn it off though, if you want. + """ + return "I can't dance, you stupid fuck, I'm just a command system! A cool one, though..." + +class CustomChatCommands(ChatCommands): + """ + This adhoc processor will be hosted by a ChatCommands processor which + dispatches commands from a chat. So commands defined here will be available + only to a chat. + """ + + IS_COMMAND_PROCESSOR = True + HOSTED_BY = ChatCommands + + @command + def sing(self): + return "Are you phreaking kidding me? Buy yourself a damn stereo..." + +class CustomPrivateChatCommands(PrivateChatCommands): + """ + This adhoc processor will be hosted by a PrivateChatCommands processor which + dispatches commands from a private chat. So commands defined here will be + available only to a private chat. + """ + + IS_COMMAND_PROCESSOR = True + HOSTED_BY = PrivateChatCommands + + @command + def make_coffee(self): + return "What do I look like, you ass? A coffee machine!?" + +class CustomGroupChatCommands(GroupChatCommands): + """ + This adhoc processor will be hosted by a GroupChatCommands processor which + dispatches commands from a group chat. So commands defined here will be + available only to a group chat. + """ + + IS_COMMAND_PROCESSOR = True + HOSTED_BY = GroupChatCommands + + @command + def fetch(self): + return "You should really buy yourself a dog and start torturing it instead of me..." diff --git a/src/commands/framework.py b/src/commands/framework.py new file mode 100644 index 000000000..ff655a56d --- /dev/null +++ b/src/commands/framework.py @@ -0,0 +1,610 @@ +# Copyright (C) 2009 red-agent +# +# 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, either version 3 of the License, or +# (at your option) any later version. +# +# 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. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Provides a tiny framework with simple, yet powerful and extensible architecture +to implement commands in a streight and flexible, declarative way. +""" + +import re +from types import FunctionType, UnicodeType, TupleType, ListType +from inspect import getargspec + +class CommandInternalError(Exception): + pass + +class Command(object): + + DOC_STRIP_PATTERN = re.compile(r'(?:^[ \t]+|\A\n)', re.MULTILINE) + DOC_FORMAT_PATTERN = re.compile(r'(?" % ', '.join(self.names) + + def __cmp__(self, other): + """ + Comparison is implemented based on a first name. + """ + return cmp(self.first_name, other.first_name) + + @property + def first_name(self): + return self.names[0] + + @property + def native_name(self): + return self.handler.__name__ + + def extract_doc(self): + """ + Extract handler's doc-string and transform it to a usable format. + """ + doc = self.handler.__doc__ or None + + if not doc: + return + + doc = re.sub(self.DOC_STRIP_PATTERN, str(), doc) + doc = re.sub(self.DOC_FORMAT_PATTERN, ' ', doc) + + return doc + + def extract_description(self): + """ + Extract handler's description (which is a first line of the doc). Try to + keep them simple yet meaningful. + """ + doc = self.extract_doc() + if doc: + return doc.split('\n', 1)[0] + + def extract_arg_spec(self): + names, var_args, var_kwargs, defaults = getargspec(self.handler) + + # Behavior of this code need to be checked. Might yield incorrect + # results on some rare occasions. + spec_args = names[:-len(defaults) if defaults else len(names)] + spec_kwargs = dict(zip(names[-len(defaults):], defaults)) if defaults else {} + + # Removing self from arguments specification in case if command handler + # is an instance method. + if self.is_instance and spec_args.pop(0) != 'self': + raise CommandInternalError("Invalid arguments specification") + + return spec_args, spec_kwargs, var_args, var_kwargs + + def extract_arg_usage(self, complete=True): + """ + Extract handler's arguments specification and wrap them in a + human-readable format. If complete is given - then ARG_USAGE_PATTERN + will be used to render it completly. + """ + names, _var_args, _var_kwargs, defaults = getargspec(self.handler) + spec_args, spec_kwargs, var_args, var_kwargs = self.extract_arg_spec() + + '__arguments__' not in spec_args or spec_args.remove('__arguments__') + + optional = '__optional__' in spec_args + if optional: + spec_args.remove('__optional__') + + kwargs = [] + letters = [] + + # The reason we don't iterate here through spec_kwargs, like we would + # normally do is that it does not retains order of items. We need to be + # sure that arguments will be printed in the order they were specified. + for key in (names[-len(defaults):] if defaults else ()): + value = spec_kwargs[key] + letter = key[0] + + if self.dashes: + key = key.replace('_', '-') + + if letter not in letters: + kwargs.append('-(-%s)%s=%s' % (letter, key[1:], value)) + letters.append(letter) + else: + kwargs.append('--%s=%s' % (key, value)) + + usage = str() + args = str() + + if len(spec_args) == 1 and self.raw: + args += ('(|%s|)' if self.empty else '|%s|') % spec_args[0] + elif spec_args or var_args or optional: + if spec_args: + args += '<%s>' % ', '.join(spec_args) + if var_args or optional: + args += (' ' if spec_args else str()) + '<<%s>>' % (var_args or self.optional) + + usage += args + + if kwargs or var_kwargs: + if kwargs: + usage += (' ' if args else str()) + '[%s]' % ', '.join(kwargs) + if var_kwargs: + usage += (' ' if args else str()) + '[[%s]]' % var_kwargs + + # Native name will be the first one if it is included. Otherwise, names + # will be in the order they were specified. + if len(self.names) > 1: + names = '%s (%s)' % (self.first_name, ', '.join(self.names[1:])) + else: + names = self.first_name + + return usage if not complete else self.ARG_USAGE_PATTERN % (names, usage) + +class CommandError(Exception): + def __init__(self, command, *args, **kwargs): + if isinstance(command, Command): + self.command = command + self.name = command.first_name + self.name = command + super(Exception, self).__init__(*args, **kwargs) + +class Dispatcher(type): + table = {} + hosted = {} + + def __init__(cls, name, bases, dct): + dispatchable = Dispatcher.check_if_dispatchable(bases, dct) + hostable = Dispatcher.check_if_hostable(bases, dct) + + if Dispatcher.is_suitable(cls, dct): + Dispatcher.register_processor(cls) + + # Sanitize names even if processor is not suitable for registering, + # because it might be inherited by an another processor. + Dispatcher.sanitize_names(cls) + + super(Dispatcher, cls).__init__(name, bases, dct) + + @classmethod + def is_suitable(cls, proc, dct): + is_not_root = dct.get('__metaclass__') is not cls + is_processor = bool(dct.get('IS_COMMAND_PROCESSOR')) + return is_not_root and is_processor + + @classmethod + def check_if_dispatchable(cls, bases, dct): + dispatcher = dct.get('DISPATCHED_BY') + if not dispatcher: + return False + if dispatcher not in bases: + raise CommandInternalError("Should be dispatched by the same processor it inherits from") + return True + + @classmethod + def check_if_hostable(cls, bases, dct): + hosters = dct.get('HOSTED_BY') + if not hosters: + return False + if not isinstance(hosters, (TupleType, ListType)): + hosters = (hosters,) + for hoster in hosters: + if hoster not in bases: + raise CommandInternalError("Should be hosted by the same processors it inherits from") + return True + + @classmethod + def check_if_conformed(cls, dispatchable, hostable): + if dispatchable and hostable: + raise CommandInternalError("Processor can not be dispatchable and hostable at the same time") + + @classmethod + def register_processor(cls, proc): + cls.table[proc] = {} + inherited = proc.__dict__.get('INHERITED') + + if 'HOSTED_BY' in proc.__dict__: + cls.register_adhocs(proc) + + commands = cls.traverse_commands(proc, inherited) + cls.register_commands(proc, commands) + + @classmethod + def sanitize_names(cls, proc): + inherited = proc.__dict__.get('INHERITED') + commands = cls.traverse_commands(proc, inherited) + for key, command in commands: + if not proc.SAFE_NAME_SCAN_PATTERN.match(key): + setattr(proc, proc.SAFE_NAME_SUBS_PATTERN % key, command) + try: + delattr(proc, key) + except AttributeError: + pass + + @classmethod + def traverse_commands(cls, proc, inherited=True): + keys = dir(proc) if inherited else proc.__dict__.iterkeys() + for key in keys: + value = getattr(proc, key) + if isinstance(value, Command): + yield key, value + + @classmethod + def register_commands(cls, proc, commands): + for key, command in commands: + for name in command.names: + name = proc.prepare_name(name) + if name not in cls.table[proc]: + cls.table[proc][name] = command + else: + raise CommandInternalError("Command with name %s already exists" % name) + @classmethod + def register_adhocs(cls, proc): + hosters = proc.HOSTED_BY + if not isinstance(hosters, (TupleType, ListType)): + hosters = (hosters,) + for hoster in hosters: + if hoster in cls.hosted: + cls.hosted[hoster].append(proc) + else: + cls.hosted[hoster] = [proc] + + @classmethod + def retrieve_command(cls, proc, name): + command = cls.table[proc.DISPATCHED_BY].get(name) + if command: + return command + if proc.DISPATCHED_BY in cls.hosted: + for adhoc in cls.hosted[proc.DISPATCHED_BY]: + command = cls.table[adhoc].get(name) + if command: + return command + + @classmethod + def list_commands(cls, proc): + commands = dict(cls.traverse_commands(proc.DISPATCHED_BY)) + if proc.DISPATCHED_BY in cls.hosted: + for adhoc in cls.hosted[proc.DISPATCHED_BY]: + inherited = adhoc.__dict__.get('INHERITED') + commands.update(dict(cls.traverse_commands(adhoc, inherited))) + return commands.values() + +class CommandProcessor(object): + """ + A base class for a drop-in command processor which you can drop (make your + class to inherit from it) in any of your classes to support commands. In + order to get it done you need to make your own processor, inheriter from + CommandProcessor and then drop it in. Don't forget about few important steps + described below. + + Every command in the processor (normally) will gain full access through self + to an object you are adding commands to. + + Your subclass, which will contain commands should define in its body + IS_COMMAND_PROCESSOR = True in order to be included in the dispatching + table. + + Every class you will drop the processor in should define DISPATCHED_BY set + to the same processor you are inheriting from. + + Names of the commands after preparation stuff id done will be sanitized + (based on SAFE_NAME_SCAN_PATTERN and SAFE_NAME_SUBS_PATTERN) in order not to + interfere with the methods defined in a class you will drop a processor in. + + If you want to create an adhoc processor (then one that parasites on the + other one (the host), so it does not have to be included directly into + whatever includes the host) you need to inherit you processor from the host + and set HOSTED_BY to that host. + + INHERITED controls whether commands inherited from base classes (which could + include other processors) will be registered or not. This is disabled + by-default because it leads to unpredictable consequences when used in adhoc + processors which inherit from more then one processor or has such processors + in its inheritance tree. In that case - encapsulation is being broken and + some (all) commands are shared between non-related processors. + """ + __metaclass__ = Dispatcher + + SAFE_NAME_SCAN_PATTERN = re.compile(r'_(?P\w+)_') + SAFE_NAME_SUBS_PATTERN = '_%s_' + + # Quite complex piece of regular expression logic. + ARG_PATTERN = re.compile(r'(\'|")?(?P(?(1).+?|\S+))(?(1)\1)') + OPT_PATTERN = re.compile(r'--?(?P[\w-]+)(?:(?:=|\s)(\'|")?(?P(?(2)[^-]+?|[^-\s]+))(?(2)\2))?') + + EXPAND_SHORT_OPTIONS = True + + COMMAND_PREFIX = '/' + CASE_SENVITIVE_COMMANDS = False + + ARG_ENCODING = 'utf8' + + def __getattr__(self, name): + """ + This allows to reach and directly (internally) call commands which are + defined in (other) adhoc processors. + """ + command_name = self.SAFE_NAME_SCAN_PATTERN.match(name) + if command_name: + command = Dispatcher.retrieve_command(self, command_name.group('name')) + if command: + return command + raise AttributeError(name) + return super(CommandProcessor, self).__getattr__(name) + + @classmethod + def prepare_name(cls, name): + return name if cls.CASE_SENVITIVE_COMMANDS else name.lower() + + @classmethod + def retrieve_command(cls, name): + name = cls.prepare_name(name) + command = Dispatcher.retrieve_command(cls, name) + if not command: + raise CommandError(name, "Command does not exist") + return command + + @classmethod + def list_commands(cls): + commands = Dispatcher.list_commands(cls) + return sorted(set(commands)) + + @classmethod + def parse_command_arguments(cls, arguments): + """ + Simple yet effective and sufficient in most cases parser which parses + command arguments and maps them to *args and **kwargs, which we all use + extensivly in daily Python coding. + + The format of the input arguments should be: + <> [-(-o)ption=value1, -(-a)nother=value2] [[extra_options]] + + Options may be given in --long or -short format. As --option=value or + --option value or -option value. Keys without values will get True as + value. Arguments and option values that contain spaces may be given as + 'one two three' or "one two three"; that is between single or double + quotes. + """ + args, kwargs = [], {} + + # Need to store every option we have parsed in order to get arguments + # to be parsed correct later. + options = [] + + def intersects((given_start, given_end)): + """ + Check if something intersects with boundaries of any parsed options. + """ + for start, end in options: + if given_start == start or given_end == end: + return True + return False + + for match in re.finditer(cls.OPT_PATTERN, arguments): + if match: + options.append(match.span()) + kwargs[match.group('key')] = match.group('value') or True + + for match in re.finditer(cls.ARG_PATTERN, arguments): + if match and not intersects(match.span()): + args.append(match.group('body')) + + return args, kwargs + + @classmethod + def adapt_command_arguments(cls, command, arguments, args, kwargs): + """ + Adapts *args and **kwargs got from a parser to a specific handler by + means of arguments specified on command definition. + + When EXPAND_SHORT_OPTIONS is set then if command receives one-latter + options (like -v or -f) they will be expanded to a verbose ones (like + --verbose or --file) if the latter are defined as a command optional + argumens. Expansion is made on a first-latter comparison basis. If more + then one long option with the same first letter defined - only first one + will be used in expanding. + + If command defines __arguments__ as a first argument - then this + argument will receive raw and unprocessed arguments. Also, if nothing + except __arguments__ (including *args, *kwargs splatting) is defined - + then all parsed arguments will be discarded. It will be discarded in the + argument usage information. + + If command defines __optional__ - that is an analogue for *args, to + collect extra arguments. This is a preffered way over *args. Because of + some Python limitations, *args could not be mapped to as expected. And + it is hardly advised to define it after all hard arguments. + + Extra arguments which are not considered extra (or optional) - will be + passed as if they were value for keywords, in the order keywords are + defined and printed in usage. + """ + spec_args, spec_kwargs, var_args, var_kwargs = command.extract_arg_spec() + + if command.raw: + if len(spec_args) == 1: + if arguments or command.empty: + return (arguments,), {} + raise CommandError(command, "Can not be used without arguments") + raise CommandInternalError("Raw command must define no more then one argument") + + if '__optional__' in spec_args: + if not var_args: + hard_len = len(spec_args) - 1 + optional = args[hard_len:] + args = args[:hard_len] + args.insert(spec_args.index('__optional__'), optional) + raise CommandInternalError("Cant have both, __optional__ and *args") + + if command.dashes: + for key, value in kwargs.items(): + if '-' in key: + del kwargs[key] + kwargs[key.replace('-', '_')] = value + + if cls.EXPAND_SHORT_OPTIONS: + expanded = [] + for key, value in spec_kwargs.iteritems(): + letter = key[0] if len(key) > 1 else None + if letter and letter in kwargs and letter not in expanded: + expanded.append(letter) + kwargs[key] = kwargs[letter] + del kwargs[letter] + + # We need to encode every keyword argument to a simple string, not the + # unicode one, because ** expanding does not support it. The nasty issue + # here to consider is that if dict key was initially set as u'test', + # then resetting it to just 'test' leaves u'test' as it was... + for key, value in kwargs.items(): + if isinstance(key, UnicodeType): + del kwargs[key] + kwargs[key.encode(cls.ARG_ENCODING)] = value + + if '__arguments__' in spec_args: + if len(spec_args) == 1 and not spec_kwargs and not var_args and not var_kwargs: + return (arguments,), {} + args.insert(spec_args.index('__arguments__'), arguments) + + return args, kwargs + + def process_as_command(self, text): + """ + Try to process text as a command. Returns True if it is a command and + False if it is not. + """ + if not text.startswith(self.COMMAND_PREFIX): + return False + + text = text[len(self.COMMAND_PREFIX):] + text = text.lstrip() + + parts = text.split(' ', 1) + + if len(parts) > 1: + name, arguments = parts + else: + name, arguments = parts[0], None + + flag = self.looks_like_command(text, name, arguments) + if flag is not None: + return flag + + self.execute_command(name, arguments) + + return True + + def execute_command(self, name, arguments): + command = self.retrieve_command(name) + + args, kwargs = self.parse_command_arguments(arguments) if arguments else ([], {}) + args, kwargs = self.adapt_command_arguments(command, arguments, args, kwargs) + + try: + if self.command_preprocessor(name, command, arguments, args, kwargs): + return + value = command(self, *args, **kwargs) + self.command_postprocessor(name, command, arguments, args, kwargs, value) + except TypeError: + raise CommandError(name, "Command received invalid arguments") + + def command_preprocessor(self, name, command, arguments, args, kwargs): + """ + Redefine this method in the subclass to execute custom code before + command gets executed. If returns True then command execution will be + interrupted and command will not be executed. + """ + pass + + def command_postprocessor(self, name, command, arguments, args, kwargs, output): + """ + Redefine this method in the subclass to execute custom code after + command gets executed. + """ + pass + + def looks_like_command(self, text, name, arguments): + """ + This hook is being called before any processing, but after it was + determined that text looks like a command. If returns non None value + - then further processing will be interrupted and that value will be + used to return from process_as_command. + """ + pass + +def command(*names, **kwargs): + """ + A decorator which provides a declarative way of defining commands. + + You can specify a set of names by which you can call the command. If names + are empty - then the name of the command will be set to native (extracted + from the handler name). If no_native=True argument is given and names is + non-empty - then native name will not be added. + + If command handler is not an instance method then is_instance=False should + be given. Though mentioned case is not covered by defined behaviour, and + should not be used, unless you know what you are doing. + + If usage=True is given - then handler's doc will be appended with an + auto-gereated usage info. + + If raw=True is given then command should define only one argument to + which all raw, unprocessed command arguments will be given. + + If dashes=True is given, then dashes (-) in the option + names will be converted to underscores. So you can map --one-more-option to + a one_more_option=None. + + If optional is set to a string then if __optional__ specified - its name + ('optional' by-default) in the usage info will be substitued by whatever is + given. + + If empty=True is given - then if raw is enabled it will allow to pass empty + (None) raw arguments to a command. + """ + names = list(names) + + no_native = kwargs.get('no_native', False) + is_instance = kwargs.get('is_instance', True) + usage = kwargs.get('usage', True) + raw = kwargs.get('raw', False) + dashes = kwargs.get('dashes', True) + optional = kwargs.get('optional', 'optional') + empty = kwargs.get('empty', False) + + def decorator(handler): + command = Command(handler, is_instance, usage, raw, dashes, optional, empty) + + # Extract and inject native name while making sure it is going to be the + # first one in the list. + if not names or names and not no_native: + names.insert(0, command.native_name) + command.names = tuple(names) + + return command + + # Workaround if we are getting called without parameters. + if len(names) == 1 and isinstance(names[0], FunctionType): + return decorator(names.pop()) + + return decorator diff --git a/src/commands/implementation.py b/src/commands/implementation.py new file mode 100644 index 000000000..a921116e7 --- /dev/null +++ b/src/commands/implementation.py @@ -0,0 +1,124 @@ +# Copyright (C) 2009 red-agent +# +# 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, either version 3 of the License, or +# (at your option) any later version. +# +# 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. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Provides an actual implementation of the standard commands. +""" + +from common import gajim + +from framework import command, CommandError +from middleware import ChatMiddleware + +class CommonCommands(ChatMiddleware): + """ + Here defined commands will be common to all, chat, private chat and group + chat. Keep in mind that self is set to an instance of either ChatControl, + PrivateChatControl or GroupchatControl when command is being called. + """ + + IS_COMMAND_PROCESSOR = True + + @command + def clear(self): + """ + Clear the text window + """ + self.conv_textview.clear() + + @command + def compact(self): + """ + Hide the chat buttons + """ + self.chat_buttons_set_visible(not self.hide_chat_buttons) + + @command + def help(self, command=None, all=False): + """ + Show help on a given command or a list of available commands if -(-a)ll is + given + """ + if command: + command = self.retrieve_command(command) + + doc = _(command.extract_doc()) + usage = command.extract_arg_usage() + + if doc: + return (doc + '\n\n' + usage) if command.usage else doc + else: + return usage + elif all: + for command in self.list_commands(): + names = ', '.join(command.names) + description = command.extract_description() + + self.echo("%s - %s" % (names, description)) + else: + self.echo(self._help_(self, 'help')) + + @command(raw=True) + def say(self, message): + """ + Send a message to the contact + """ + self.send(message) + + @command(raw=True) + def me(self, action): + """ + Send action (in the third person) to the current chat + """ + self.send("/me %s" % action) + +class ChatCommands(CommonCommands): + """ + Here defined commands will be unique to a chat. Use it as a hoster to provide + commands which should be unique to a chat. Keep in mind that self is set to + an instance of ChatControl when command is being called. + """ + + IS_COMMAND_PROCESSOR = True + INHERITED = True + + @command + def ping(self): + """ + Send a ping to the contact + """ + if self.account == gajim.ZEROCONF_ACC_NAME: + raise CommandError(ping, _('Command is not supported for zeroconf accounts')) + gajim.connections[self.account].sendPing(self.contact) + +class PrivateChatCommands(CommonCommands): + """ + Here defined commands will be unique to a private chat. Use it as a hoster to + provide commands which should be unique to a private chat. Keep in mind that + self is set to an instance of PrivateChatControl when command is being called. + """ + + IS_COMMAND_PROCESSOR = True + INHERITED = True + +class GroupChatCommands(CommonCommands): + """ + Here defined commands will be unique to a group chat. Use it as a hoster to + provide commands which should be unique to a group chat. Keep in mind that + self is set to an instance of GroupchatControl when command is being called. + """ + + IS_COMMAND_PROCESSOR = True + INHERITED = True diff --git a/src/commands/middleware.py b/src/commands/middleware.py new file mode 100644 index 000000000..a0db07bdd --- /dev/null +++ b/src/commands/middleware.py @@ -0,0 +1,97 @@ +# Copyright (C) 2009 red-agent +# +# 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, either version 3 of the License, or +# (at your option) any later version. +# +# 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. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Provides a glue to tie command system framework and the actual code where it +would be dropped in. Defines a little bit of scaffolding to support interaction +between the two and a few utility methods so you don't need to dig up the host +code to write basic commands. +""" + +from types import StringTypes +from framework import CommandProcessor, CommandError +from traceback import print_exc + +class ChatMiddleware(CommandProcessor): + """ + Provides basic scaffolding for the convenient interaction with ChatControl. + Also provides some few basic utilities for the same purpose. + """ + + def process_as_command(self, text): + try: + return super(ChatMiddleware, self).process_as_command(text) + except CommandError, exception: + self.echo("%s: %s" %(exception.name, exception.message), 'error') + return True + except Exception: + self.echo("An error occured while trying to execute the command", 'error') + print_exc() + return True + finally: + self.add_history(text) + self.clear_input() + + def looks_like_command(self, text, name, arguments): + # Command escape stuff ggoes here. If text was prepended by the command + # prefix twice, like //not_a_command (if prefix is set to /) then it + # will be escaped, that is sent just as a regular message with one (only + # one) prefix removed, so message will be /not_a_command. + if name.startswith(self.COMMAND_PREFIX): + self._say_(self, text) + return True + + def command_preprocessor(self, name, command, arguments, args, kwargs): + if 'h' in kwargs or 'help' in kwargs: + # Forwarding to the /help command. Dont forget to pass self, as + # all commands are unbound. And also don't forget to print output. + self.echo(self._help_(self, name)) + return True + + def command_postprocessor(self, name, command, arguments, args, kwargs, value): + if value and isinstance(value, StringTypes): + self.echo(value) + + def echo(self, text, kind='info'): + """ + Print given text to the user. + """ + self.print_conversation(str(text), kind) + + def send(self, text): + """ + Send a message to the contact. + """ + self.send_message(text, process_commands=False) + + def set_input(self, text): + """ + Set given text into the input. + """ + message_buffer = self.msg_textview.get_buffer() + message_buffer.set_text(text) + + def clear_input(self): + """ + Clear input. + """ + self.set_input(str()) + + def add_history(self, text): + """ + Add given text to the input history, so user can scroll through it + using ctrl + up/down arrow keys. + """ + self.save_sent_message(text) diff --git a/src/gajim.py b/src/gajim.py index b8512ac43..fc7f581f7 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -230,6 +230,15 @@ from chat_control import ChatControlBase from chat_control import ChatControl from groupchat_control import GroupchatControl from groupchat_control import PrivateChatControl + +# Here custom adhoc processors should be loaded. At this point there is +# everything they need to function properly. The next line loads custom exmple +# adhoc processors. Technically, they could be loaded earlier as host processors +# themself does not depend on the chat controls, but that should not be done +# uless there is a really good reason for that.. +# +# from commands import custom + from atom_window import AtomWindow from session import ChatControlSession diff --git a/src/groupchat_control.py b/src/groupchat_control.py index 72912953c..6b959a5e0 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -47,6 +47,8 @@ from chat_control import ChatControl from chat_control import ChatControlBase from common.exceptions import GajimGeneralException +from commands.implementation import PrivateChatCommands, GroupChatCommands + import logging log = logging.getLogger('gajim.groupchat_control') @@ -116,9 +118,11 @@ def tree_cell_data_func(column, renderer, model, iter_, tv=None): renderer.set_property('font', gtkgui_helpers.get_theme_font_for_option(theme, 'groupfont')) -class PrivateChatControl(ChatControl): +class PrivateChatControl(ChatControl, PrivateChatCommands): TYPE_ID = message_control.TYPE_PM + DISPATCHED_BY = PrivateChatCommands + def __init__(self, parent_win, gc_contact, contact, account, session): room_jid = contact.jid.split('/')[0] room_ctrl = gajim.interface.msg_win_mgr.get_gc_control(room_jid, account) @@ -132,7 +136,7 @@ class PrivateChatControl(ChatControl): ChatControl.__init__(self, parent_win, contact, account, session) self.TYPE_ID = 'pm' - def send_message(self, message, xhtml=None): + def send_message(self, message, xhtml=None, process_commands=True): '''call this function to send our message''' if not message: return @@ -158,7 +162,8 @@ class PrivateChatControl(ChatControl): 'left.') % {'room': room, 'nick': nick}) return - ChatControl.send_message(self, message, xhtml=xhtml) + ChatControl.send_message(self, message, xhtml=xhtml, + process_commands=process_commands) def update_ui(self): if self.contact.show == 'offline': @@ -180,9 +185,11 @@ class PrivateChatControl(ChatControl): self.session.negotiate_e2e(False) -class GroupchatControl(ChatControlBase): +class GroupchatControl(ChatControlBase, GroupChatCommands): TYPE_ID = message_control.TYPE_GC + DISPATCHED_BY = GroupChatCommands + def __init__(self, parent_win, contact, acct, is_continued=False): ChatControlBase.__init__(self, self.TYPE_ID, parent_win, 'muc_child_vbox', contact, acct) @@ -1505,11 +1512,14 @@ class GroupchatControl(ChatControlBase): if model.iter_n_children(parent_iter) == 0: model.remove(parent_iter) - def send_message(self, message, xhtml=None): + def send_message(self, message, xhtml=None, process_commands=True): '''call this function to send our message''' if not message: return + if process_commands and self.process_as_command(message): + return + message = helpers.remove_invalid_xml_chars(message) if not message: From 981572f79d95a66d3f99120381d96d34f2aabcae Mon Sep 17 00:00:00 2001 From: red-agent Date: Sat, 12 Sep 2009 23:22:50 +0300 Subject: [PATCH 12/39] Improved error handling in the command system --- src/commands/framework.py | 51 ++++++++++++++++++++-------------- src/commands/implementation.py | 2 +- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/commands/framework.py b/src/commands/framework.py index ff655a56d..c5a2fe84c 100644 --- a/src/commands/framework.py +++ b/src/commands/framework.py @@ -25,6 +25,20 @@ from inspect import getargspec class CommandInternalError(Exception): pass +class CommandError(Exception): + def __init__(self, message=None, command=None, name=None): + if command: + self.command = command + self.name = command.first_name + elif name: + self.command = None + self.name = name + + if message: + super(CommandError, self).__init__(message) + else: + super(CommandError, self).__init__() + class Command(object): DOC_STRIP_PATTERN = re.compile(r'(?:^[ \t]+|\A\n)', re.MULTILINE) @@ -43,7 +57,13 @@ class Command(object): self.empty = empty def __call__(self, *args, **kwargs): - return self.handler(*args, **kwargs) + try: + return self.handler(*args, **kwargs) + except CommandError, exception: + if not exception.command and not exception.name: + raise CommandError(exception.message, self) + except TypeError: + raise CommandError("Command received invalid arguments", self) def __repr__(self): return "" % ', '.join(self.names) @@ -56,7 +76,7 @@ class Command(object): @property def first_name(self): - return self.names[0] + return self.names[0] @property def native_name(self): @@ -162,14 +182,6 @@ class Command(object): return usage if not complete else self.ARG_USAGE_PATTERN % (names, usage) -class CommandError(Exception): - def __init__(self, command, *args, **kwargs): - if isinstance(command, Command): - self.command = command - self.name = command.first_name - self.name = command - super(Exception, self).__init__(*args, **kwargs) - class Dispatcher(type): table = {} hosted = {} @@ -180,7 +192,7 @@ class Dispatcher(type): if Dispatcher.is_suitable(cls, dct): Dispatcher.register_processor(cls) - + # Sanitize names even if processor is not suitable for registering, # because it might be inherited by an another processor. Dispatcher.sanitize_names(cls) @@ -226,7 +238,7 @@ class Dispatcher(type): if 'HOSTED_BY' in proc.__dict__: cls.register_adhocs(proc) - + commands = cls.traverse_commands(proc, inherited) cls.register_commands(proc, commands) @@ -362,7 +374,7 @@ class CommandProcessor(object): name = cls.prepare_name(name) command = Dispatcher.retrieve_command(cls, name) if not command: - raise CommandError(name, "Command does not exist") + raise CommandError("Command does not exist", name=name) return command @classmethod @@ -446,7 +458,7 @@ class CommandProcessor(object): if len(spec_args) == 1: if arguments or command.empty: return (arguments,), {} - raise CommandError(command, "Can not be used without arguments") + raise CommandError("Can not be used without arguments", command) raise CommandInternalError("Raw command must define no more then one argument") if '__optional__' in spec_args: @@ -520,13 +532,10 @@ class CommandProcessor(object): args, kwargs = self.parse_command_arguments(arguments) if arguments else ([], {}) args, kwargs = self.adapt_command_arguments(command, arguments, args, kwargs) - try: - if self.command_preprocessor(name, command, arguments, args, kwargs): - return - value = command(self, *args, **kwargs) - self.command_postprocessor(name, command, arguments, args, kwargs, value) - except TypeError: - raise CommandError(name, "Command received invalid arguments") + if self.command_preprocessor(name, command, arguments, args, kwargs): + return + value = command(self, *args, **kwargs) + self.command_postprocessor(name, command, arguments, args, kwargs, value) def command_preprocessor(self, name, command, arguments, args, kwargs): """ diff --git a/src/commands/implementation.py b/src/commands/implementation.py index a921116e7..a1a1ebf84 100644 --- a/src/commands/implementation.py +++ b/src/commands/implementation.py @@ -100,7 +100,7 @@ class ChatCommands(CommonCommands): Send a ping to the contact """ if self.account == gajim.ZEROCONF_ACC_NAME: - raise CommandError(ping, _('Command is not supported for zeroconf accounts')) + raise CommandError(_('Command is not supported for zeroconf accounts')) gajim.connections[self.account].sendPing(self.contact) class PrivateChatCommands(CommonCommands): From cae86299e496abbd2b09884d8eb8848fd3902428 Mon Sep 17 00:00:00 2001 From: red-agent Date: Sun, 13 Sep 2009 00:22:17 +0300 Subject: [PATCH 13/39] Rolled in a bunch of fixes for the command system --- src/chat_control.py | 2 -- src/commands/framework.py | 11 +++++------ src/commands/middleware.py | 1 - 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index dc259dfbe..66e13114b 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -82,8 +82,6 @@ class ChatControlBase(MessageControl, CommonCommands): '''A base class containing a banner, ConversationTextview, MessageTextView ''' - DISPATCHED_BY = CommonCommands - def make_href(self, match): url_color = gajim.config.get('urlmsgcolor') return '%s' % (match.group(), diff --git a/src/commands/framework.py b/src/commands/framework.py index c5a2fe84c..86e890db9 100644 --- a/src/commands/framework.py +++ b/src/commands/framework.py @@ -27,12 +27,11 @@ class CommandInternalError(Exception): class CommandError(Exception): def __init__(self, message=None, command=None, name=None): + self.command = command + self.name = name + if command: - self.command = command self.name = command.first_name - elif name: - self.command = None - self.name = name if message: super(CommandError, self).__init__(message) @@ -343,7 +342,7 @@ class CommandProcessor(object): # Quite complex piece of regular expression logic. ARG_PATTERN = re.compile(r'(\'|")?(?P(?(1).+?|\S+))(?(1)\1)') - OPT_PATTERN = re.compile(r'--?(?P[\w-]+)(?:(?:=|\s)(\'|")?(?P(?(2)[^-]+?|[^-\s]+))(?(2)\2))?') + OPT_PATTERN = re.compile(r'(?[\w-]+)(?:(?:=|\s)(\'|")?(?P(?(2)[^-]+?|[^-\s]+))(?(2)\2))?') EXPAND_SHORT_OPTIONS = True @@ -509,7 +508,7 @@ class CommandProcessor(object): return False text = text[len(self.COMMAND_PREFIX):] - text = text.lstrip() + text = text.strip() parts = text.split(' ', 1) diff --git a/src/commands/middleware.py b/src/commands/middleware.py index a0db07bdd..839c82c3e 100644 --- a/src/commands/middleware.py +++ b/src/commands/middleware.py @@ -19,7 +19,6 @@ would be dropped in. Defines a little bit of scaffolding to support interaction between the two and a few utility methods so you don't need to dig up the host code to write basic commands. """ - from types import StringTypes from framework import CommandProcessor, CommandError from traceback import print_exc From 949dbe6e38855e37328baada39e9d71cb2b7f024 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Sun, 13 Sep 2009 11:21:23 +0200 Subject: [PATCH 14/39] better focus behavior when adding/removing a new proxy profile. Fixes #4017 --- src/config.py | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/src/config.py b/src/config.py index 8d36742e2..7aea772a7 100644 --- a/src/config.py +++ b/src/config.py @@ -1077,6 +1077,7 @@ class ManageProxiesWindow: self.proxytype_combobox = self.xml.get_widget('proxytype_combobox') self.init_list() + self.block_signal = False self.xml.signal_autoconnect(self) self.window.show_all() # hide the BOSH fields by default @@ -1134,6 +1135,7 @@ class ManageProxiesWindow: iter_ = model.append() model.set(iter_, 0, 'proxy' + unicode(i)) gajim.config.add_per('proxies', 'proxy' + unicode(i)) + self.proxies_treeview.set_cursor(model.get_path(iter_)) def on_remove_proxy_button_clicked(self, widget): (model, iter_) = self.proxies_treeview.get_selection().get_selected() @@ -1143,11 +1145,16 @@ class ManageProxiesWindow: model.remove(iter_) gajim.config.del_per('proxies', proxy) self.xml.get_widget('remove_proxy_button').set_sensitive(False) + self.block_signal = True + self.on_proxies_treeview_cursor_changed(self.proxies_treeview) + self.block_signal = False def on_close_button_clicked(self, widget): self.window.destroy() def on_useauth_checkbutton_toggled(self, widget): + if self.block_signal: + return act = widget.get_active() proxy = self.proxyname_entry.get_text().decode('utf-8') gajim.config.set_per('proxies', proxy, 'useauth', act) @@ -1155,6 +1162,8 @@ class ManageProxiesWindow: self.xml.get_widget('proxypass_entry').set_sensitive(act) def on_boshuseproxy_checkbutton_toggled(self, widget): + if self.block_signal: + return act = widget.get_active() proxy = self.proxyname_entry.get_text().decode('utf-8') gajim.config.set_per('proxies', proxy, 'bosh_useproxy', act) @@ -1164,11 +1173,6 @@ class ManageProxiesWindow: def on_proxies_treeview_cursor_changed(self, widget): #FIXME: check if off proxy settings are correct (see # http://trac.gajim.org/changeset/1921#file2 line 1221 - (model, iter_) = widget.get_selection().get_selected() - if not iter_: - return - proxy = model[iter_][0] - self.xml.get_widget('proxyname_entry').set_text(proxy) proxyhost_entry = self.xml.get_widget('proxyhost_entry') proxyport_entry = self.xml.get_widget('proxyport_entry') proxyuser_entry = self.xml.get_widget('proxyuser_entry') @@ -1176,6 +1180,7 @@ class ManageProxiesWindow: boshuri_entry = self.xml.get_widget('boshuri_entry') useauth_checkbutton = self.xml.get_widget('useauth_checkbutton') boshuseproxy_checkbutton = self.xml.get_widget('boshuseproxy_checkbutton') + self.block_signal = True proxyhost_entry.set_text('') proxyport_entry.set_text('') proxyuser_entry.set_text('') @@ -1188,6 +1193,17 @@ class ManageProxiesWindow: #useauth_checkbutton.set_active(False) #self.on_useauth_checkbutton_toggled(useauth_checkbutton) + (model, iter_) = widget.get_selection().get_selected() + if not iter_: + self.xml.get_widget('proxyname_entry').set_text('') + self.xml.get_widget('proxytype_combobox').set_sensitive(False) + self.xml.get_widget('proxy_table').set_sensitive(False) + self.block_signal = False + return + + proxy = model[iter_][0] + self.xml.get_widget('proxyname_entry').set_text(proxy) + if proxy == _('None'): # special proxy None self.show_bosh_fields(False) self.proxyname_entry.set_editable(False) @@ -1219,12 +1235,15 @@ class ManageProxiesWindow: gajim.config.get_per('proxies', proxy, 'bosh_useproxy')) useauth_checkbutton.set_active( gajim.config.get_per('proxies', proxy, 'useauth')) + self.block_signal = False def on_proxies_treeview_key_press_event(self, widget, event): if event.keyval == gtk.keysyms.Delete: self.on_remove_proxy_button_clicked(widget) def on_proxyname_entry_changed(self, widget): + if self.block_signal: + return (model, iter_) = self.proxies_treeview.get_selection().get_selected() if not iter_: return @@ -1243,6 +1262,8 @@ class ManageProxiesWindow: model.set_value(iter_, 0, new_name) def on_proxytype_combobox_changed(self, widget): + if self.block_signal: + return types = ['http', 'socks5', 'bosh'] type_ = self.proxytype_combobox.get_active() self.show_bosh_fields(types[type_]=='bosh') @@ -1250,26 +1271,36 @@ class ManageProxiesWindow: gajim.config.set_per('proxies', proxy, 'type', types[type_]) def on_proxyhost_entry_changed(self, widget): + if self.block_signal: + return value = widget.get_text().decode('utf-8') proxy = self.proxyname_entry.get_text().decode('utf-8') gajim.config.set_per('proxies', proxy, 'host', value) def on_proxyport_entry_changed(self, widget): + if self.block_signal: + return value = widget.get_text().decode('utf-8') proxy = self.proxyname_entry.get_text().decode('utf-8') gajim.config.set_per('proxies', proxy, 'port', value) def on_proxyuser_entry_changed(self, widget): + if self.block_signal: + return value = widget.get_text().decode('utf-8') proxy = self.proxyname_entry.get_text().decode('utf-8') gajim.config.set_per('proxies', proxy, 'user', value) def on_boshuri_entry_changed(self, widget): + if self.block_signal: + return value = widget.get_text().decode('utf-8') proxy = self.proxyname_entry.get_text().decode('utf-8') gajim.config.set_per('proxies', proxy, 'bosh_uri', value) def on_proxypass_entry_changed(self, widget): + if self.block_signal: + return value = widget.get_text().decode('utf-8') proxy = self.proxyname_entry.get_text().decode('utf-8') gajim.config.set_per('proxies', proxy, 'pass', value) From 880952b5c3cc04570805c9ba9768884015101587 Mon Sep 17 00:00:00 2001 From: red-agent Date: Sun, 13 Sep 2009 16:40:33 +0300 Subject: [PATCH 15/39] Few more fixes for the command system --- src/commands/framework.py | 3 ++- src/commands/implementation.py | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/commands/framework.py b/src/commands/framework.py index 86e890db9..30e69ad35 100644 --- a/src/commands/framework.py +++ b/src/commands/framework.py @@ -466,7 +466,8 @@ class CommandProcessor(object): optional = args[hard_len:] args = args[:hard_len] args.insert(spec_args.index('__optional__'), optional) - raise CommandInternalError("Cant have both, __optional__ and *args") + else: + raise CommandInternalError("Cant have both, __optional__ and *args") if command.dashes: for key, value in kwargs.items(): diff --git a/src/commands/implementation.py b/src/commands/implementation.py index a1a1ebf84..c2acb956b 100644 --- a/src/commands/implementation.py +++ b/src/commands/implementation.py @@ -29,8 +29,6 @@ class CommonCommands(ChatMiddleware): PrivateChatControl or GroupchatControl when command is being called. """ - IS_COMMAND_PROCESSOR = True - @command def clear(self): """ From 6a20a33b70faaa1c5c64a5b507bad0841bc60e1b Mon Sep 17 00:00:00 2001 From: red-agent Date: Sun, 13 Sep 2009 17:43:44 +0300 Subject: [PATCH 16/39] Dropped in an implementation of standard commands --- src/commands/__init__.py | 20 +++++ src/commands/implementation.py | 142 +++++++++++++++++++++++++++++++++ src/commands/middleware.py | 18 +++++ 3 files changed, 180 insertions(+) diff --git a/src/commands/__init__.py b/src/commands/__init__.py index e69de29bb..48f99c416 100644 --- a/src/commands/__init__.py +++ b/src/commands/__init__.py @@ -0,0 +1,20 @@ +# Copyright (C) 2009 red-agent +# +# 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, either version 3 of the License, or +# (at your option) any later version. +# +# 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. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +The command system providing scalable and convenient architecture in combination +with declarative way of defining commands and a fair amount of automatization +for routine processes. +""" diff --git a/src/commands/implementation.py b/src/commands/implementation.py index c2acb956b..dedbcb016 100644 --- a/src/commands/implementation.py +++ b/src/commands/implementation.py @@ -17,7 +17,10 @@ Provides an actual implementation of the standard commands. """ +import dialogs from common import gajim +from common import helpers +from common.exceptions import GajimGeneralException from framework import command, CommandError from middleware import ChatMiddleware @@ -120,3 +123,142 @@ class GroupChatCommands(CommonCommands): IS_COMMAND_PROCESSOR = True INHERITED = True + + @command(raw=True) + def nick(self, new_nick): + """ + Change your nickname in a group chat + """ + try: + new_nick = helpers.parse_resource(new_nick) + except Exception: + raise CommandError(_("Invalid nickname")) + self.connection.join_gc(new_nick, self.room_jid, None, change_nick=True) + + @command('query', raw=True) + def chat(self, nick): + """ + Open a private chat window with a specified occupant + """ + nicks = gajim.contacts.get_nick_list(self.account, self.room_jid) + if nick in nicks: + self.on_send_pm(nick=nick) + else: + raise CommandError(_("Nickname not found")) + + @command('msg') + def message(self, nick, *a_message): + """ + Open a private chat window with a specified occupant and send him a + message + """ + a_message = self.collect(a_message, False) + nicks = gajim.contacts.get_nick_list(self.account, self.room_jid) + if nick in nicks: + self.on_send_pm(nick=nick, msg=a_message) + else: + raise CommandError(_("Nickname not found")) + + @command(raw=True, empty=True) + def topic(self, new_topic): + """ + Display or change a group chat topic + """ + if new_topic: + self.connection.send_gc_subject(self.room_jid, new_topic) + else: + return self.subject + + @command + def invite(self, jid, *reason): + """ + Invite a user to a room for a reason + """ + reason = self.collect(reason) + self.connection.send_invite(self.room_jid, jid, reason) + return _("Invited %s to %s") % (jid, self.room_jid) + + @command + def join(self, jid, *nick): + """ + Join a group chat given by a jid, optionally using given nickname + """ + nick = self.collect(nick) or self.nick + + if '@' not in jid: + jid = jid + '@' + gajim.get_server_from_jid(self.room_jid) + + try: + gajim.interface.instances[self.account]['join_gc'].window.present() + except KeyError: + try: + dialogs.JoinGroupchatWindow(account=None, room_jid=jid, nick=nick) + except GajimGeneralException: + pass + + @command('part', 'close', raw=True, empty=True) + def leave(self, reason): + """ + Leave the groupchat, optionally giving a reason, and close tab or window + """ + self.parent_win.remove_tab(self, self.parent_win.CLOSE_COMMAND, reason) + + @command + def ban(self, who, *reason): + """ + Ban user by a nick or a jid from a groupchat + + If given nickname is not found it will be treated as a jid. + """ + reason = self.collect(reason, none=False) + if who in gajim.contacts.get_nick_list(self.account, self.room_jid): + contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, who) + who = contact.jid + self.connection.gc_set_affiliation(self.room_jid, who, 'outcast', reason) + + @command + def kick(self, who, *reason): + """ + Kick user by a nick from a groupchat + """ + reason = self.collect(reason, none=False) + if not who in gajim.contacts.get_nick_list(self.account, self.room_jid): + raise CommandError(_("Nickname not found")) + self.connection.gc_set_role(self.room_jid, who, 'none', reason) + + @command + def names(self, verbose=False): + """ + Display names of all group chat occupants + """ + get_contact = lambda nick: gajim.contacts.get_gc_contact(self.account, self.room_jid, nick) + nicks = gajim.contacts.get_nick_list(self.account, self.room_jid) + + # First we do alpha-numeric sort and then role-based one. + nicks.sort() + nicks.sort(key=lambda nick: get_contact(nick).role) + + if verbose: + for nick in nicks: + contact = get_contact(nick) + + role = helpers.get_uf_role(contact.role) + affiliation = helpers.get_uf_affiliation(contact.affiliation) + + self.echo("%s - %s - %s" % (nick, role, affiliation)) + else: + return ', '.join(nicks) + + @command(raw=True) + def block(self, who): + """ + Forbid an occupant to send you public or private messages + """ + self.on_block(None, who) + + @command(raw=True) + def unblock(self, who): + """ + Allow an occupant to send you public or privates messages + """ + self.on_unblock(None, who) diff --git a/src/commands/middleware.py b/src/commands/middleware.py index 839c82c3e..1d024c5e4 100644 --- a/src/commands/middleware.py +++ b/src/commands/middleware.py @@ -19,6 +19,8 @@ would be dropped in. Defines a little bit of scaffolding to support interaction between the two and a few utility methods so you don't need to dig up the host code to write basic commands. """ +from common import gajim + from types import StringTypes from framework import CommandProcessor, CommandError from traceback import print_exc @@ -94,3 +96,19 @@ class ChatMiddleware(CommandProcessor): using ctrl + up/down arrow keys. """ self.save_sent_message(text) + + def collect(self, arguments, empty=True, separator=' ', none=True): + """ + Might come in handy in case if you want to map some arguments and + collect the rest of them into a string. + """ + if not empty and not arguments: + raise CommandError(_("Missing argument")) + return None if not arguments and none else separator.join(arguments) + + @property + def connection(self): + """ + Get the current connection object. + """ + return gajim.connections[self.account] From 620d102ac5602ecc8409a6d0891c9c0d97f257d5 Mon Sep 17 00:00:00 2001 From: red-agent Date: Sun, 13 Sep 2009 21:19:10 +0300 Subject: [PATCH 17/39] Fixed a typo --- src/commands/framework.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/framework.py b/src/commands/framework.py index 30e69ad35..400e59677 100644 --- a/src/commands/framework.py +++ b/src/commands/framework.py @@ -347,7 +347,7 @@ class CommandProcessor(object): EXPAND_SHORT_OPTIONS = True COMMAND_PREFIX = '/' - CASE_SENVITIVE_COMMANDS = False + CASE_SENSITIVE_COMMANDS = False ARG_ENCODING = 'utf8' @@ -366,7 +366,7 @@ class CommandProcessor(object): @classmethod def prepare_name(cls, name): - return name if cls.CASE_SENVITIVE_COMMANDS else name.lower() + return name if cls.CASE_SENSITIVE_COMMANDS else name.lower() @classmethod def retrieve_command(cls, name): From 0a1ef72c0d463c9aadfaa7716be30cd11f810626 Mon Sep 17 00:00:00 2001 From: red-agent Date: Sun, 13 Sep 2009 22:11:40 +0300 Subject: [PATCH 18/39] Removed unused code from the Dispatcher --- src/commands/framework.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/commands/framework.py b/src/commands/framework.py index 400e59677..f481b7f36 100644 --- a/src/commands/framework.py +++ b/src/commands/framework.py @@ -186,8 +186,8 @@ class Dispatcher(type): hosted = {} def __init__(cls, name, bases, dct): - dispatchable = Dispatcher.check_if_dispatchable(bases, dct) - hostable = Dispatcher.check_if_hostable(bases, dct) + Dispatcher.check_if_dispatchable(bases, dct) + Dispatcher.check_if_hostable(bases, dct) if Dispatcher.is_suitable(cls, dct): Dispatcher.register_processor(cls) @@ -208,22 +208,20 @@ class Dispatcher(type): def check_if_dispatchable(cls, bases, dct): dispatcher = dct.get('DISPATCHED_BY') if not dispatcher: - return False + return if dispatcher not in bases: raise CommandInternalError("Should be dispatched by the same processor it inherits from") - return True @classmethod def check_if_hostable(cls, bases, dct): hosters = dct.get('HOSTED_BY') if not hosters: - return False + return if not isinstance(hosters, (TupleType, ListType)): hosters = (hosters,) for hoster in hosters: if hoster not in bases: raise CommandInternalError("Should be hosted by the same processors it inherits from") - return True @classmethod def check_if_conformed(cls, dispatchable, hostable): From 7bec311cfb2105b9ba01c7a964912ad5d66518ae Mon Sep 17 00:00:00 2001 From: red-agent Date: Mon, 14 Sep 2009 22:04:49 +0300 Subject: [PATCH 19/39] Made some cosmetic API changes to the command system --- src/commands/custom.py | 8 ++++---- src/commands/framework.py | 25 ++++++++++++------------- src/commands/implementation.py | 12 ++++++------ 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/commands/custom.py b/src/commands/custom.py index 84827c5b0..44a8ab073 100644 --- a/src/commands/custom.py +++ b/src/commands/custom.py @@ -29,7 +29,7 @@ class CustomCommonCommands(ChatCommands, PrivateChatCommands, GroupChatCommands) here will be available to all of them. """ - IS_COMMAND_PROCESSOR = True + DISPATCH = True HOSTED_BY = ChatCommands, PrivateChatCommands, GroupChatCommands @command @@ -52,7 +52,7 @@ class CustomChatCommands(ChatCommands): only to a chat. """ - IS_COMMAND_PROCESSOR = True + DISPATCH = True HOSTED_BY = ChatCommands @command @@ -66,7 +66,7 @@ class CustomPrivateChatCommands(PrivateChatCommands): available only to a private chat. """ - IS_COMMAND_PROCESSOR = True + DISPATCH = True HOSTED_BY = PrivateChatCommands @command @@ -80,7 +80,7 @@ class CustomGroupChatCommands(GroupChatCommands): available only to a group chat. """ - IS_COMMAND_PROCESSOR = True + DISPATCH = True HOSTED_BY = GroupChatCommands @command diff --git a/src/commands/framework.py b/src/commands/framework.py index f481b7f36..b10c96550 100644 --- a/src/commands/framework.py +++ b/src/commands/framework.py @@ -201,8 +201,8 @@ class Dispatcher(type): @classmethod def is_suitable(cls, proc, dct): is_not_root = dct.get('__metaclass__') is not cls - is_processor = bool(dct.get('IS_COMMAND_PROCESSOR')) - return is_not_root and is_processor + to_be_dispatched = bool(dct.get('DISPATCH')) + return is_not_root and to_be_dispatched @classmethod def check_if_dispatchable(cls, bases, dct): @@ -231,18 +231,18 @@ class Dispatcher(type): @classmethod def register_processor(cls, proc): cls.table[proc] = {} - inherited = proc.__dict__.get('INHERITED') + inherit = proc.__dict__.get('INHERIT') if 'HOSTED_BY' in proc.__dict__: cls.register_adhocs(proc) - commands = cls.traverse_commands(proc, inherited) + commands = cls.traverse_commands(proc, inherit) cls.register_commands(proc, commands) @classmethod def sanitize_names(cls, proc): - inherited = proc.__dict__.get('INHERITED') - commands = cls.traverse_commands(proc, inherited) + inherit = proc.__dict__.get('INHERIT') + commands = cls.traverse_commands(proc, inherit) for key, command in commands: if not proc.SAFE_NAME_SCAN_PATTERN.match(key): setattr(proc, proc.SAFE_NAME_SUBS_PATTERN % key, command) @@ -252,8 +252,8 @@ class Dispatcher(type): pass @classmethod - def traverse_commands(cls, proc, inherited=True): - keys = dir(proc) if inherited else proc.__dict__.iterkeys() + def traverse_commands(cls, proc, inherit=True): + keys = dir(proc) if inherit else proc.__dict__.iterkeys() for key in keys: value = getattr(proc, key) if isinstance(value, Command): @@ -295,8 +295,8 @@ class Dispatcher(type): commands = dict(cls.traverse_commands(proc.DISPATCHED_BY)) if proc.DISPATCHED_BY in cls.hosted: for adhoc in cls.hosted[proc.DISPATCHED_BY]: - inherited = adhoc.__dict__.get('INHERITED') - commands.update(dict(cls.traverse_commands(adhoc, inherited))) + inherit = adhoc.__dict__.get('INHERIT') + commands.update(dict(cls.traverse_commands(adhoc, inherit))) return commands.values() class CommandProcessor(object): @@ -311,8 +311,7 @@ class CommandProcessor(object): to an object you are adding commands to. Your subclass, which will contain commands should define in its body - IS_COMMAND_PROCESSOR = True in order to be included in the dispatching - table. + DISPATCH = True in order to be included in the dispatching table. Every class you will drop the processor in should define DISPATCHED_BY set to the same processor you are inheriting from. @@ -326,7 +325,7 @@ class CommandProcessor(object): whatever includes the host) you need to inherit you processor from the host and set HOSTED_BY to that host. - INHERITED controls whether commands inherited from base classes (which could + INHERIT controls whether commands inherited from base classes (which could include other processors) will be registered or not. This is disabled by-default because it leads to unpredictable consequences when used in adhoc processors which inherit from more then one processor or has such processors diff --git a/src/commands/implementation.py b/src/commands/implementation.py index dedbcb016..c8f7a38c4 100644 --- a/src/commands/implementation.py +++ b/src/commands/implementation.py @@ -92,8 +92,8 @@ class ChatCommands(CommonCommands): an instance of ChatControl when command is being called. """ - IS_COMMAND_PROCESSOR = True - INHERITED = True + DISPATCH = True + INHERIT = True @command def ping(self): @@ -111,8 +111,8 @@ class PrivateChatCommands(CommonCommands): self is set to an instance of PrivateChatControl when command is being called. """ - IS_COMMAND_PROCESSOR = True - INHERITED = True + DISPATCH = True + INHERIT = True class GroupChatCommands(CommonCommands): """ @@ -121,8 +121,8 @@ class GroupChatCommands(CommonCommands): self is set to an instance of GroupchatControl when command is being called. """ - IS_COMMAND_PROCESSOR = True - INHERITED = True + DISPATCH = True + INHERIT = True @command(raw=True) def nick(self, new_nick): From a264608b2920fdfbef602c63e0a2f98b0bb9b191 Mon Sep 17 00:00:00 2001 From: red-agent Date: Tue, 15 Sep 2009 01:20:49 +0300 Subject: [PATCH 20/39] Slighlty changed command arguments parser/adapter internal logic Made parser to preserve the order of options, so I can use it later to provide some enhancments for the adapter. --- src/commands/framework.py | 62 +++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/commands/framework.py b/src/commands/framework.py index b10c96550..ab3133cc2 100644 --- a/src/commands/framework.py +++ b/src/commands/framework.py @@ -382,8 +382,9 @@ class CommandProcessor(object): def parse_command_arguments(cls, arguments): """ Simple yet effective and sufficient in most cases parser which parses - command arguments and maps them to *args and **kwargs, which we all use - extensivly in daily Python coding. + command arguments and returns them as two lists, first representing + positional arguments, and second representing options as (key, value) + tuples. The format of the input arguments should be: <> [-(-o)ption=value1, -(-a)nother=value2] [[extra_options]] @@ -394,37 +395,38 @@ class CommandProcessor(object): 'one two three' or "one two three"; that is between single or double quotes. """ - args, kwargs = [], {} + args, opts = [], [] - # Need to store every option we have parsed in order to get arguments - # to be parsed correct later. - options = [] + # Need to store position of every option we have parsed in order to get + # arguments to be parsed correct later. + opt_positions = [] def intersects((given_start, given_end)): """ Check if something intersects with boundaries of any parsed options. """ - for start, end in options: + for start, end in opt_positions: if given_start == start or given_end == end: return True return False for match in re.finditer(cls.OPT_PATTERN, arguments): if match: - options.append(match.span()) - kwargs[match.group('key')] = match.group('value') or True + opt_positions.append(match.span()) + opts.append((match.group('key'), match.group('value') or True)) for match in re.finditer(cls.ARG_PATTERN, arguments): if match and not intersects(match.span()): args.append(match.group('body')) - return args, kwargs + return args, opts @classmethod - def adapt_command_arguments(cls, command, arguments, args, kwargs): + def adapt_command_arguments(cls, command, arguments, args, opts): """ - Adapts *args and **kwargs got from a parser to a specific handler by - means of arguments specified on command definition. + Adapts args and opts got from the parser to a specific handler by means + of arguments specified on command definition. That is transforms them to + *args and **kwargs suitable for passing to a command handler. When EXPAND_SHORT_OPTIONS is set then if command receives one-latter options (like -v or -f) they will be expanded to a verbose ones (like @@ -467,35 +469,33 @@ class CommandProcessor(object): raise CommandInternalError("Cant have both, __optional__ and *args") if command.dashes: - for key, value in kwargs.items(): + for index, (key, value) in enumerate(opts): if '-' in key: - del kwargs[key] - kwargs[key.replace('-', '_')] = value + opts[index] = (key.replace('-', '_'), value) if cls.EXPAND_SHORT_OPTIONS: expanded = [] - for key, value in spec_kwargs.iteritems(): - letter = key[0] if len(key) > 1 else None - if letter and letter in kwargs and letter not in expanded: - expanded.append(letter) - kwargs[key] = kwargs[letter] - del kwargs[letter] + for spec_key, spec_value in spec_kwargs.iteritems(): + letter = spec_key[0] if len(spec_key) > 1 else None + if letter and letter not in expanded: + for index, (key, value) in enumerate(opts): + if key == letter: + expanded.append(letter) + opts[index] = (spec_key, value) + break # We need to encode every keyword argument to a simple string, not the - # unicode one, because ** expanding does not support it. The nasty issue - # here to consider is that if dict key was initially set as u'test', - # then resetting it to just 'test' leaves u'test' as it was... - for key, value in kwargs.items(): + # unicode one, because ** expanding does not support it. + for index, (key, value) in enumerate(opts): if isinstance(key, UnicodeType): - del kwargs[key] - kwargs[key.encode(cls.ARG_ENCODING)] = value + opts[index] = (key.encode(cls.ARG_ENCODING), value) if '__arguments__' in spec_args: if len(spec_args) == 1 and not spec_kwargs and not var_args and not var_kwargs: return (arguments,), {} args.insert(spec_args.index('__arguments__'), arguments) - return args, kwargs + return tuple(args), dict(opts) def process_as_command(self, text): """ @@ -526,8 +526,8 @@ class CommandProcessor(object): def execute_command(self, name, arguments): command = self.retrieve_command(name) - args, kwargs = self.parse_command_arguments(arguments) if arguments else ([], {}) - args, kwargs = self.adapt_command_arguments(command, arguments, args, kwargs) + args, opts = self.parse_command_arguments(arguments) if arguments else ([], []) + args, kwargs = self.adapt_command_arguments(command, arguments, args, opts) if self.command_preprocessor(name, command, arguments, args, kwargs): return From c194b92136991a4b34e2976a69ab1c584d7ec682 Mon Sep 17 00:00:00 2001 From: red-agent Date: Tue, 15 Sep 2009 03:33:02 +0300 Subject: [PATCH 21/39] Made command options refactoring along with some fixes --- src/commands/framework.py | 115 +++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 59 deletions(-) diff --git a/src/commands/framework.py b/src/commands/framework.py index ab3133cc2..3e883a8bd 100644 --- a/src/commands/framework.py +++ b/src/commands/framework.py @@ -45,15 +45,14 @@ class Command(object): ARG_USAGE_PATTERN = 'Usage: %s %s' - def __init__(self, handler, is_instance, usage, raw, dashes, optional, empty): + def __init__(self, handler, usage, raw, optional, empty, expand_short): self.handler = handler - self.is_instance = is_instance self.usage = usage self.raw = raw - self.dashes = dashes self.optional = optional self.empty = empty + self.expand_short = expand_short def __call__(self, *args, **kwargs): try: @@ -110,12 +109,12 @@ class Command(object): # Behavior of this code need to be checked. Might yield incorrect # results on some rare occasions. spec_args = names[:-len(defaults) if defaults else len(names)] - spec_kwargs = dict(zip(names[-len(defaults):], defaults)) if defaults else {} + spec_kwargs = list(zip(names[-len(defaults):], defaults)) if defaults else {} - # Removing self from arguments specification in case if command handler - # is an instance method. - if self.is_instance and spec_args.pop(0) != 'self': - raise CommandInternalError("Invalid arguments specification") + # Removing self from arguments specification. Command handler should + # normally be an instance method. + if spec_args.pop(0) != 'self': + raise CommandInternalError("First argument must be self") return spec_args, spec_kwargs, var_args, var_kwargs @@ -125,27 +124,28 @@ class Command(object): human-readable format. If complete is given - then ARG_USAGE_PATTERN will be used to render it completly. """ - names, _var_args, _var_kwargs, defaults = getargspec(self.handler) spec_args, spec_kwargs, var_args, var_kwargs = self.extract_arg_spec() - '__arguments__' not in spec_args or spec_args.remove('__arguments__') + # If command defines special __arguments__ parameter - it should not be + # included in the usage information, but may be used for internal + # purposes while generating usage information. + sp_arguments = '__arguments__' in spec_args + if sp_arguments: + spec_args.remove('__arguments__') - optional = '__optional__' in spec_args - if optional: + # If command defines special __optional__ parameter - it should not be + # included in the usage information, but may be used for internal + # purposes while generating usage information. + sp_optional = '__optional__' in spec_args + if sp_optional: spec_args.remove('__optional__') kwargs = [] letters = [] - # The reason we don't iterate here through spec_kwargs, like we would - # normally do is that it does not retains order of items. We need to be - # sure that arguments will be printed in the order they were specified. - for key in (names[-len(defaults):] if defaults else ()): - value = spec_kwargs[key] + for key, value in spec_kwargs: letter = key[0] - - if self.dashes: - key = key.replace('_', '-') + key = key.replace('_', '-') if letter not in letters: kwargs.append('-(-%s)%s=%s' % (letter, key[1:], value)) @@ -158,10 +158,10 @@ class Command(object): if len(spec_args) == 1 and self.raw: args += ('(|%s|)' if self.empty else '|%s|') % spec_args[0] - elif spec_args or var_args or optional: + elif spec_args or var_args or sp_optional: if spec_args: args += '<%s>' % ', '.join(spec_args) - if var_args or optional: + if var_args or sp_optional: args += (' ' if spec_args else str()) + '<<%s>>' % (var_args or self.optional) usage += args @@ -341,8 +341,6 @@ class CommandProcessor(object): ARG_PATTERN = re.compile(r'(\'|")?(?P(?(1).+?|\S+))(?(1)\1)') OPT_PATTERN = re.compile(r'(?[\w-]+)(?:(?:=|\s)(\'|")?(?P(?(2)[^-]+?|[^-\s]+))(?(2)\2))?') - EXPAND_SHORT_OPTIONS = True - COMMAND_PREFIX = '/' CASE_SENSITIVE_COMMANDS = False @@ -428,13 +426,6 @@ class CommandProcessor(object): of arguments specified on command definition. That is transforms them to *args and **kwargs suitable for passing to a command handler. - When EXPAND_SHORT_OPTIONS is set then if command receives one-latter - options (like -v or -f) they will be expanded to a verbose ones (like - --verbose or --file) if the latter are defined as a command optional - argumens. Expansion is made on a first-latter comparison basis. If more - then one long option with the same first letter defined - only first one - will be used in expanding. - If command defines __arguments__ as a first argument - then this argument will receive raw and unprocessed arguments. Also, if nothing except __arguments__ (including *args, *kwargs splatting) is defined - @@ -449,8 +440,16 @@ class CommandProcessor(object): Extra arguments which are not considered extra (or optional) - will be passed as if they were value for keywords, in the order keywords are defined and printed in usage. + + Dashes (-) in the option names will be converted to underscores. So you + can map --one-more-option to a one_more_option=None. """ spec_args, spec_kwargs, var_args, var_kwargs = command.extract_arg_spec() + spec_kwargs = dict(spec_kwargs) + + # Check if some special arguments are present. + sp_arguments = '__arguments__' in spec_args + sp_optional = '__optional__' in spec_args if command.raw: if len(spec_args) == 1: @@ -459,21 +458,20 @@ class CommandProcessor(object): raise CommandError("Can not be used without arguments", command) raise CommandInternalError("Raw command must define no more then one argument") - if '__optional__' in spec_args: + if sp_optional: if not var_args: - hard_len = len(spec_args) - 1 + hard_len = len(spec_args) - (1 if not sp_arguments else 2) optional = args[hard_len:] args = args[:hard_len] args.insert(spec_args.index('__optional__'), optional) else: - raise CommandInternalError("Cant have both, __optional__ and *args") + raise CommandInternalError("Can not have both, __optional__ and *args") - if command.dashes: - for index, (key, value) in enumerate(opts): - if '-' in key: - opts[index] = (key.replace('-', '_'), value) + for index, (key, value) in enumerate(opts): + if '-' in key: + opts[index] = (key.replace('-', '_'), value) - if cls.EXPAND_SHORT_OPTIONS: + if command.expand_short: expanded = [] for spec_key, spec_value in spec_kwargs.iteritems(): letter = spec_key[0] if len(spec_key) > 1 else None @@ -485,12 +483,12 @@ class CommandProcessor(object): break # We need to encode every keyword argument to a simple string, not the - # unicode one, because ** expanding does not support it. + # unicode one, because ** expansion does not support it. for index, (key, value) in enumerate(opts): if isinstance(key, UnicodeType): opts[index] = (key.encode(cls.ARG_ENCODING), value) - if '__arguments__' in spec_args: + if sp_arguments: if len(spec_args) == 1 and not spec_kwargs and not var_args and not var_kwargs: return (arguments,), {} args.insert(spec_args.index('__arguments__'), arguments) @@ -564,52 +562,51 @@ def command(*names, **kwargs): You can specify a set of names by which you can call the command. If names are empty - then the name of the command will be set to native (extracted - from the handler name). If no_native=True argument is given and names is - non-empty - then native name will not be added. - - If command handler is not an instance method then is_instance=False should - be given. Though mentioned case is not covered by defined behaviour, and - should not be used, unless you know what you are doing. + from the handler name). If include_native=True argument is given and names + is non-empty - then native name will be added as well. If usage=True is given - then handler's doc will be appended with an - auto-gereated usage info. + auto-generated usage info. - If raw=True is given then command should define only one argument to + If raw=True is given - then command should define only one argument to which all raw, unprocessed command arguments will be given. - If dashes=True is given, then dashes (-) in the option - names will be converted to underscores. So you can map --one-more-option to - a one_more_option=None. - If optional is set to a string then if __optional__ specified - its name ('optional' by-default) in the usage info will be substitued by whatever is given. If empty=True is given - then if raw is enabled it will allow to pass empty (None) raw arguments to a command. + + If expand_short=True is given - then if command receives one-letter + options (like -v or -f) they will be expanded to a verbose ones (like + --verbose or --file) if the latter are defined as a command optional + arguments. Expansion is made on a first-letter comparison basis. If more + then one long option with the same first letter defined - only first one + will be used in expansion. """ names = list(names) + include_native = kwargs.get('include_native', True) - no_native = kwargs.get('no_native', False) - is_instance = kwargs.get('is_instance', True) usage = kwargs.get('usage', True) raw = kwargs.get('raw', False) - dashes = kwargs.get('dashes', True) optional = kwargs.get('optional', 'optional') empty = kwargs.get('empty', False) + expand_short = kwargs.get('expand_short', True) def decorator(handler): - command = Command(handler, is_instance, usage, raw, dashes, optional, empty) + command = Command(handler, usage, raw, optional, empty, expand_short) # Extract and inject native name while making sure it is going to be the # first one in the list. - if not names or names and not no_native: + if not names or include_native: names.insert(0, command.native_name) command.names = tuple(names) return command - # Workaround if we are getting called without parameters. + # Workaround if we are getting called without parameters. Keep in mind that + # in that case - first item in the names will be the handler. if len(names) == 1 and isinstance(names[0], FunctionType): return decorator(names.pop()) From 8a72870a6d9886991fbf1c49c49a34c8304c16d1 Mon Sep 17 00:00:00 2001 From: red-agent Date: Tue, 15 Sep 2009 17:46:02 +0300 Subject: [PATCH 22/39] More code refactoring --- src/commands/framework.py | 104 ++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 61 deletions(-) diff --git a/src/commands/framework.py b/src/commands/framework.py index 3e883a8bd..866d8ff23 100644 --- a/src/commands/framework.py +++ b/src/commands/framework.py @@ -45,12 +45,13 @@ class Command(object): ARG_USAGE_PATTERN = 'Usage: %s %s' - def __init__(self, handler, usage, raw, optional, empty, expand_short): + def __init__(self, handler, usage, source, raw, extra, empty, expand_short): self.handler = handler self.usage = usage + self.source = source self.raw = raw - self.optional = optional + self.extra = extra self.empty = empty self.expand_short = expand_short @@ -100,8 +101,7 @@ class Command(object): keep them simple yet meaningful. """ doc = self.extract_doc() - if doc: - return doc.split('\n', 1)[0] + return doc.split('\n', 1)[0] if doc else None def extract_arg_spec(self): names, var_args, var_kwargs, defaults = getargspec(self.handler) @@ -126,19 +126,10 @@ class Command(object): """ spec_args, spec_kwargs, var_args, var_kwargs = self.extract_arg_spec() - # If command defines special __arguments__ parameter - it should not be - # included in the usage information, but may be used for internal - # purposes while generating usage information. - sp_arguments = '__arguments__' in spec_args - if sp_arguments: - spec_args.remove('__arguments__') - - # If command defines special __optional__ parameter - it should not be - # included in the usage information, but may be used for internal - # purposes while generating usage information. - sp_optional = '__optional__' in spec_args - if sp_optional: - spec_args.remove('__optional__') + # Remove some special positional arguments from the specifiaction, but + # store their names so they can be used for usage info generation. + sp_source = spec_args.pop(0) if self.source else None + sp_extra = spec_args.pop() if self.extra else None kwargs = [] letters = [] @@ -158,11 +149,11 @@ class Command(object): if len(spec_args) == 1 and self.raw: args += ('(|%s|)' if self.empty else '|%s|') % spec_args[0] - elif spec_args or var_args or sp_optional: + elif spec_args or var_args or sp_extra: if spec_args: args += '<%s>' % ', '.join(spec_args) - if var_args or sp_optional: - args += (' ' if spec_args else str()) + '<<%s>>' % (var_args or self.optional) + if var_args or sp_extra: + args += (' ' if spec_args else str()) + '<<%s>>' % (var_args or sp_extra) usage += args @@ -385,7 +376,7 @@ class CommandProcessor(object): tuples. The format of the input arguments should be: - <> [-(-o)ption=value1, -(-a)nother=value2] [[extra_options]] + <> [-(-o)ption=value1, -(-a)nother=value2] [[extra_options]] Options may be given in --long or -short format. As --option=value or --option value or -option value. Keys without values will get True as @@ -426,17 +417,6 @@ class CommandProcessor(object): of arguments specified on command definition. That is transforms them to *args and **kwargs suitable for passing to a command handler. - If command defines __arguments__ as a first argument - then this - argument will receive raw and unprocessed arguments. Also, if nothing - except __arguments__ (including *args, *kwargs splatting) is defined - - then all parsed arguments will be discarded. It will be discarded in the - argument usage information. - - If command defines __optional__ - that is an analogue for *args, to - collect extra arguments. This is a preffered way over *args. Because of - some Python limitations, *args could not be mapped to as expected. And - it is hardly advised to define it after all hard arguments. - Extra arguments which are not considered extra (or optional) - will be passed as if they were value for keywords, in the order keywords are defined and printed in usage. @@ -447,10 +427,6 @@ class CommandProcessor(object): spec_args, spec_kwargs, var_args, var_kwargs = command.extract_arg_spec() spec_kwargs = dict(spec_kwargs) - # Check if some special arguments are present. - sp_arguments = '__arguments__' in spec_args - sp_optional = '__optional__' in spec_args - if command.raw: if len(spec_args) == 1: if arguments or command.empty: @@ -458,18 +434,14 @@ class CommandProcessor(object): raise CommandError("Can not be used without arguments", command) raise CommandInternalError("Raw command must define no more then one argument") - if sp_optional: + if command.extra: if not var_args: - hard_len = len(spec_args) - (1 if not sp_arguments else 2) - optional = args[hard_len:] - args = args[:hard_len] - args.insert(spec_args.index('__optional__'), optional) + positional_len = len(spec_args) - (1 if not command.source else 2) + extra = args[positional_len:] + args = args[:positional_len] + args.append(extra) else: - raise CommandInternalError("Can not have both, __optional__ and *args") - - for index, (key, value) in enumerate(opts): - if '-' in key: - opts[index] = (key.replace('-', '_'), value) + raise CommandInternalError("Can not have both, extra and *args") if command.expand_short: expanded = [] @@ -482,16 +454,18 @@ class CommandProcessor(object): opts[index] = (spec_key, value) break + for index, (key, value) in enumerate(opts): + if '-' in key: + opts[index] = (key.replace('-', '_'), value) + # We need to encode every keyword argument to a simple string, not the # unicode one, because ** expansion does not support it. for index, (key, value) in enumerate(opts): if isinstance(key, UnicodeType): opts[index] = (key.encode(cls.ARG_ENCODING), value) - if sp_arguments: - if len(spec_args) == 1 and not spec_kwargs and not var_args and not var_kwargs: - return (arguments,), {} - args.insert(spec_args.index('__arguments__'), arguments) + if command.source: + args.insert(0, arguments) return tuple(args), dict(opts) @@ -561,22 +535,29 @@ def command(*names, **kwargs): A decorator which provides a declarative way of defining commands. You can specify a set of names by which you can call the command. If names - are empty - then the name of the command will be set to native (extracted - from the handler name). If include_native=True argument is given and names - is non-empty - then native name will be added as well. + is empty - then the name of the command will be set to native one (extracted + from the handler name). + + If include_native=True argument is given and names is non-empty - then + native name will be added as well. If usage=True is given - then handler's doc will be appended with an auto-generated usage info. + If source=True is given - then the first positional argument of the command + handler will receive a string with a raw and unprocessed source arguments. + If raw=True is given - then command should define only one argument to - which all raw, unprocessed command arguments will be given. + which all raw and unprocessed source arguments will be given. - If optional is set to a string then if __optional__ specified - its name - ('optional' by-default) in the usage info will be substitued by whatever is - given. + If empty=True is given - then when raw=True is set and command receives no + arguments - an exception will be raised. - If empty=True is given - then if raw is enabled it will allow to pass empty - (None) raw arguments to a command. + If extra=True is given - then last positional argument will receive every + extra positional argument that will be given to a command. This is an + analogue to specifing *args, but the latter one should be used in simplest + cases only because of some Python limitations on this - arguments can't be + mapped correctly when there are keyword arguments present. If expand_short=True is given - then if command receives one-letter options (like -v or -f) they will be expanded to a verbose ones (like @@ -589,13 +570,14 @@ def command(*names, **kwargs): include_native = kwargs.get('include_native', True) usage = kwargs.get('usage', True) + source = kwargs.get('source', False) raw = kwargs.get('raw', False) - optional = kwargs.get('optional', 'optional') + extra = kwargs.get('extra', False) empty = kwargs.get('empty', False) expand_short = kwargs.get('expand_short', True) def decorator(handler): - command = Command(handler, usage, raw, optional, empty, expand_short) + command = Command(handler, usage, source, raw, extra, empty, expand_short) # Extract and inject native name while making sure it is going to be the # first one in the list. From 651e801334da40bd947006b9542529e21d24a56f Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Tue, 15 Sep 2009 17:41:47 +0200 Subject: [PATCH 23/39] correctly change the resource of a control. Fixes #5261 --- src/chat_control.py | 13 +++++++++++++ src/message_window.py | 9 +++++++++ src/session.py | 4 ++-- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 4d2a8806f..e5886c2be 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1433,6 +1433,19 @@ class ChatControl(ChatControlBase): else: self._tune_image.hide() + def change_resource(self, resource): + old_full_jid = self.get_full_jid() + self.resource = resource + new_full_jid = self.get_full_jid() + # update gajim.last_message_time + if old_full_jid in gajim.last_message_time[self.account]: + gajim.last_message_time[self.account][new_full_jid] = \ + gajim.last_message_time[self.account][old_full_jid] + # update events + gajim.events.change_jid(self.account, old_full_jid, new_full_jid) + # update MessageWindow._controls + self.parent_win.change_jid(self.account, old_full_jid, new_full_jid) + def on_avatar_eventbox_enter_notify_event(self, widget, event): ''' we enter the eventbox area so we under conditions add a timeout diff --git a/src/message_window.py b/src/message_window.py index 7d227206c..5b27ee421 100644 --- a/src/message_window.py +++ b/src/message_window.py @@ -158,6 +158,15 @@ class MessageWindow(object): if self.account == old_name: self.account = new_name + def change_jid(self, account, old_jid, new_jid): + ''' call then when the full jid of a contral change''' + if account not in self._controls: + return + if old_jid not in self._controls[account]: + return + self._controls[account][new_jid] = self._controls[account][old_jid] + del self._controls[account][old_jid] + def get_num_controls(self): return sum(len(d) for d in self._controls.values()) diff --git a/src/session.py b/src/session.py index f68d7c2dc..f23f3fe9f 100644 --- a/src/session.py +++ b/src/session.py @@ -88,8 +88,8 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession): subject = msg.getSubject() if self.jid != full_jid_with_resource: self.resource = gajim.get_nick_from_fjid(full_jid_with_resource) - if self.control: - self.control.resource = self.resource + if self.control and self.control.resource: + self.control.change_resource(self.resource) if not msg_type or msg_type not in ('chat', 'groupchat', 'error'): msg_type = 'normal' From da2984434289d317da4d2f708a9cff81f362ed29 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Tue, 15 Sep 2009 19:49:56 +0200 Subject: [PATCH 24/39] [Dicson] fix typo. Fixes #5267 --- src/message_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/message_window.py b/src/message_window.py index 5b27ee421..9e4bc725c 100644 --- a/src/message_window.py +++ b/src/message_window.py @@ -165,7 +165,7 @@ class MessageWindow(object): if old_jid not in self._controls[account]: return self._controls[account][new_jid] = self._controls[account][old_jid] - del self._controls[account][old_jid] + del self._controls[account][old_jid] def get_num_controls(self): return sum(len(d) for d in self._controls.values()) From ebe93d25ff2ebe5821b12a52eb9cc8d3314c29d6 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Tue, 15 Sep 2009 21:19:38 +0200 Subject: [PATCH 25/39] fix memory leak with gtkspell. Fixed #2025 --- src/chat_control.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/chat_control.py b/src/chat_control.py index e5886c2be..dc8a0a1e3 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -74,6 +74,9 @@ if gajim.config.get('use_speller') and HAS_GTK_SPELL: spell.set_language(langs[lang]) except OSError: del langs[lang] + if spell: + spell.detach() + del tv ################################################################################ class ChatControlBase(MessageControl): @@ -2346,6 +2349,10 @@ class ChatControl(ChatControlBase): self.handlers[i].disconnect(i) del self.handlers[i] self.conv_textview.del_handlers() + if gajim.config.get('use_speller') and HAS_GTK_SPELL: + spell_obj = gtkspell.get_from_text_view(self.msg_textview) + if spell_obj: + spell_obj.detach() self.msg_textview.destroy() def minimizable(self): From 10a0867e596a68e90492d28167c0cbb4f296e91d Mon Sep 17 00:00:00 2001 From: red-agent Date: Tue, 15 Sep 2009 22:57:01 +0300 Subject: [PATCH 26/39] Enhanced command arguments parser/adapter --- src/commands/framework.py | 97 +++++++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 29 deletions(-) diff --git a/src/commands/framework.py b/src/commands/framework.py index 866d8ff23..15daa250f 100644 --- a/src/commands/framework.py +++ b/src/commands/framework.py @@ -19,8 +19,9 @@ to implement commands in a streight and flexible, declarative way. """ import re -from types import FunctionType, UnicodeType, TupleType, ListType +from types import FunctionType, UnicodeType, TupleType, ListType, BooleanType from inspect import getargspec +from operator import itemgetter class CommandInternalError(Exception): pass @@ -59,8 +60,13 @@ class Command(object): try: return self.handler(*args, **kwargs) except CommandError, exception: + # Re-raise an excepttion with a proper command attribute set, + # unless it is already set by the one who raised an exception. if not exception.command and not exception.name: raise CommandError(exception.message, self) + # This one is a little bit too wide, but as Python does not have + # anything more constrained - there is no other choice. Take a look here + # if command complains about invalid arguments while they are ok. except TypeError: raise CommandError("Command received invalid arguments", self) @@ -136,13 +142,15 @@ class Command(object): for key, value in spec_kwargs: letter = key[0] + key = key.replace('_', '-') + value = ('=%s' % value) if not isinstance(value, BooleanType) else str() if letter not in letters: - kwargs.append('-(-%s)%s=%s' % (letter, key[1:], value)) + kwargs.append('-(-%s)%s%s' % (letter, key[1:], value)) letters.append(letter) else: - kwargs.append('--%s=%s' % (key, value)) + kwargs.append('--%s%s' % (key, value)) usage = str() args = str() @@ -371,9 +379,10 @@ class CommandProcessor(object): def parse_command_arguments(cls, arguments): """ Simple yet effective and sufficient in most cases parser which parses - command arguments and returns them as two lists, first representing - positional arguments, and second representing options as (key, value) - tuples. + command arguments and returns them as two lists. First represents + positional arguments as (argument, position), and second representing + options as (key, value, position) tuples, where position is a (start, + end) span tuple of where it was found in the string. The format of the input arguments should be: <> [-(-o)ption=value1, -(-a)nother=value2] [[extra_options]] @@ -386,27 +395,36 @@ class CommandProcessor(object): """ args, opts = [], [] - # Need to store position of every option we have parsed in order to get - # arguments to be parsed correct later. - opt_positions = [] + def intersects_opts((given_start, given_end)): + """ + Check if something intersects with boundaries of any parsed option. + """ + for key, value, (start, end) in opts: + if given_start >= start and given_end <= end: + return True + return False - def intersects((given_start, given_end)): + def intersects_args((given_start, given_end)): """ - Check if something intersects with boundaries of any parsed options. + Check if something intersects with boundaries of any parsed argument. """ - for start, end in opt_positions: - if given_start == start or given_end == end: + for arg, (start, end) in args: + if given_start >= start and given_end <= end: return True return False for match in re.finditer(cls.OPT_PATTERN, arguments): if match: - opt_positions.append(match.span()) - opts.append((match.group('key'), match.group('value') or True)) + key = match.group('key') + value = match.group('value') or None + position = match.span() + opts.append((key, value, position)) for match in re.finditer(cls.ARG_PATTERN, arguments): - if match and not intersects(match.span()): - args.append(match.group('body')) + if match and not intersects_opts(match.span()): + body = match.group('body') + position = match.span() + args.append((body, position)) return args, opts @@ -423,17 +441,49 @@ class CommandProcessor(object): Dashes (-) in the option names will be converted to underscores. So you can map --one-more-option to a one_more_option=None. + + If initial value of a keyword argument is a boolean (False in most + cases) then this option will be treated as a switch, that is an option + which does not take an argument. Argument preceded by a switch will be + treated just like a normal positional argument. """ spec_args, spec_kwargs, var_args, var_kwargs = command.extract_arg_spec() spec_kwargs = dict(spec_kwargs) if command.raw: - if len(spec_args) == 1: + if len(spec_args) == 1 and not spec_kwargs and not var_args and not var_kwargs: if arguments or command.empty: return (arguments,), {} raise CommandError("Can not be used without arguments", command) raise CommandInternalError("Raw command must define no more then one argument") + if command.expand_short: + expanded = [] + for spec_key, spec_value in spec_kwargs.iteritems(): + letter = spec_key[0] if len(spec_key) > 1 else None + if letter and letter not in expanded: + for index, (key, value, position) in enumerate(opts): + if key == letter: + expanded.append(letter) + opts[index] = (spec_key, value, position) + break + + for index, (key, value, position) in enumerate(opts): + if isinstance(spec_kwargs.get(key), BooleanType): + opts[index] = (key, True, position) + if value: + args.append((value, position)) + + # Sorting arguments and options (just to be sure) in regarding to their + # positions in the string. + args.sort(key=itemgetter(1)) + opts.sort(key=itemgetter(2)) + + # Stripping down position information supplied with arguments and options as it + # won't be needed again. + args = map(lambda (arg, position): arg, args) + opts = map(lambda (key, value, position): (key, value), opts) + if command.extra: if not var_args: positional_len = len(spec_args) - (1 if not command.source else 2) @@ -443,17 +493,6 @@ class CommandProcessor(object): else: raise CommandInternalError("Can not have both, extra and *args") - if command.expand_short: - expanded = [] - for spec_key, spec_value in spec_kwargs.iteritems(): - letter = spec_key[0] if len(spec_key) > 1 else None - if letter and letter not in expanded: - for index, (key, value) in enumerate(opts): - if key == letter: - expanded.append(letter) - opts[index] = (spec_key, value) - break - for index, (key, value) in enumerate(opts): if '-' in key: opts[index] = (key.replace('-', '_'), value) From 6630d88c0817bc3243269a91d23c4d2b9d5375d4 Mon Sep 17 00:00:00 2001 From: red-agent Date: Tue, 15 Sep 2009 22:59:48 +0300 Subject: [PATCH 27/39] Fixed a bug in the nick command --- src/commands/implementation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/implementation.py b/src/commands/implementation.py index c8f7a38c4..372bd9ff4 100644 --- a/src/commands/implementation.py +++ b/src/commands/implementation.py @@ -134,6 +134,7 @@ class GroupChatCommands(CommonCommands): except Exception: raise CommandError(_("Invalid nickname")) self.connection.join_gc(new_nick, self.room_jid, None, change_nick=True) + self.new_nick = new_nick @command('query', raw=True) def chat(self, nick): From 709dba715a0daec6c9a488b30d493a3f8da63239 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Tue, 15 Sep 2009 22:26:42 +0200 Subject: [PATCH 28/39] global option alwaysauth become a per-account autoauth option. Fixes #5264 --- configure.ac | 2 +- src/common/config.py | 2 +- src/common/connection_handlers.py | 7 ++++--- src/common/defs.py | 2 +- src/common/optparser.py | 9 +++++++++ 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/configure.ac b/configure.ac index 7545378a6..cc04ee34b 100644 --- a/configure.ac +++ b/configure.ac @@ -1,5 +1,5 @@ AC_INIT([Gajim - A Jabber Instant Messager], - [0.12.5.1-dev],[http://trac.gajim.org/],[gajim]) + [0.12.5.2-dev],[http://trac.gajim.org/],[gajim]) AC_PREREQ([2.59]) AC_CONFIG_HEADER(config.h) diff --git a/src/common/config.py b/src/common/config.py index 1f99ef702..f2277f93e 100644 --- a/src/common/config.py +++ b/src/common/config.py @@ -67,7 +67,6 @@ class Config: __options = { # name: [ type, default_value, help_string ] 'verbose': [ opt_bool, False, '', True ], - 'alwaysauth': [ opt_bool, False ], 'autopopup': [ opt_bool, False ], 'notify_on_signin': [ opt_bool, True ], 'notify_on_signout': [ opt_bool, False ], @@ -287,6 +286,7 @@ class Config: 'autoconnect_as': [ opt_str, 'online', _('Status used to autoconnect as. Can be online, chat, away, xa, dnd, invisible. NOTE: this option is used only if restore_last_status is disabled'), True ], 'restore_last_status': [ opt_bool, False, _('If enabled, restore the last status that was used.') ], 'autoreconnect': [ opt_bool, True ], + 'autoauth': [ opt_bool, False, _('If True, Contacts requesting authorization will be automatically accepted.')], 'active': [ opt_bool, True], 'proxy': [ opt_str, '', '', True ], 'keyid': [ opt_str, '', '', True ], diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index b4576a455..60c9fbb5c 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -2353,13 +2353,14 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco, if ptype == 'subscribe': log.debug('subscribe request from %s' % who) - if gajim.config.get('alwaysauth') or who.find("@") <= 0 or \ - jid_stripped in self.jids_for_auto_auth or transport_auto_auth: + if gajim.config.get_per('accounts', self.name, 'autoauth') or \ + who.find('@') <= 0 or jid_stripped in self.jids_for_auto_auth or \ + transport_auto_auth: if self.connection: p = common.xmpp.Presence(who, 'subscribed') p = self.add_sha(p) self.connection.send(p) - if who.find("@") <= 0 or transport_auto_auth: + if who.find('@') <= 0 or transport_auto_auth: self.dispatch('NOTIFY', (jid_stripped, 'offline', 'offline', resource, prio, keyID, timestamp, None)) if transport_auto_auth: diff --git a/src/common/defs.py b/src/common/defs.py index e213fdab1..83ef075a3 100644 --- a/src/common/defs.py +++ b/src/common/defs.py @@ -27,7 +27,7 @@ docdir = '../' datadir = '../' localedir = '../po' -version = '0.12.5.1-dev' +version = '0.12.5.2-dev' import sys, os.path for base in ('.', 'common'): diff --git a/src/common/optparser.py b/src/common/optparser.py index bc455e1f6..5cedeeaca 100644 --- a/src/common/optparser.py +++ b/src/common/optparser.py @@ -202,6 +202,8 @@ class OptionsParser: self.update_config_to_01231() if old < [0, 12, 5, 1] and new >= [0, 12, 5, 1]: self.update_config_to_01251() + if old < [0, 12, 5, 2] and new >= [0, 12, 5, 2]: + self.update_config_to_01252() gajim.logger.init_vars() gajim.config.set('version', new_version) @@ -727,4 +729,11 @@ class OptionsParser: con.close() gajim.config.set('version', '0.12.5.1') + def update_config_to_01252(self): + if 'alwaysauth' in self.old_values: + val = self.old_values['alwaysauth'] + for account in gajim.config.get_per('accounts'): + gajim.config.set_per('accounts', account, 'autoauth', val) + gajim.config.set('version', '0.12.5.2') + # vim: se ts=3: From 4dae0bde445fbce3870a1bcf290647bb69ae0f47 Mon Sep 17 00:00:00 2001 From: red-agent Date: Tue, 15 Sep 2009 23:40:29 +0300 Subject: [PATCH 29/39] Removed unused code --- src/commands/framework.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/commands/framework.py b/src/commands/framework.py index 15daa250f..bb15fd2ca 100644 --- a/src/commands/framework.py +++ b/src/commands/framework.py @@ -222,11 +222,6 @@ class Dispatcher(type): if hoster not in bases: raise CommandInternalError("Should be hosted by the same processors it inherits from") - @classmethod - def check_if_conformed(cls, dispatchable, hostable): - if dispatchable and hostable: - raise CommandInternalError("Processor can not be dispatchable and hostable at the same time") - @classmethod def register_processor(cls, proc): cls.table[proc] = {} From 4e4a91dbe112f2eeecd01822a3d39c89f0ab5eb1 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 16 Sep 2009 16:14:03 +0200 Subject: [PATCH 30/39] prevent traceback. Fixes #5269 --- src/dialogs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dialogs.py b/src/dialogs.py index a12ceee8a..d67d85ee2 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -1698,7 +1698,8 @@ class ChangeNickDialog(InputDialogCheck): if len(self.room_queue) == 0: self.cancel_handler = None self.dialog.destroy() - del gajim.interface.instances['change_nick_dialog'] + if 'change_nick_dialog' in gajim.interface.instances: + del gajim.interface.instances['change_nick_dialog'] return self.account, self.room_jid, self.prompt = self.room_queue.pop(0) self.setup_dialog() From 1a327414cae14ef089ea5233853bda065c2b04e6 Mon Sep 17 00:00:00 2001 From: red-agent Date: Thu, 17 Sep 2009 04:38:39 +0300 Subject: [PATCH 31/39] Moderate refactoring and parser/adapter enhancements --- src/commands/framework.py | 194 +++++++++++++++++++++++++++------ src/commands/implementation.py | 42 ++++--- src/commands/middleware.py | 9 -- 3 files changed, 182 insertions(+), 63 deletions(-) diff --git a/src/commands/framework.py b/src/commands/framework.py index bb15fd2ca..85b43eef9 100644 --- a/src/commands/framework.py +++ b/src/commands/framework.py @@ -23,7 +23,7 @@ from types import FunctionType, UnicodeType, TupleType, ListType, BooleanType from inspect import getargspec from operator import itemgetter -class CommandInternalError(Exception): +class InternalError(Exception): pass class CommandError(Exception): @@ -46,13 +46,14 @@ class Command(object): ARG_USAGE_PATTERN = 'Usage: %s %s' - def __init__(self, handler, usage, source, raw, extra, empty, expand_short): + def __init__(self, handler, usage, source, raw, extra, overlap, empty, expand_short): self.handler = handler self.usage = usage self.source = source self.raw = raw self.extra = extra + self.overlap = overlap self.empty = empty self.expand_short = expand_short @@ -120,7 +121,7 @@ class Command(object): # Removing self from arguments specification. Command handler should # normally be an instance method. if spec_args.pop(0) != 'self': - raise CommandInternalError("First argument must be self") + raise InternalError("First argument must be self") return spec_args, spec_kwargs, var_args, var_kwargs @@ -142,9 +143,14 @@ class Command(object): for key, value in spec_kwargs: letter = key[0] - key = key.replace('_', '-') - value = ('=%s' % value) if not isinstance(value, BooleanType) else str() + + if isinstance(value, BooleanType): + value = str() + elif isinstance(value, (TupleType, ListType)): + value = '={%s}' % ', '.join(value) + else: + value = '=%s' % value if letter not in letters: kwargs.append('-(-%s)%s%s' % (letter, key[1:], value)) @@ -155,9 +161,12 @@ class Command(object): usage = str() args = str() - if len(spec_args) == 1 and self.raw: - args += ('(|%s|)' if self.empty else '|%s|') % spec_args[0] - elif spec_args or var_args or sp_extra: + if self.raw: + spec_len = len(spec_args) - 1 + if spec_len: + args += ('<%s>' % ', '.join(spec_args[:spec_len])) + ' ' + args += ('(|%s|)' if self.empty else '|%s|') % spec_args[-1] + else: if spec_args: args += '<%s>' % ', '.join(spec_args) if var_args or sp_extra: @@ -185,8 +194,10 @@ class Dispatcher(type): hosted = {} def __init__(cls, name, bases, dct): - Dispatcher.check_if_dispatchable(bases, dct) - Dispatcher.check_if_hostable(bases, dct) + dispatchable = Dispatcher.check_if_dispatchable(bases, dct) + hostable = Dispatcher.check_if_hostable(bases, dct) + + cls.check_if_conformed(dispatchable, hostable) if Dispatcher.is_suitable(cls, dct): Dispatcher.register_processor(cls) @@ -207,20 +218,27 @@ class Dispatcher(type): def check_if_dispatchable(cls, bases, dct): dispatcher = dct.get('DISPATCHED_BY') if not dispatcher: - return + return False if dispatcher not in bases: - raise CommandInternalError("Should be dispatched by the same processor it inherits from") + raise InternalError("Should be dispatched by the same processor it inherits from") + return True @classmethod def check_if_hostable(cls, bases, dct): hosters = dct.get('HOSTED_BY') if not hosters: - return + return False if not isinstance(hosters, (TupleType, ListType)): hosters = (hosters,) for hoster in hosters: if hoster not in bases: - raise CommandInternalError("Should be hosted by the same processors it inherits from") + raise InternalError("Should be hosted by the same processors it inherits from") + return True + + @classmethod + def check_if_conformed(cls, dispatchable, hostable): + if dispatchable and hostable: + raise InternalError("Processor can not be dispatchable and hostable at the same time") @classmethod def register_processor(cls, proc): @@ -261,7 +279,7 @@ class Dispatcher(type): if name not in cls.table[proc]: cls.table[proc][name] = command else: - raise CommandInternalError("Command with name %s already exists" % name) + raise InternalError("Command with name %s already exists" % name) @classmethod def register_adhocs(cls, proc): hosters = proc.HOSTED_BY @@ -421,6 +439,13 @@ class CommandProcessor(object): position = match.span() args.append((body, position)) + # In rare occasions quoted options are being captured, while they should + # not be. This fixes the problem by finding options which intersect with + # arguments and removing them. + for key, value, position in opts[:]: + if intersects_args(position): + opts.remove((key, value, position)) + return args, opts @classmethod @@ -441,20 +466,85 @@ class CommandProcessor(object): cases) then this option will be treated as a switch, that is an option which does not take an argument. Argument preceded by a switch will be treated just like a normal positional argument. + + If keyword argument's initial value is a sequence (tuple or a string) + then possible values of the option will be restricted to one of the + values given by the sequence. """ spec_args, spec_kwargs, var_args, var_kwargs = command.extract_arg_spec() - spec_kwargs = dict(spec_kwargs) + norm_kwargs = dict(spec_kwargs) + # Quite complex piece of neck-breaking logic to extract raw arguments if + # there is more, then one positional argument specified by the command. + # In case if it's just one argument which is the collector this is + # fairly easy. But when it's more then one argument - the neck-breaking + # logic of how to retrieve residual arguments as a raw, all in one piece + # string, kicks on. if command.raw: - if len(spec_args) == 1 and not spec_kwargs and not var_args and not var_kwargs: - if arguments or command.empty: - return (arguments,), {} - raise CommandError("Can not be used without arguments", command) - raise CommandInternalError("Raw command must define no more then one argument") + if spec_kwargs or var_args or var_kwargs: + raise InternalError("Raw commands should define only positional arguments") + if arguments: + spec_fix = 1 if command.source else 0 + spec_len = len(spec_args) - spec_fix + arguments_end = len(arguments) - 1 + + # If there are any optional arguments given they should be + # either an unquoted postional argument or part of the raw + # argument. So we find all optional arguments that can possibly + # be unquoted argument and append them as is to the args. + for key, value, (start, end) in opts[:spec_len]: + if value: + end -= len(value) + 1 + args.append((arguments[start:end], (start, end))) + args.append((value, (end, end + len(value) + 1))) + else: + args.append((arguments[start:end], (start, end))) + + # We need in-place sort here because after manipulations with + # options order of arguments might be wrong and we just can't + # have more complex logic to not let that happen. + args.sort(key=itemgetter(1)) + + if spec_len > 1: + stopper, (start, end) = args[spec_len - 2] + + raw = arguments[end:] + raw = raw.strip() or None + + if not raw and not command.empty: + raise CommandError("Missing arguments", command) + + # Discard residual arguments and all of the options as raw + # command does not support options and if an option is given + # it is rather a part of a raw argument. + args = args[:spec_len - 1] + opts = [] + + args.append((raw, (end, arguments_end))) + elif spec_len == 1: + args = [(arguments, (0, arguments_end))] + else: + raise InternalError("Raw command must define a collector") + else: + if command.empty: + args.append((None, (0, 0))) + else: + raise CommandError("Missing arguments", command) + + # The first stage of transforming options we have got to a format that + # can be used to associate them with declared keyword arguments. + # Substituting dashes (-) in their names with underscores (_). + for index, (key, value, position) in enumerate(opts): + if '-' in key: + opts[index] = (key.replace('-', '_'), value, position) + + # The second stage of transforming options to an associatable state. + # Expanding short, one-letter options to a verbose ones, if + # corresponding optin has been given. if command.expand_short: expanded = [] - for spec_key, spec_value in spec_kwargs.iteritems(): + for spec_key, spec_value in norm_kwargs.iteritems(): letter = spec_key[0] if len(spec_key) > 1 else None if letter and letter not in expanded: for index, (key, value, position) in enumerate(opts): @@ -463,8 +553,10 @@ class CommandProcessor(object): opts[index] = (spec_key, value, position) break + # Detect switches and set their values accordingly. If any of them + # carries a value - append it to args. for index, (key, value, position) in enumerate(opts): - if isinstance(spec_kwargs.get(key), BooleanType): + if isinstance(norm_kwargs.get(key), BooleanType): opts[index] = (key, True, position) if value: args.append((value, position)) @@ -479,18 +571,40 @@ class CommandProcessor(object): args = map(lambda (arg, position): arg, args) opts = map(lambda (key, value, position): (key, value), opts) + # If command has extra option enabled - collect all extra arguments and + # pass them to a last positional argument command defines as a list. if command.extra: if not var_args: - positional_len = len(spec_args) - (1 if not command.source else 2) - extra = args[positional_len:] - args = args[:positional_len] + spec_fix = 1 if not command.source else 2 + spec_len = len(spec_args) - spec_fix + extra = args[spec_len:] + args = args[:spec_len] args.append(extra) else: - raise CommandInternalError("Can not have both, extra and *args") + raise InternalError("Can not have both, extra and *args") - for index, (key, value) in enumerate(opts): - if '-' in key: - opts[index] = (key.replace('-', '_'), value) + # Detect if positional arguments overlap keyword arguments. If so and + # this is allowed by command options - then map them directly to their + # options, so they can get propert further processings. + spec_fix = 1 if command.source else 0 + spec_len = len(spec_args) - spec_fix + if len(args) > spec_len: + if command.overlap: + overlapped = args[spec_len:] + args = args[:spec_len] + for arg, (spec_key, spec_value) in zip(overlapped, spec_kwargs): + opts.append((spec_key, arg)) + else: + raise CommandError("Excessive arguments", command) + + # Detect every contraint sequences and ensure that if corresponding + # options are given - they contain proper values, within constraint + # range. + for key, value in opts: + initial = norm_kwargs.get(key) + if isinstance(initial, (TupleType, ListType)) and value not in initial: + if value: + raise CommandError("Wrong argument", command) # We need to encode every keyword argument to a simple string, not the # unicode one, because ** expansion does not support it. @@ -498,9 +612,13 @@ class CommandProcessor(object): if isinstance(key, UnicodeType): opts[index] = (key.encode(cls.ARG_ENCODING), value) + # Inject the source arguments as a string as a first argument, if + # command has enabled the corresponding option. if command.source: args.insert(0, arguments) + # Return *args and **kwargs in the form suitable for passing to a + # command handlers and being expanded. return tuple(args), dict(opts) def process_as_command(self, text): @@ -515,11 +633,7 @@ class CommandProcessor(object): text = text.strip() parts = text.split(' ', 1) - - if len(parts) > 1: - name, arguments = parts - else: - name, arguments = parts[0], None + name, arguments = parts if len(parts) > 1 else (parts[0], None) flag = self.looks_like_command(text, name, arguments) if flag is not None: @@ -593,6 +707,10 @@ def command(*names, **kwargs): cases only because of some Python limitations on this - arguments can't be mapped correctly when there are keyword arguments present. + If overlap=True is given - then if extra=False and there is extra arguments + given to the command - they will be mapped as if they were values for the + keyword arguments, in the order they are defined. + If expand_short=True is given - then if command receives one-letter options (like -v or -f) they will be expanded to a verbose ones (like --verbose or --file) if the latter are defined as a command optional @@ -607,11 +725,15 @@ def command(*names, **kwargs): source = kwargs.get('source', False) raw = kwargs.get('raw', False) extra = kwargs.get('extra', False) + overlap = kwargs.get('overlap', False) empty = kwargs.get('empty', False) expand_short = kwargs.get('expand_short', True) + if extra and overlap: + raise InternalError("Extra and overlap options can not be used together") + def decorator(handler): - command = Command(handler, usage, source, raw, extra, empty, expand_short) + command = Command(handler, usage, source, raw, extra, overlap, empty, expand_short) # Extract and inject native name while making sure it is going to be the # first one in the list. diff --git a/src/commands/implementation.py b/src/commands/implementation.py index 372bd9ff4..fd5e676a5 100644 --- a/src/commands/implementation.py +++ b/src/commands/implementation.py @@ -46,7 +46,7 @@ class CommonCommands(ChatMiddleware): """ self.chat_buttons_set_visible(not self.hide_chat_buttons) - @command + @command(overlap=True) def help(self, command=None, all=False): """ Show help on a given command or a list of available commands if -(-a)ll is @@ -85,6 +85,15 @@ class CommonCommands(ChatMiddleware): """ self.send("/me %s" % action) + @command(raw=True, empty=True) + def test(self, one, two, three): + self.echo(one) + self.echo(two) + self.echo(three) + + from pprint import pformat + return "Locals:\n%s" % pformat(locals()) + class ChatCommands(CommonCommands): """ Here defined commands will be unique to a chat. Use it as a hoster to provide @@ -147,13 +156,12 @@ class GroupChatCommands(CommonCommands): else: raise CommandError(_("Nickname not found")) - @command('msg') - def message(self, nick, *a_message): + @command('msg', raw=True) + def message(self, nick, a_message): """ Open a private chat window with a specified occupant and send him a message """ - a_message = self.collect(a_message, False) nicks = gajim.contacts.get_nick_list(self.account, self.room_jid) if nick in nicks: self.on_send_pm(nick=nick, msg=a_message) @@ -170,21 +178,21 @@ class GroupChatCommands(CommonCommands): else: return self.subject - @command - def invite(self, jid, *reason): + @command(raw=True, empty=True) + def invite(self, jid, reason): """ Invite a user to a room for a reason """ - reason = self.collect(reason) self.connection.send_invite(self.room_jid, jid, reason) return _("Invited %s to %s") % (jid, self.room_jid) - @command - def join(self, jid, *nick): + @command(raw=True, empty=True) + def join(self, jid, nick): """ Join a group chat given by a jid, optionally using given nickname """ - nick = self.collect(nick) or self.nick + if not nick: + nick = self.nick if '@' not in jid: jid = jid + '@' + gajim.get_server_from_jid(self.room_jid) @@ -204,28 +212,26 @@ class GroupChatCommands(CommonCommands): """ self.parent_win.remove_tab(self, self.parent_win.CLOSE_COMMAND, reason) - @command - def ban(self, who, *reason): + @command(raw=True, empty=True) + def ban(self, who, reason): """ Ban user by a nick or a jid from a groupchat If given nickname is not found it will be treated as a jid. """ - reason = self.collect(reason, none=False) if who in gajim.contacts.get_nick_list(self.account, self.room_jid): contact = gajim.contacts.get_gc_contact(self.account, self.room_jid, who) who = contact.jid - self.connection.gc_set_affiliation(self.room_jid, who, 'outcast', reason) + self.connection.gc_set_affiliation(self.room_jid, who, 'outcast', reason or str()) - @command - def kick(self, who, *reason): + @command(raw=True, empty=True) + def kick(self, who, reason): """ Kick user by a nick from a groupchat """ - reason = self.collect(reason, none=False) if not who in gajim.contacts.get_nick_list(self.account, self.room_jid): raise CommandError(_("Nickname not found")) - self.connection.gc_set_role(self.room_jid, who, 'none', reason) + self.connection.gc_set_role(self.room_jid, who, 'none', reason or str()) @command def names(self, verbose=False): diff --git a/src/commands/middleware.py b/src/commands/middleware.py index 1d024c5e4..ad4fcbf9e 100644 --- a/src/commands/middleware.py +++ b/src/commands/middleware.py @@ -97,15 +97,6 @@ class ChatMiddleware(CommandProcessor): """ self.save_sent_message(text) - def collect(self, arguments, empty=True, separator=' ', none=True): - """ - Might come in handy in case if you want to map some arguments and - collect the rest of them into a string. - """ - if not empty and not arguments: - raise CommandError(_("Missing argument")) - return None if not arguments and none else separator.join(arguments) - @property def connection(self): """ From 684662c15d66c7f3fdadcc3c5004ea65636d0ade Mon Sep 17 00:00:00 2001 From: red-agent Date: Thu, 17 Sep 2009 09:03:57 +0300 Subject: [PATCH 32/39] Few fixes for the command system --- src/commands/framework.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/commands/framework.py b/src/commands/framework.py index 85b43eef9..5d2b2f19c 100644 --- a/src/commands/framework.py +++ b/src/commands/framework.py @@ -65,6 +65,11 @@ class Command(object): # unless it is already set by the one who raised an exception. if not exception.command and not exception.name: raise CommandError(exception.message, self) + + # Do not forget to re-raise an exception just like it was if at + # least either, command or name attribute is set properly. + raise + # This one is a little bit too wide, but as Python does not have # anything more constrained - there is no other choice. Take a look here # if command complains about invalid arguments while they are ok. @@ -603,8 +608,14 @@ class CommandProcessor(object): for key, value in opts: initial = norm_kwargs.get(key) if isinstance(initial, (TupleType, ListType)) and value not in initial: - if value: - raise CommandError("Wrong argument", command) + raise CommandError("Wrong argument", command) + + # Detect every switch and ensure it will not receive any arguments. + # Normally this does not happen unless overlapping is enabled. + for key, value in opts: + initial = norm_kwargs.get(key) + if isinstance(initial, BooleanType) and not isinstance(value, BooleanType): + raise CommandError("Switches do not take arguments", command) # We need to encode every keyword argument to a simple string, not the # unicode one, because ** expansion does not support it. From 91e28d0ed9b00307ab55d0b1091ffd07357a2647 Mon Sep 17 00:00:00 2001 From: red-agent Date: Thu, 17 Sep 2009 11:24:58 +0300 Subject: [PATCH 33/39] Removed the command that was accidently commited --- src/commands/implementation.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/commands/implementation.py b/src/commands/implementation.py index fd5e676a5..54a13b8a2 100644 --- a/src/commands/implementation.py +++ b/src/commands/implementation.py @@ -85,15 +85,6 @@ class CommonCommands(ChatMiddleware): """ self.send("/me %s" % action) - @command(raw=True, empty=True) - def test(self, one, two, three): - self.echo(one) - self.echo(two) - self.echo(three) - - from pprint import pformat - return "Locals:\n%s" % pformat(locals()) - class ChatCommands(CommonCommands): """ Here defined commands will be unique to a chat. Use it as a hoster to provide From 90d378941b253e1b007d0aefb61f5932c4d85d48 Mon Sep 17 00:00:00 2001 From: red-agent Date: Thu, 17 Sep 2009 11:36:47 +0300 Subject: [PATCH 34/39] Fixed arguments check in raw commands --- src/commands/framework.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/commands/framework.py b/src/commands/framework.py index 5d2b2f19c..72b0000b2 100644 --- a/src/commands/framework.py +++ b/src/commands/framework.py @@ -512,7 +512,10 @@ class CommandProcessor(object): args.sort(key=itemgetter(1)) if spec_len > 1: - stopper, (start, end) = args[spec_len - 2] + try: + stopper, (start, end) = args[spec_len - 2] + except IndexError: + raise CommandError("Missing arguments", command) raw = arguments[end:] raw = raw.strip() or None From 39eef45bca02c4328ba76992571fcb6a39118d79 Mon Sep 17 00:00:00 2001 From: red-agent Date: Thu, 17 Sep 2009 13:25:25 +0300 Subject: [PATCH 35/39] Command auto-completion along with some fixes --- src/chat_control.py | 49 ++++++++++++++++++++++++++++++++++++++- src/commands/framework.py | 5 ++-- src/groupchat_control.py | 7 ++++++ 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 66e13114b..5aeba6248 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -148,7 +148,52 @@ class ChatControlBase(MessageControl, CommonCommands): event_keymod): # Derived should implement this rather than connecting to the event # itself. - pass + + event = gtk.gdk.Event(gtk.gdk.KEY_PRESS) + event.keyval = event_keyval + event.state = event_keymod + event.time = 0 + + buffer = widget.get_buffer() + start, end = buffer.get_bounds() + + if event.keyval -- gtk.keysyms.Tab: + position = buffer.get_insert() + end = buffer.get_iter_at_mark(position) + + text = buffer.get_text(start, end, False) + text = text.decode('utf8') + + if (text.startswith(self.COMMAND_PREFIX) and not + text.startswith(self.COMMAND_PREFIX * 2)): + + text = text.split()[0] + bare = text.lstrip(self.COMMAND_PREFIX) + + if len(text) == 1: + self.command_hits = [] + for command in self.list_commands(): + for name in command.names: + self.command_hits.append(name) + else: + if (self.last_key_tabs and self.command_hits and + self.command_hits[0].startswith(bare)): + self.command_hits.append(self.command_hits.pop(0)) + else: + self.command_hits = [] + for command in self.list_commands(): + for name in command.names: + if name.startswith(bare): + self.command_hits.append(name) + + if self.command_hits: + buffer.delete(start, end) + buffer.insert_at_cursor(self.COMMAND_PREFIX + self.command_hits[0] + ' ') + self.last_key_tabs = True + + return True + + self.last_key_tabs = False def status_url_clicked(self, widget, url): helpers.launch_browser_mailer('url', url) @@ -305,6 +350,8 @@ class ChatControlBase(MessageControl, CommonCommands): self.smooth = True self.msg_textview.grab_focus() + self.command_hits = [] + def set_speller(self): # now set the one the user selected per_type = 'contacts' diff --git a/src/commands/framework.py b/src/commands/framework.py index 72b0000b2..9520533ff 100644 --- a/src/commands/framework.py +++ b/src/commands/framework.py @@ -370,11 +370,10 @@ class CommandProcessor(object): """ command_name = self.SAFE_NAME_SCAN_PATTERN.match(name) if command_name: - command = Dispatcher.retrieve_command(self, command_name.group('name')) + command = self.retrieve_command(command_name.group('name')) if command: return command - raise AttributeError(name) - return super(CommandProcessor, self).__getattr__(name) + raise AttributeError(name) @classmethod def prepare_name(cls, name): diff --git a/src/groupchat_control.py b/src/groupchat_control.py index 6b959a5e0..4d6acf591 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -1783,6 +1783,13 @@ class GroupchatControl(ChatControlBase, GroupChatCommands): splitted_text = text.split() + # HACK: Not the best soltution. + if (text.startswith(self.COMMAND_PREFIX) and not + text.startswith(self.COMMAND_PREFIX * 2)): + return super(GroupchatControl, + self).handle_message_textview_mykey_press(widget, event_keyval, + event_keymod) + # nick completion # check if tab is pressed with empty message if len(splitted_text): # if there are any words From b6481d54eae73cd31f56b437d38da2fd1f6373c5 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Thu, 17 Sep 2009 13:54:20 +0200 Subject: [PATCH 36/39] [Jonathan Michalon] Make latex images theme aware. --- src/common/latex.py | 8 +++++++- src/gajim.py | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/common/latex.py b/src/common/latex.py index 2d783726d..777577d1c 100644 --- a/src/common/latex.py +++ b/src/common/latex.py @@ -114,6 +114,12 @@ def latex_to_image(str_): result = None exitcode = 0 + try: + bg_str, fg_str = gajim.interface.get_bg_fg_colors() + except: + # interface may not be available when we test latext at startup + bg_str, fg_str = 'rgb 1.0 1.0 1.0', 'rgb 0.0 0.0 0.0' + # filter latex code with bad commands if check_blacklist(str_): # we triggered the blacklist, immediately return None @@ -131,7 +137,7 @@ def latex_to_image(str_): if exitcode == 0: # convert dvi to png latex_png_dpi = gajim.config.get('latex_png_dpi') - exitcode = try_run(['dvipng', '-bg', 'rgb 1.0 1.0 1.0', '-T', + exitcode = try_run(['dvipng', '-bg', bg_str, '-fg', fg_str, '-T', 'tight', '-D', latex_png_dpi, tmpfile + '.dvi', '-o', tmpfile + '.png']) diff --git a/src/gajim.py b/src/gajim.py index b8512ac43..46357eb4b 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -3036,6 +3036,25 @@ class Interface: pep.user_send_tune(acct, artist, title, source) gajim.connections[acct].music_track_info = music_track_info + def get_bg_fg_colors(self): + def gdkcolor_to_rgb (gdkcolor): + return [c / 65535. for c in (gdkcolor.red, gdkcolor.green, + gdkcolor.blue)] + + def format_rgb (r, g, b): + return ' '.join([str(c) for c in ('rgb', r, g, b)]) + + def format_gdkcolor (gdkcolor): + return format_rgb (*gdkcolor_to_rgb (gdkcolor)) + + # get style colors and create string for dvipng + dummy = gtk.Invisible() + dummy.ensure_style() + style = dummy.get_style() + bg_str = format_gdkcolor(style.base[gtk.STATE_NORMAL]) + fg_str = format_gdkcolor(style.text[gtk.STATE_NORMAL]) + return (bg_str, fg_str) + def read_sleepy(self): '''Check idle status and change that status if needed''' if not self.sleeper.poll(): From e0e4dd6c31e8de4f0fb317e08ce37b002bd7a397 Mon Sep 17 00:00:00 2001 From: red-agent Date: Thu, 17 Sep 2009 19:06:38 +0300 Subject: [PATCH 37/39] A bunch of fixes for the command completion --- src/chat_control.py | 9 ++++++--- src/groupchat_control.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/chat_control.py b/src/chat_control.py index 5aeba6248..6c4d613e3 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -164,10 +164,12 @@ class ChatControlBase(MessageControl, CommonCommands): text = buffer.get_text(start, end, False) text = text.decode('utf8') - if (text.startswith(self.COMMAND_PREFIX) and not - text.startswith(self.COMMAND_PREFIX * 2)): + splitted = text.split() - text = text.split()[0] + if (text.startswith(self.COMMAND_PREFIX) and not + text.startswith(self.COMMAND_PREFIX * 2) and len(splitted) == 1): + + text = splitted[0] bare = text.lstrip(self.COMMAND_PREFIX) if len(text) == 1: @@ -351,6 +353,7 @@ class ChatControlBase(MessageControl, CommonCommands): self.msg_textview.grab_focus() self.command_hits = [] + self.last_key_tabs = False def set_speller(self): # now set the one the user selected diff --git a/src/groupchat_control.py b/src/groupchat_control.py index 4d6acf591..4cecf8c97 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -1785,7 +1785,7 @@ class GroupchatControl(ChatControlBase, GroupChatCommands): # HACK: Not the best soltution. if (text.startswith(self.COMMAND_PREFIX) and not - text.startswith(self.COMMAND_PREFIX * 2)): + text.startswith(self.COMMAND_PREFIX * 2) and len(splitted_text) == 1): return super(GroupchatControl, self).handle_message_textview_mykey_press(widget, event_keyval, event_keymod) From 8ae6679fe8effbc16a1e02f59c920c8a578a0f79 Mon Sep 17 00:00:00 2001 From: red-agent Date: Fri, 18 Sep 2009 17:32:27 +0300 Subject: [PATCH 38/39] Added the new command system to the build system --- src/Makefile.am | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Makefile.am b/src/Makefile.am index 8f6c153a8..2210b7b3a 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -41,12 +41,17 @@ gajimsrc3dir = $(pkgdatadir)/src/common/zeroconf gajimsrc3_PYTHON = \ $(srcdir)/common/zeroconf/*.py +gajimsrc4dir = $(pkglibdir)/src/commands +gajimsrc4_PYTHON = \ + $(srcdir)/commands/*.py + DISTCLEANFILES = EXTRA_DIST = $(gajimsrc_PYTHON) \ $(gajimsrc1_PYTHON) \ $(gajimsrc2_PYTHON) \ $(gajimsrc3_PYTHON) \ + $(gajimsrc4_PYTHON) \ eggtrayicon.c \ trayiconmodule.c \ eggtrayicon.h \ From 89bfa782dc7cea01070bacb08010bff9eb2ee4e8 Mon Sep 17 00:00:00 2001 From: red-agent Date: Fri, 18 Sep 2009 18:45:08 +0300 Subject: [PATCH 39/39] Gajim's build system is a little bit funky. Fixes 5274 --- src/Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Makefile.am b/src/Makefile.am index 2210b7b3a..17a1d1c9c 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -41,7 +41,7 @@ gajimsrc3dir = $(pkgdatadir)/src/common/zeroconf gajimsrc3_PYTHON = \ $(srcdir)/common/zeroconf/*.py -gajimsrc4dir = $(pkglibdir)/src/commands +gajimsrc4dir = $(pkgdatadir)/src/commands gajimsrc4_PYTHON = \ $(srcdir)/commands/*.py