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: