Dropped in the brand new and shiny command system

This commit is contained in:
red-agent 2009-09-12 16:51:21 +03:00
parent c38e7050f5
commit ae0f32d922
9 changed files with 961 additions and 11 deletions

View File

@ -1,3 +1,3 @@
#!/bin/sh
cd "$(dirname $0)/src"
exec python -OOt gajim.py $@
exec python -Ot gajim.py $@

View File

@ -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 '<a href="%s"><span color="%s">%s</span></a>' % (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

0
src/commands/__init__.py Normal file
View File

88
src/commands/custom.py Normal file
View File

@ -0,0 +1,88 @@
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
"""
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..."

610
src/commands/framework.py Normal file
View File

@ -0,0 +1,610 @@
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
"""
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'(?<!\n)\n(?!\n)', re.MULTILINE)
ARG_USAGE_PATTERN = 'Usage: %s %s'
def __init__(self, handler, is_instance, usage, raw, dashes, optional, empty):
self.handler = handler
self.is_instance = is_instance
self.usage = usage
self.raw = raw
self.dashes = dashes
self.optional = optional
self.empty = empty
def __call__(self, *args, **kwargs):
return self.handler(*args, **kwargs)
def __repr__(self):
return "<Command %s>" % ', '.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<name>\w+)_')
SAFE_NAME_SUBS_PATTERN = '_%s_'
# Quite complex piece of regular expression logic.
ARG_PATTERN = re.compile(r'(\'|")?(?P<body>(?(1).+?|\S+))(?(1)\1)')
OPT_PATTERN = re.compile(r'--?(?P<key>[\w-]+)(?:(?:=|\s)(\'|")?(?P<value>(?(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:
<arg1, arg2> <<optional>> [-(-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

View File

@ -0,0 +1,124 @@
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
"""
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

View File

@ -0,0 +1,97 @@
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
"""
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)

View File

@ -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

View File

@ -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: