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/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/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/Makefile.am b/src/Makefile.am index 8f6c153a8..17a1d1c9c 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 = $(pkgdatadir)/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 \ diff --git a/src/chat_control.py b/src/chat_control.py index bafe57be5..2ff30f878 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -52,6 +52,8 @@ from common.xmpp.protocol import NS_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC from common.xmpp.protocol import NS_RECEIPTS, NS_ESESSION from common.xmpp.protocol import NS_JINGLE_RTP_AUDIO +from commands.implementation import CommonCommands, ChatCommands + try: import gtkspell HAS_GTK_SPELL = True @@ -75,11 +77,15 @@ 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): +class ChatControlBase(MessageControl, CommonCommands): '''A base class containing a banner, ConversationTextview, MessageTextView ''' + def make_href(self, match): url_color = gajim.config.get('urlmsgcolor') return '%s' % (match.group(), @@ -146,7 +152,54 @@ class ChatControlBase(MessageControl): 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') + + splitted = text.split() + + 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: + 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) @@ -303,6 +356,9 @@ class ChatControlBase(MessageControl): self.smooth = True self.msg_textview.grab_focus() + self.command_hits = [] + self.last_key_tabs = False + def set_speller(self): # now set the one the user selected per_type = 'contacts' @@ -604,45 +660,27 @@ 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, - xhtml=None, callback=None, callback_args=[]): + msg_id=None, composing_xep=None, resource=None, + xhtml=None, callback=None, callback_args=[], process_commands=True): '''Send the given message to the active tab. Doesn't return None if error ''' if not message or message == '\n': return None - 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) + if process_commands and self.process_as_command(message): + return - # Record message history - self.save_sent_message(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) - # Be sure to send user nickname only once according to JEP-0172 - self.user_nick = None + # Record message history + self.save_sent_message(message) + + # 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() @@ -1126,7 +1164,7 @@ class ChatControlBase(MessageControl): # FIXME: Set sensitivity for toolbar ################################################################################ -class ChatControl(ChatControlBase): +class ChatControl(ChatControlBase, ChatCommands): '''A control for standard 1-1 chat''' ( AUDIO_STATE_NOT_AVAILABLE, @@ -1139,7 +1177,8 @@ class ChatControl(ChatControlBase): TYPE_ID = message_control.TYPE_CHAT old_msg_kind = None # last kind of the printed message - CHAT_CMDS = ['clear', 'compact', 'help', 'me', 'ping', 'say'] + + DISPATCHED_BY = ChatCommands def __init__(self, parent_win, contact, acct, session, resource = None): ChatControlBase.__init__(self, self.TYPE_ID, parent_win, @@ -1490,6 +1529,19 @@ class ChatControl(ChatControlBase): self._audio_image.set_from_stock(gtk.STOCK_DIALOG_WARNING, 1) self.update_toolbar() + 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 set_audio_state(self, state, sid=None, reason=None): if state in ('connecting', 'connected', 'stop'): str = _('Audio state : %s') % state @@ -1813,83 +1865,12 @@ 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): + def send_message(self, message, keyID='', chatstate=None, xhtml=None, + process_commands=True): '''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() @@ -1948,8 +1929,9 @@ 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, - callback_args=[contact, message, encrypted, xhtml]) + xhtml=xhtml, callback=_on_sent, + 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 @@ -2428,6 +2410,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): diff --git a/src/commands/__init__.py b/src/commands/__init__.py new file mode 100644 index 000000000..48f99c416 --- /dev/null +++ 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/custom.py b/src/commands/custom.py new file mode 100644 index 000000000..44a8ab073 --- /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. + """ + + DISPATCH = 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. + """ + + DISPATCH = 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. + """ + + DISPATCH = 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. + """ + + DISPATCH = 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..9520533ff --- /dev/null +++ b/src/commands/framework.py @@ -0,0 +1,764 @@ +# 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, BooleanType +from inspect import getargspec +from operator import itemgetter + +class InternalError(Exception): + pass + +class CommandError(Exception): + def __init__(self, message=None, command=None, name=None): + self.command = command + self.name = name + + if command: + self.name = command.first_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) + 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() + return doc.split('\n', 1)[0] if doc else None + + 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 = list(zip(names[-len(defaults):], defaults)) if defaults else {} + + # Removing self from arguments specification. Command handler should + # normally be an instance method. + if spec_args.pop(0) != 'self': + raise InternalError("First argument must be self") + + 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. + """ + spec_args, spec_kwargs, var_args, var_kwargs = self.extract_arg_spec() + + # 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 = [] + + for key, value in spec_kwargs: + letter = key[0] + key = key.replace('_', '-') + + 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)) + letters.append(letter) + else: + kwargs.append('--%s%s' % (key, value)) + + usage = str() + args = str() + + 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: + args += (' ' if spec_args else str()) + '<<%s>>' % (var_args or sp_extra) + + 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 Dispatcher(type): + table = {} + hosted = {} + + def __init__(cls, name, 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) + + # 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 + to_be_dispatched = bool(dct.get('DISPATCH')) + return is_not_root and to_be_dispatched + + @classmethod + def check_if_dispatchable(cls, bases, dct): + dispatcher = dct.get('DISPATCHED_BY') + if not dispatcher: + return False + if dispatcher not in bases: + 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 False + if not isinstance(hosters, (TupleType, ListType)): + hosters = (hosters,) + for hoster in hosters: + if hoster not in bases: + 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): + cls.table[proc] = {} + inherit = proc.__dict__.get('INHERIT') + + if 'HOSTED_BY' in proc.__dict__: + cls.register_adhocs(proc) + + commands = cls.traverse_commands(proc, inherit) + cls.register_commands(proc, commands) + + @classmethod + def sanitize_names(cls, proc): + 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) + try: + delattr(proc, key) + except AttributeError: + pass + + @classmethod + 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): + 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 InternalError("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]: + inherit = adhoc.__dict__.get('INHERIT') + commands.update(dict(cls.traverse_commands(adhoc, inherit))) + 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 + 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. + + 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. + + 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 + 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'(?[\w-]+)(?:(?:=|\s)(\'|")?(?P(?(2)[^-]+?|[^-\s]+))(?(2)\2))?') + + COMMAND_PREFIX = '/' + CASE_SENSITIVE_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 = self.retrieve_command(command_name.group('name')) + if command: + return command + raise AttributeError(name) + + @classmethod + def prepare_name(cls, name): + return name if cls.CASE_SENSITIVE_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("Command does not exist", name=name) + 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 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]] + + 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, opts = [], [] + + 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_args((given_start, given_end)): + """ + Check if something intersects with boundaries of any parsed argument. + """ + 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: + 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_opts(match.span()): + body = match.group('body') + 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 + def adapt_command_arguments(cls, command, arguments, args, opts): + """ + 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. + + 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. + + 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. + + 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() + 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 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: + try: + stopper, (start, end) = args[spec_len - 2] + except IndexError: + raise CommandError("Missing arguments", command) + + 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 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): + if key == letter: + expanded.append(letter) + 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(norm_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 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: + 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 InternalError("Can not have both, extra and *args") + + # 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: + 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. + for index, (key, value) in enumerate(opts): + 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): + """ + 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.strip() + + parts = text.split(' ', 1) + name, arguments = parts if len(parts) > 1 else (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, 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 + value = command(self, *args, **kwargs) + self.command_postprocessor(name, command, arguments, args, kwargs, value) + + 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 + 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 and unprocessed source arguments will be given. + + If empty=True is given - then when raw=True is set and command receives no + arguments - an exception will be raised. + + 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 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 + 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) + + usage = kwargs.get('usage', True) + 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, overlap, 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 include_native: + names.insert(0, command.native_name) + command.names = tuple(names) + + return command + + # 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()) + + return decorator diff --git a/src/commands/implementation.py b/src/commands/implementation.py new file mode 100644 index 000000000..54a13b8a2 --- /dev/null +++ b/src/commands/implementation.py @@ -0,0 +1,262 @@ +# 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. +""" + +import dialogs +from common import gajim +from common import helpers +from common.exceptions import GajimGeneralException + +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. + """ + + @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(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 + 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. + """ + + DISPATCH = True + INHERIT = True + + @command + def ping(self): + """ + Send a ping to the contact + """ + if self.account == gajim.ZEROCONF_ACC_NAME: + raise CommandError(_('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. + """ + + DISPATCH = True + INHERIT = 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. + """ + + DISPATCH = True + INHERIT = 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) + self.new_nick = new_nick + + @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', raw=True) + def message(self, nick, a_message): + """ + Open a private chat window with a specified occupant and send him a + message + """ + 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(raw=True, empty=True) + def invite(self, jid, reason): + """ + Invite a user to a room for a reason + """ + self.connection.send_invite(self.room_jid, jid, reason) + return _("Invited %s to %s") % (jid, self.room_jid) + + @command(raw=True, empty=True) + def join(self, jid, nick): + """ + Join a group chat given by a jid, optionally using given nickname + """ + if not nick: + nick = 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(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. + """ + 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 or str()) + + @command(raw=True, empty=True) + def kick(self, who, reason): + """ + Kick user by a nick from a groupchat + """ + 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 or str()) + + @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 new file mode 100644 index 000000000..ad4fcbf9e --- /dev/null +++ b/src/commands/middleware.py @@ -0,0 +1,105 @@ +# 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 common import gajim + +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) + + @property + def connection(self): + """ + Get the current connection object. + """ + return gajim.connections[self.account] diff --git a/src/common/config.py b/src/common/config.py index 6482d7516..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 ], @@ -249,6 +248,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.')], @@ -285,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.py b/src/common/connection.py index d58c6aae5..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 @@ -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 @@ -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: diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 24b4a81bb..85afe8947 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -900,8 +900,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 @@ -2355,13 +2355,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: @@ -2617,9 +2618,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/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/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/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: 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/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) diff --git a/src/dialogs.py b/src/dialogs.py index 26fb0da10..9b1abfec8 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() 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() diff --git a/src/gajim.py b/src/gajim.py index c922f4df2..5a55dfc8f 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 @@ -229,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 @@ -243,6 +253,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 @@ -1507,10 +1518,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,)) @@ -3048,6 +3064,93 @@ 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() + 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 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(): @@ -3603,6 +3706,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/groupchat_control.py b/src/groupchat_control.py index 754c20df9..4cecf8c97 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,12 +185,10 @@ class PrivateChatControl(ChatControl): self.session.negotiate_e2e(False) -class GroupchatControl(ChatControlBase): +class GroupchatControl(ChatControlBase, GroupChatCommands): 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'] + + DISPATCHED_BY = GroupChatCommands def __init__(self, parent_win, contact, acct, is_continued=False): ChatControlBase.__init__(self, self.TYPE_ID, parent_win, @@ -281,7 +284,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,267 +1512,14 @@ 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): + 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: @@ -1778,79 +1527,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 +1782,13 @@ 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 + # HACK: Not the best soltution. + if (text.startswith(self.COMMAND_PREFIX) and not + 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) # nick completion # check if tab is pressed with empty message diff --git a/src/message_window.py b/src/message_window.py index 7d227206c..9e4bc725c 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/roster_window.py b/src/roster_window.py index b9358f07b..5e1f33221 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 @@ -1736,64 +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 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 @@ -2189,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) @@ -2634,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. ''' @@ -2886,8 +2844,15 @@ 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): + contact=None): if contact is None: dialogs.SingleMessageWindow(account, action='send') elif isinstance(contact, list): @@ -2937,7 +2902,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_: @@ -3243,8 +3208,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''' @@ -3354,21 +3337,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 @@ -5919,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) @@ -6190,7 +6173,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 +6393,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: diff --git a/src/session.py b/src/session.py index 6f5e0627c..f23f3fe9f 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 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'