diff --git a/launch.sh b/launch.sh index bcb968553..c7c361f3d 100755 --- a/launch.sh +++ b/launch.sh @@ -1,3 +1,3 @@ #!/bin/sh cd "$(dirname $0)/src" -exec python -OOt gajim.py $@ +exec python -Ot gajim.py $@ diff --git a/src/chat_control.py b/src/chat_control.py index 6bf451ff9..dc259dfbe 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -51,6 +51,8 @@ from common.pep import MOODS, ACTIVITIES from common.xmpp.protocol import NS_XHTML, NS_XHTML_IM, NS_FILE, NS_MUC from common.xmpp.protocol import NS_RECEIPTS, NS_ESESSION +from commands.implementation import CommonCommands, ChatCommands + try: import gtkspell HAS_GTK_SPELL = True @@ -76,9 +78,12 @@ if gajim.config.get('use_speller') and HAS_GTK_SPELL: del langs[lang] ################################################################################ -class ChatControlBase(MessageControl): +class ChatControlBase(MessageControl, CommonCommands): '''A base class containing a banner, ConversationTextview, MessageTextView ''' + + DISPATCHED_BY = CommonCommands + def make_href(self, match): url_color = gajim.config.get('urlmsgcolor') return '%s' % (match.group(), @@ -605,12 +610,15 @@ class ChatControlBase(MessageControl): def send_message(self, message, keyID='', type_='chat', chatstate=None, msg_id=None, composing_xep=None, resource=None, - xhtml=None, callback=None, callback_args=[]): + xhtml=None, callback=None, callback_args=[], process_commands=True): '''Send the given message to the active tab. Doesn't return None if error ''' if not message or message == '\n': return None + if process_commands and self.process_as_command(message): + return + MessageControl.send_message(self, message, keyID, type_=type_, chatstate=chatstate, msg_id=msg_id, composing_xep=composing_xep, resource=resource, user_nick=self.user_nick, xhtml=xhtml, @@ -1104,11 +1112,13 @@ class ChatControlBase(MessageControl): # FIXME: Set sensitivity for toolbar ################################################################################ -class ChatControl(ChatControlBase): +class ChatControl(ChatControlBase, ChatCommands): '''A control for standard 1-1 chat''' TYPE_ID = message_control.TYPE_CHAT old_msg_kind = None # last kind of the printed message + DISPATCHED_BY = ChatCommands + def __init__(self, parent_win, contact, acct, session, resource = None): ChatControlBase.__init__(self, self.TYPE_ID, parent_win, 'chat_child_vbox', contact, acct, resource) @@ -1696,7 +1706,8 @@ class ChatControl(ChatControlBase): elif self.session and self.session.enable_encryption: dialogs.ESessionInfoWindow(self.session) - def send_message(self, message, keyID='', chatstate=None, xhtml=None): + def send_message(self, message, keyID='', chatstate=None, xhtml=None, + process_commands=True): '''Send a message to contact''' if message in ('', None, '\n'): return None @@ -1760,7 +1771,8 @@ class ChatControl(ChatControlBase): ChatControlBase.send_message(self, message, keyID, type_='chat', chatstate=chatstate_to_send, composing_xep=composing_xep, xhtml=xhtml, callback=_on_sent, - callback_args=[contact, message, encrypted, xhtml]) + callback_args=[contact, message, encrypted, xhtml], + process_commands=process_commands) def check_for_possible_paused_chatstate(self, arg): ''' did we move mouse of that window or write something in message diff --git a/src/commands/__init__.py b/src/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/commands/custom.py b/src/commands/custom.py new file mode 100644 index 000000000..84827c5b0 --- /dev/null +++ b/src/commands/custom.py @@ -0,0 +1,88 @@ +# Copyright (C) 2009 red-agent +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +This module contains examples of how to create your own commands by creating an +adhoc command processor. Each adhoc command processor should be hosted by one or +more which dispatch the real deal and droppped in to where it belongs. +""" + +from framework import command +from implementation import ChatCommands, PrivateChatCommands, GroupChatCommands + +class CustomCommonCommands(ChatCommands, PrivateChatCommands, GroupChatCommands): + """ + This adhoc processor will be hosted by a multiple processors which dispatch + commands from all, chat, private chat and group chat. So commands defined + here will be available to all of them. + """ + + IS_COMMAND_PROCESSOR = True + HOSTED_BY = ChatCommands, PrivateChatCommands, GroupChatCommands + + @command + def dance(self): + """ + First line of the doc string is called a description and will be + programmatically extracted. + + After that you can give more help, like explanation of the options. This + one will be programatically extracted and formatted too. After this one + there will be autogenerated (based on the method signature) usage + information appended. You can turn it off though, if you want. + """ + return "I can't dance, you stupid fuck, I'm just a command system! A cool one, though..." + +class CustomChatCommands(ChatCommands): + """ + This adhoc processor will be hosted by a ChatCommands processor which + dispatches commands from a chat. So commands defined here will be available + only to a chat. + """ + + IS_COMMAND_PROCESSOR = True + HOSTED_BY = ChatCommands + + @command + def sing(self): + return "Are you phreaking kidding me? Buy yourself a damn stereo..." + +class CustomPrivateChatCommands(PrivateChatCommands): + """ + This adhoc processor will be hosted by a PrivateChatCommands processor which + dispatches commands from a private chat. So commands defined here will be + available only to a private chat. + """ + + IS_COMMAND_PROCESSOR = True + HOSTED_BY = PrivateChatCommands + + @command + def make_coffee(self): + return "What do I look like, you ass? A coffee machine!?" + +class CustomGroupChatCommands(GroupChatCommands): + """ + This adhoc processor will be hosted by a GroupChatCommands processor which + dispatches commands from a group chat. So commands defined here will be + available only to a group chat. + """ + + IS_COMMAND_PROCESSOR = True + HOSTED_BY = GroupChatCommands + + @command + def fetch(self): + return "You should really buy yourself a dog and start torturing it instead of me..." diff --git a/src/commands/framework.py b/src/commands/framework.py new file mode 100644 index 000000000..ff655a56d --- /dev/null +++ b/src/commands/framework.py @@ -0,0 +1,610 @@ +# Copyright (C) 2009 red-agent +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Provides a tiny framework with simple, yet powerful and extensible architecture +to implement commands in a streight and flexible, declarative way. +""" + +import re +from types import FunctionType, UnicodeType, TupleType, ListType +from inspect import getargspec + +class CommandInternalError(Exception): + pass + +class Command(object): + + DOC_STRIP_PATTERN = re.compile(r'(?:^[ \t]+|\A\n)', re.MULTILINE) + DOC_FORMAT_PATTERN = re.compile(r'(?" % ', '.join(self.names) + + def __cmp__(self, other): + """ + Comparison is implemented based on a first name. + """ + return cmp(self.first_name, other.first_name) + + @property + def first_name(self): + return self.names[0] + + @property + def native_name(self): + return self.handler.__name__ + + def extract_doc(self): + """ + Extract handler's doc-string and transform it to a usable format. + """ + doc = self.handler.__doc__ or None + + if not doc: + return + + doc = re.sub(self.DOC_STRIP_PATTERN, str(), doc) + doc = re.sub(self.DOC_FORMAT_PATTERN, ' ', doc) + + return doc + + def extract_description(self): + """ + Extract handler's description (which is a first line of the doc). Try to + keep them simple yet meaningful. + """ + doc = self.extract_doc() + if doc: + return doc.split('\n', 1)[0] + + def extract_arg_spec(self): + names, var_args, var_kwargs, defaults = getargspec(self.handler) + + # Behavior of this code need to be checked. Might yield incorrect + # results on some rare occasions. + spec_args = names[:-len(defaults) if defaults else len(names)] + spec_kwargs = dict(zip(names[-len(defaults):], defaults)) if defaults else {} + + # Removing self from arguments specification in case if command handler + # is an instance method. + if self.is_instance and spec_args.pop(0) != 'self': + raise CommandInternalError("Invalid arguments specification") + + return spec_args, spec_kwargs, var_args, var_kwargs + + def extract_arg_usage(self, complete=True): + """ + Extract handler's arguments specification and wrap them in a + human-readable format. If complete is given - then ARG_USAGE_PATTERN + will be used to render it completly. + """ + names, _var_args, _var_kwargs, defaults = getargspec(self.handler) + spec_args, spec_kwargs, var_args, var_kwargs = self.extract_arg_spec() + + '__arguments__' not in spec_args or spec_args.remove('__arguments__') + + optional = '__optional__' in spec_args + if optional: + spec_args.remove('__optional__') + + kwargs = [] + letters = [] + + # The reason we don't iterate here through spec_kwargs, like we would + # normally do is that it does not retains order of items. We need to be + # sure that arguments will be printed in the order they were specified. + for key in (names[-len(defaults):] if defaults else ()): + value = spec_kwargs[key] + letter = key[0] + + if self.dashes: + key = key.replace('_', '-') + + if letter not in letters: + kwargs.append('-(-%s)%s=%s' % (letter, key[1:], value)) + letters.append(letter) + else: + kwargs.append('--%s=%s' % (key, value)) + + usage = str() + args = str() + + if len(spec_args) == 1 and self.raw: + args += ('(|%s|)' if self.empty else '|%s|') % spec_args[0] + elif spec_args or var_args or optional: + if spec_args: + args += '<%s>' % ', '.join(spec_args) + if var_args or optional: + args += (' ' if spec_args else str()) + '<<%s>>' % (var_args or self.optional) + + usage += args + + if kwargs or var_kwargs: + if kwargs: + usage += (' ' if args else str()) + '[%s]' % ', '.join(kwargs) + if var_kwargs: + usage += (' ' if args else str()) + '[[%s]]' % var_kwargs + + # Native name will be the first one if it is included. Otherwise, names + # will be in the order they were specified. + if len(self.names) > 1: + names = '%s (%s)' % (self.first_name, ', '.join(self.names[1:])) + else: + names = self.first_name + + return usage if not complete else self.ARG_USAGE_PATTERN % (names, usage) + +class CommandError(Exception): + def __init__(self, command, *args, **kwargs): + if isinstance(command, Command): + self.command = command + self.name = command.first_name + self.name = command + super(Exception, self).__init__(*args, **kwargs) + +class Dispatcher(type): + table = {} + hosted = {} + + def __init__(cls, name, bases, dct): + dispatchable = Dispatcher.check_if_dispatchable(bases, dct) + hostable = Dispatcher.check_if_hostable(bases, dct) + + if Dispatcher.is_suitable(cls, dct): + Dispatcher.register_processor(cls) + + # Sanitize names even if processor is not suitable for registering, + # because it might be inherited by an another processor. + Dispatcher.sanitize_names(cls) + + super(Dispatcher, cls).__init__(name, bases, dct) + + @classmethod + def is_suitable(cls, proc, dct): + is_not_root = dct.get('__metaclass__') is not cls + is_processor = bool(dct.get('IS_COMMAND_PROCESSOR')) + return is_not_root and is_processor + + @classmethod + def check_if_dispatchable(cls, bases, dct): + dispatcher = dct.get('DISPATCHED_BY') + if not dispatcher: + return False + if dispatcher not in bases: + raise CommandInternalError("Should be dispatched by the same processor it inherits from") + return True + + @classmethod + def check_if_hostable(cls, bases, dct): + hosters = dct.get('HOSTED_BY') + if not hosters: + return False + if not isinstance(hosters, (TupleType, ListType)): + hosters = (hosters,) + for hoster in hosters: + if hoster not in bases: + raise CommandInternalError("Should be hosted by the same processors it inherits from") + return True + + @classmethod + def check_if_conformed(cls, dispatchable, hostable): + if dispatchable and hostable: + raise CommandInternalError("Processor can not be dispatchable and hostable at the same time") + + @classmethod + def register_processor(cls, proc): + cls.table[proc] = {} + inherited = proc.__dict__.get('INHERITED') + + if 'HOSTED_BY' in proc.__dict__: + cls.register_adhocs(proc) + + commands = cls.traverse_commands(proc, inherited) + cls.register_commands(proc, commands) + + @classmethod + def sanitize_names(cls, proc): + inherited = proc.__dict__.get('INHERITED') + commands = cls.traverse_commands(proc, inherited) + for key, command in commands: + if not proc.SAFE_NAME_SCAN_PATTERN.match(key): + setattr(proc, proc.SAFE_NAME_SUBS_PATTERN % key, command) + try: + delattr(proc, key) + except AttributeError: + pass + + @classmethod + def traverse_commands(cls, proc, inherited=True): + keys = dir(proc) if inherited else proc.__dict__.iterkeys() + for key in keys: + value = getattr(proc, key) + if isinstance(value, Command): + yield key, value + + @classmethod + def register_commands(cls, proc, commands): + for key, command in commands: + for name in command.names: + name = proc.prepare_name(name) + if name not in cls.table[proc]: + cls.table[proc][name] = command + else: + raise CommandInternalError("Command with name %s already exists" % name) + @classmethod + def register_adhocs(cls, proc): + hosters = proc.HOSTED_BY + if not isinstance(hosters, (TupleType, ListType)): + hosters = (hosters,) + for hoster in hosters: + if hoster in cls.hosted: + cls.hosted[hoster].append(proc) + else: + cls.hosted[hoster] = [proc] + + @classmethod + def retrieve_command(cls, proc, name): + command = cls.table[proc.DISPATCHED_BY].get(name) + if command: + return command + if proc.DISPATCHED_BY in cls.hosted: + for adhoc in cls.hosted[proc.DISPATCHED_BY]: + command = cls.table[adhoc].get(name) + if command: + return command + + @classmethod + def list_commands(cls, proc): + commands = dict(cls.traverse_commands(proc.DISPATCHED_BY)) + if proc.DISPATCHED_BY in cls.hosted: + for adhoc in cls.hosted[proc.DISPATCHED_BY]: + inherited = adhoc.__dict__.get('INHERITED') + commands.update(dict(cls.traverse_commands(adhoc, inherited))) + return commands.values() + +class CommandProcessor(object): + """ + A base class for a drop-in command processor which you can drop (make your + class to inherit from it) in any of your classes to support commands. In + order to get it done you need to make your own processor, inheriter from + CommandProcessor and then drop it in. Don't forget about few important steps + described below. + + Every command in the processor (normally) will gain full access through self + to an object you are adding commands to. + + Your subclass, which will contain commands should define in its body + IS_COMMAND_PROCESSOR = True in order to be included in the dispatching + table. + + Every class you will drop the processor in should define DISPATCHED_BY set + to the same processor you are inheriting from. + + Names of the commands after preparation stuff id done will be sanitized + (based on SAFE_NAME_SCAN_PATTERN and SAFE_NAME_SUBS_PATTERN) in order not to + interfere with the methods defined in a class you will drop a processor in. + + If you want to create an adhoc processor (then one that parasites on the + other one (the host), so it does not have to be included directly into + whatever includes the host) you need to inherit you processor from the host + and set HOSTED_BY to that host. + + INHERITED controls whether commands inherited from base classes (which could + include other processors) will be registered or not. This is disabled + by-default because it leads to unpredictable consequences when used in adhoc + processors which inherit from more then one processor or has such processors + in its inheritance tree. In that case - encapsulation is being broken and + some (all) commands are shared between non-related processors. + """ + __metaclass__ = Dispatcher + + SAFE_NAME_SCAN_PATTERN = re.compile(r'_(?P\w+)_') + SAFE_NAME_SUBS_PATTERN = '_%s_' + + # Quite complex piece of regular expression logic. + ARG_PATTERN = re.compile(r'(\'|")?(?P(?(1).+?|\S+))(?(1)\1)') + OPT_PATTERN = re.compile(r'--?(?P[\w-]+)(?:(?:=|\s)(\'|")?(?P(?(2)[^-]+?|[^-\s]+))(?(2)\2))?') + + EXPAND_SHORT_OPTIONS = True + + COMMAND_PREFIX = '/' + CASE_SENVITIVE_COMMANDS = False + + ARG_ENCODING = 'utf8' + + def __getattr__(self, name): + """ + This allows to reach and directly (internally) call commands which are + defined in (other) adhoc processors. + """ + command_name = self.SAFE_NAME_SCAN_PATTERN.match(name) + if command_name: + command = Dispatcher.retrieve_command(self, command_name.group('name')) + if command: + return command + raise AttributeError(name) + return super(CommandProcessor, self).__getattr__(name) + + @classmethod + def prepare_name(cls, name): + return name if cls.CASE_SENVITIVE_COMMANDS else name.lower() + + @classmethod + def retrieve_command(cls, name): + name = cls.prepare_name(name) + command = Dispatcher.retrieve_command(cls, name) + if not command: + raise CommandError(name, "Command does not exist") + return command + + @classmethod + def list_commands(cls): + commands = Dispatcher.list_commands(cls) + return sorted(set(commands)) + + @classmethod + def parse_command_arguments(cls, arguments): + """ + Simple yet effective and sufficient in most cases parser which parses + command arguments and maps them to *args and **kwargs, which we all use + extensivly in daily Python coding. + + The format of the input arguments should be: + <> [-(-o)ption=value1, -(-a)nother=value2] [[extra_options]] + + Options may be given in --long or -short format. As --option=value or + --option value or -option value. Keys without values will get True as + value. Arguments and option values that contain spaces may be given as + 'one two three' or "one two three"; that is between single or double + quotes. + """ + args, kwargs = [], {} + + # Need to store every option we have parsed in order to get arguments + # to be parsed correct later. + options = [] + + def intersects((given_start, given_end)): + """ + Check if something intersects with boundaries of any parsed options. + """ + for start, end in options: + if given_start == start or given_end == end: + return True + return False + + for match in re.finditer(cls.OPT_PATTERN, arguments): + if match: + options.append(match.span()) + kwargs[match.group('key')] = match.group('value') or True + + for match in re.finditer(cls.ARG_PATTERN, arguments): + if match and not intersects(match.span()): + args.append(match.group('body')) + + return args, kwargs + + @classmethod + def adapt_command_arguments(cls, command, arguments, args, kwargs): + """ + Adapts *args and **kwargs got from a parser to a specific handler by + means of arguments specified on command definition. + + When EXPAND_SHORT_OPTIONS is set then if command receives one-latter + options (like -v or -f) they will be expanded to a verbose ones (like + --verbose or --file) if the latter are defined as a command optional + argumens. Expansion is made on a first-latter comparison basis. If more + then one long option with the same first letter defined - only first one + will be used in expanding. + + If command defines __arguments__ as a first argument - then this + argument will receive raw and unprocessed arguments. Also, if nothing + except __arguments__ (including *args, *kwargs splatting) is defined - + then all parsed arguments will be discarded. It will be discarded in the + argument usage information. + + If command defines __optional__ - that is an analogue for *args, to + collect extra arguments. This is a preffered way over *args. Because of + some Python limitations, *args could not be mapped to as expected. And + it is hardly advised to define it after all hard arguments. + + Extra arguments which are not considered extra (or optional) - will be + passed as if they were value for keywords, in the order keywords are + defined and printed in usage. + """ + spec_args, spec_kwargs, var_args, var_kwargs = command.extract_arg_spec() + + if command.raw: + if len(spec_args) == 1: + if arguments or command.empty: + return (arguments,), {} + raise CommandError(command, "Can not be used without arguments") + raise CommandInternalError("Raw command must define no more then one argument") + + if '__optional__' in spec_args: + if not var_args: + hard_len = len(spec_args) - 1 + optional = args[hard_len:] + args = args[:hard_len] + args.insert(spec_args.index('__optional__'), optional) + raise CommandInternalError("Cant have both, __optional__ and *args") + + if command.dashes: + for key, value in kwargs.items(): + if '-' in key: + del kwargs[key] + kwargs[key.replace('-', '_')] = value + + if cls.EXPAND_SHORT_OPTIONS: + expanded = [] + for key, value in spec_kwargs.iteritems(): + letter = key[0] if len(key) > 1 else None + if letter and letter in kwargs and letter not in expanded: + expanded.append(letter) + kwargs[key] = kwargs[letter] + del kwargs[letter] + + # We need to encode every keyword argument to a simple string, not the + # unicode one, because ** expanding does not support it. The nasty issue + # here to consider is that if dict key was initially set as u'test', + # then resetting it to just 'test' leaves u'test' as it was... + for key, value in kwargs.items(): + if isinstance(key, UnicodeType): + del kwargs[key] + kwargs[key.encode(cls.ARG_ENCODING)] = value + + if '__arguments__' in spec_args: + if len(spec_args) == 1 and not spec_kwargs and not var_args and not var_kwargs: + return (arguments,), {} + args.insert(spec_args.index('__arguments__'), arguments) + + return args, kwargs + + def process_as_command(self, text): + """ + Try to process text as a command. Returns True if it is a command and + False if it is not. + """ + if not text.startswith(self.COMMAND_PREFIX): + return False + + text = text[len(self.COMMAND_PREFIX):] + text = text.lstrip() + + parts = text.split(' ', 1) + + if len(parts) > 1: + name, arguments = parts + else: + name, arguments = parts[0], None + + flag = self.looks_like_command(text, name, arguments) + if flag is not None: + return flag + + self.execute_command(name, arguments) + + return True + + def execute_command(self, name, arguments): + command = self.retrieve_command(name) + + args, kwargs = self.parse_command_arguments(arguments) if arguments else ([], {}) + args, kwargs = self.adapt_command_arguments(command, arguments, args, kwargs) + + try: + if self.command_preprocessor(name, command, arguments, args, kwargs): + return + value = command(self, *args, **kwargs) + self.command_postprocessor(name, command, arguments, args, kwargs, value) + except TypeError: + raise CommandError(name, "Command received invalid arguments") + + def command_preprocessor(self, name, command, arguments, args, kwargs): + """ + Redefine this method in the subclass to execute custom code before + command gets executed. If returns True then command execution will be + interrupted and command will not be executed. + """ + pass + + def command_postprocessor(self, name, command, arguments, args, kwargs, output): + """ + Redefine this method in the subclass to execute custom code after + command gets executed. + """ + pass + + def looks_like_command(self, text, name, arguments): + """ + This hook is being called before any processing, but after it was + determined that text looks like a command. If returns non None value + - then further processing will be interrupted and that value will be + used to return from process_as_command. + """ + pass + +def command(*names, **kwargs): + """ + A decorator which provides a declarative way of defining commands. + + You can specify a set of names by which you can call the command. If names + are empty - then the name of the command will be set to native (extracted + from the handler name). If no_native=True argument is given and names is + non-empty - then native name will not be added. + + If command handler is not an instance method then is_instance=False should + be given. Though mentioned case is not covered by defined behaviour, and + should not be used, unless you know what you are doing. + + If usage=True is given - then handler's doc will be appended with an + auto-gereated usage info. + + If raw=True is given then command should define only one argument to + which all raw, unprocessed command arguments will be given. + + If dashes=True is given, then dashes (-) in the option + names will be converted to underscores. So you can map --one-more-option to + a one_more_option=None. + + If optional is set to a string then if __optional__ specified - its name + ('optional' by-default) in the usage info will be substitued by whatever is + given. + + If empty=True is given - then if raw is enabled it will allow to pass empty + (None) raw arguments to a command. + """ + names = list(names) + + no_native = kwargs.get('no_native', False) + is_instance = kwargs.get('is_instance', True) + usage = kwargs.get('usage', True) + raw = kwargs.get('raw', False) + dashes = kwargs.get('dashes', True) + optional = kwargs.get('optional', 'optional') + empty = kwargs.get('empty', False) + + def decorator(handler): + command = Command(handler, is_instance, usage, raw, dashes, optional, empty) + + # Extract and inject native name while making sure it is going to be the + # first one in the list. + if not names or names and not no_native: + names.insert(0, command.native_name) + command.names = tuple(names) + + return command + + # Workaround if we are getting called without parameters. + if len(names) == 1 and isinstance(names[0], FunctionType): + return decorator(names.pop()) + + return decorator diff --git a/src/commands/implementation.py b/src/commands/implementation.py new file mode 100644 index 000000000..a921116e7 --- /dev/null +++ b/src/commands/implementation.py @@ -0,0 +1,124 @@ +# Copyright (C) 2009 red-agent +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Provides an actual implementation of the standard commands. +""" + +from common import gajim + +from framework import command, CommandError +from middleware import ChatMiddleware + +class CommonCommands(ChatMiddleware): + """ + Here defined commands will be common to all, chat, private chat and group + chat. Keep in mind that self is set to an instance of either ChatControl, + PrivateChatControl or GroupchatControl when command is being called. + """ + + IS_COMMAND_PROCESSOR = True + + @command + def clear(self): + """ + Clear the text window + """ + self.conv_textview.clear() + + @command + def compact(self): + """ + Hide the chat buttons + """ + self.chat_buttons_set_visible(not self.hide_chat_buttons) + + @command + def help(self, command=None, all=False): + """ + Show help on a given command or a list of available commands if -(-a)ll is + given + """ + if command: + command = self.retrieve_command(command) + + doc = _(command.extract_doc()) + usage = command.extract_arg_usage() + + if doc: + return (doc + '\n\n' + usage) if command.usage else doc + else: + return usage + elif all: + for command in self.list_commands(): + names = ', '.join(command.names) + description = command.extract_description() + + self.echo("%s - %s" % (names, description)) + else: + self.echo(self._help_(self, 'help')) + + @command(raw=True) + def say(self, message): + """ + Send a message to the contact + """ + self.send(message) + + @command(raw=True) + def me(self, action): + """ + Send action (in the third person) to the current chat + """ + self.send("/me %s" % action) + +class ChatCommands(CommonCommands): + """ + Here defined commands will be unique to a chat. Use it as a hoster to provide + commands which should be unique to a chat. Keep in mind that self is set to + an instance of ChatControl when command is being called. + """ + + IS_COMMAND_PROCESSOR = True + INHERITED = True + + @command + def ping(self): + """ + Send a ping to the contact + """ + if self.account == gajim.ZEROCONF_ACC_NAME: + raise CommandError(ping, _('Command is not supported for zeroconf accounts')) + gajim.connections[self.account].sendPing(self.contact) + +class PrivateChatCommands(CommonCommands): + """ + Here defined commands will be unique to a private chat. Use it as a hoster to + provide commands which should be unique to a private chat. Keep in mind that + self is set to an instance of PrivateChatControl when command is being called. + """ + + IS_COMMAND_PROCESSOR = True + INHERITED = True + +class GroupChatCommands(CommonCommands): + """ + Here defined commands will be unique to a group chat. Use it as a hoster to + provide commands which should be unique to a group chat. Keep in mind that + self is set to an instance of GroupchatControl when command is being called. + """ + + IS_COMMAND_PROCESSOR = True + INHERITED = True diff --git a/src/commands/middleware.py b/src/commands/middleware.py new file mode 100644 index 000000000..a0db07bdd --- /dev/null +++ b/src/commands/middleware.py @@ -0,0 +1,97 @@ +# Copyright (C) 2009 red-agent +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Provides a glue to tie command system framework and the actual code where it +would be dropped in. Defines a little bit of scaffolding to support interaction +between the two and a few utility methods so you don't need to dig up the host +code to write basic commands. +""" + +from types import StringTypes +from framework import CommandProcessor, CommandError +from traceback import print_exc + +class ChatMiddleware(CommandProcessor): + """ + Provides basic scaffolding for the convenient interaction with ChatControl. + Also provides some few basic utilities for the same purpose. + """ + + def process_as_command(self, text): + try: + return super(ChatMiddleware, self).process_as_command(text) + except CommandError, exception: + self.echo("%s: %s" %(exception.name, exception.message), 'error') + return True + except Exception: + self.echo("An error occured while trying to execute the command", 'error') + print_exc() + return True + finally: + self.add_history(text) + self.clear_input() + + def looks_like_command(self, text, name, arguments): + # Command escape stuff ggoes here. If text was prepended by the command + # prefix twice, like //not_a_command (if prefix is set to /) then it + # will be escaped, that is sent just as a regular message with one (only + # one) prefix removed, so message will be /not_a_command. + if name.startswith(self.COMMAND_PREFIX): + self._say_(self, text) + return True + + def command_preprocessor(self, name, command, arguments, args, kwargs): + if 'h' in kwargs or 'help' in kwargs: + # Forwarding to the /help command. Dont forget to pass self, as + # all commands are unbound. And also don't forget to print output. + self.echo(self._help_(self, name)) + return True + + def command_postprocessor(self, name, command, arguments, args, kwargs, value): + if value and isinstance(value, StringTypes): + self.echo(value) + + def echo(self, text, kind='info'): + """ + Print given text to the user. + """ + self.print_conversation(str(text), kind) + + def send(self, text): + """ + Send a message to the contact. + """ + self.send_message(text, process_commands=False) + + def set_input(self, text): + """ + Set given text into the input. + """ + message_buffer = self.msg_textview.get_buffer() + message_buffer.set_text(text) + + def clear_input(self): + """ + Clear input. + """ + self.set_input(str()) + + def add_history(self, text): + """ + Add given text to the input history, so user can scroll through it + using ctrl + up/down arrow keys. + """ + self.save_sent_message(text) diff --git a/src/gajim.py b/src/gajim.py index b8512ac43..fc7f581f7 100644 --- a/src/gajim.py +++ b/src/gajim.py @@ -230,6 +230,15 @@ from chat_control import ChatControlBase from chat_control import ChatControl from groupchat_control import GroupchatControl from groupchat_control import PrivateChatControl + +# Here custom adhoc processors should be loaded. At this point there is +# everything they need to function properly. The next line loads custom exmple +# adhoc processors. Technically, they could be loaded earlier as host processors +# themself does not depend on the chat controls, but that should not be done +# uless there is a really good reason for that.. +# +# from commands import custom + from atom_window import AtomWindow from session import ChatControlSession diff --git a/src/groupchat_control.py b/src/groupchat_control.py index 72912953c..6b959a5e0 100644 --- a/src/groupchat_control.py +++ b/src/groupchat_control.py @@ -47,6 +47,8 @@ from chat_control import ChatControl from chat_control import ChatControlBase from common.exceptions import GajimGeneralException +from commands.implementation import PrivateChatCommands, GroupChatCommands + import logging log = logging.getLogger('gajim.groupchat_control') @@ -116,9 +118,11 @@ def tree_cell_data_func(column, renderer, model, iter_, tv=None): renderer.set_property('font', gtkgui_helpers.get_theme_font_for_option(theme, 'groupfont')) -class PrivateChatControl(ChatControl): +class PrivateChatControl(ChatControl, PrivateChatCommands): TYPE_ID = message_control.TYPE_PM + DISPATCHED_BY = PrivateChatCommands + def __init__(self, parent_win, gc_contact, contact, account, session): room_jid = contact.jid.split('/')[0] room_ctrl = gajim.interface.msg_win_mgr.get_gc_control(room_jid, account) @@ -132,7 +136,7 @@ class PrivateChatControl(ChatControl): ChatControl.__init__(self, parent_win, contact, account, session) self.TYPE_ID = 'pm' - def send_message(self, message, xhtml=None): + def send_message(self, message, xhtml=None, process_commands=True): '''call this function to send our message''' if not message: return @@ -158,7 +162,8 @@ class PrivateChatControl(ChatControl): 'left.') % {'room': room, 'nick': nick}) return - ChatControl.send_message(self, message, xhtml=xhtml) + ChatControl.send_message(self, message, xhtml=xhtml, + process_commands=process_commands) def update_ui(self): if self.contact.show == 'offline': @@ -180,9 +185,11 @@ class PrivateChatControl(ChatControl): self.session.negotiate_e2e(False) -class GroupchatControl(ChatControlBase): +class GroupchatControl(ChatControlBase, GroupChatCommands): TYPE_ID = message_control.TYPE_GC + DISPATCHED_BY = GroupChatCommands + def __init__(self, parent_win, contact, acct, is_continued=False): ChatControlBase.__init__(self, self.TYPE_ID, parent_win, 'muc_child_vbox', contact, acct) @@ -1505,11 +1512,14 @@ class GroupchatControl(ChatControlBase): if model.iter_n_children(parent_iter) == 0: model.remove(parent_iter) - def send_message(self, message, xhtml=None): + def send_message(self, message, xhtml=None, process_commands=True): '''call this function to send our message''' if not message: return + if process_commands and self.process_as_command(message): + return + message = helpers.remove_invalid_xml_chars(message) if not message: