diff options
-rw-r--r-- | gajim/command_system/__init__.py | 20 | ||||
-rw-r--r-- | gajim/command_system/dispatcher.py | 134 | ||||
-rw-r--r-- | gajim/command_system/errors.py | 54 | ||||
-rw-r--r-- | gajim/command_system/framework.py | 353 | ||||
-rw-r--r-- | gajim/command_system/implementation/__init__.py | 20 | ||||
-rw-r--r-- | gajim/command_system/implementation/custom.py | 131 | ||||
-rw-r--r-- | gajim/command_system/implementation/execute.py | 136 | ||||
-rw-r--r-- | gajim/command_system/implementation/hosts.py | 45 | ||||
-rw-r--r-- | gajim/command_system/implementation/middleware.py | 161 | ||||
-rw-r--r-- | gajim/command_system/implementation/standard.py | 460 | ||||
-rw-r--r-- | gajim/command_system/mapping.py | 349 | ||||
-rw-r--r-- | gajim/command_system/tools.py | 34 | ||||
-rw-r--r-- | gajim/common/setting_values.py | 5 | ||||
-rw-r--r-- | setup.cfg | 1 |
14 files changed, 0 insertions, 1903 deletions
diff --git a/gajim/command_system/__init__.py b/gajim/command_system/__init__.py deleted file mode 100644 index 2fb336264..000000000 --- a/gajim/command_system/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@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/>. - -""" -The command system providing scalable, clean and convenient architecture -in combination with declarative way of defining commands and a fair -amount of automatization for routine processes. -""" diff --git a/gajim/command_system/dispatcher.py b/gajim/command_system/dispatcher.py deleted file mode 100644 index 4f292dafc..000000000 --- a/gajim/command_system/dispatcher.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright (c) 2010, Alexander Cherniuk (ts33kr@gmail.com) -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -""" -Backbone of the command system. Provides smart and controllable -dispatching mechanism with an auto-discovery functionality. In addition -to automatic discovery and dispatching, also features manual control -over the process. -""" - -from typing import Any - -from gajim.command_system.tools import remove - -COMMANDS: dict[Any, Any] = {} -CONTAINERS: dict[Any, Any] = {} - - -def add_host(host): - CONTAINERS[host] = [] - - -def remove_host(host): - remove(CONTAINERS, host) - - -def add_container(container): - for host in container.HOSTS: - CONTAINERS[host].append(container) - - -def remove_container(container): - for host in container.HOSTS: - remove(CONTAINERS[host], container) - - -def add_commands(container): - commands = COMMANDS.setdefault(container, {}) - for command in traverse_commands(container): - for name in command.names: - commands[name] = command - - -def remove_commands(container): - remove(COMMANDS, container) - - -def traverse_commands(container): - for name in dir(container): - attribute = getattr(container, name) - if is_command(attribute): - yield attribute - - -def is_command(attribute): - from gajim.command_system.framework import Command - return isinstance(attribute, Command) - - -def is_root(namespace): - metaclass = namespace.get("__metaclass__", None) - if not metaclass: - return False - return issubclass(metaclass, Dispatchable) - - -def get_command(host, name): - for container in CONTAINERS[host]: - command = COMMANDS[container].get(name) - if command: - return command - - -def list_commands(host): - for container in CONTAINERS[host]: - commands = COMMANDS[container] - for name, command in commands.items(): - yield name, command - - -class Dispatchable(type): - # pylint: disable=no-value-for-parameter - def __init__(cls, name, bases, namespace): - parents = super(Dispatchable, cls) - parents.__init__(name, bases, namespace) - if not is_root(namespace): - cls.dispatch() - - def dispatch(cls): - if cls.AUTOMATIC: - cls.enable() - - -class Host(Dispatchable): - - def enable(cls): - add_host(cls) - - def disable(cls): - remove_host(cls) - - -class Container(Dispatchable): - - def enable(cls): - add_container(cls) - add_commands(cls) - - def disable(cls): - remove_commands(cls) - remove_container(cls) diff --git a/gajim/command_system/errors.py b/gajim/command_system/errors.py deleted file mode 100644 index 1f02051c0..000000000 --- a/gajim/command_system/errors.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@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/>. - - -class BaseError(Exception): - """ - Common base for errors which relate to a specific command. - Encapsulates everything needed to identify a command, by either its - object or name. - """ - - def __init__(self, message, command=None, name=None): - self.message = message - - self.command = command - self.name = name - - if command and not name: - self.name = command.first_name - - super(BaseError, self).__init__() - - def __str__(self): - return self.message - - -class DefinitionError(BaseError): - """ - Used to indicate errors occurred on command definition. - """ - - -class CommandError(BaseError): - """ - Used to indicate errors occurred during command execution. - """ - - -class NoCommandError(BaseError): - """ - Used to indicate an inability to find the specified command. - """ diff --git a/gajim/command_system/framework.py b/gajim/command_system/framework.py deleted file mode 100644 index a738fca64..000000000 --- a/gajim/command_system/framework.py +++ /dev/null @@ -1,353 +0,0 @@ -# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@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 straight and flexible, -declarative way. -""" - -from types import FunctionType -from inspect import getfullargspec -from inspect import getdoc - -from gajim.command_system.dispatcher import Host -from gajim.command_system.dispatcher import Container -from gajim.command_system.dispatcher import get_command -from gajim.command_system.dispatcher import list_commands -from gajim.command_system.mapping import parse_arguments -from gajim.command_system.mapping import adapt_arguments -from gajim.command_system.errors import DefinitionError -from gajim.command_system.errors import CommandError -from gajim.command_system.errors import NoCommandError - - -class CommandHost(metaclass=Host): - """ - Command host is a hub between numerous command processors and - command containers. Aimed to participate in a dispatching process in - order to provide clean and transparent architecture. - - The AUTOMATIC class variable, which must be defined by a command - host, specifies whether the command host should be automatically - dispatched and enabled by the dispatcher or not. - """ - __metaclass__ = Host - - -class CommandContainer(metaclass=Container): - """ - Command container is an entity which holds defined commands, - allowing them to be dispatched and processed correctly. Each - command container may be bound to a one or more command hosts. - - The AUTOMATIC class variable, which must be defined by a command - processor, specifies whether the command processor should be - automatically dispatched and enabled by the dispatcher or not. - - Bounding is controlled by the HOSTS class variable, which must be - defined by the command container. This variable should contain a - sequence of hosts to bound to, as a tuple or list. - """ - __metaclass__ = Container - - -class CommandProcessor: - """ - Command processor is an immediate command emitter. It does not - participate in the dispatching process directly, but must define a - host to bound to. - - Bounding is controlled by the COMMAND_HOST variable, which must be - defined in the body of the command processor. This variable should - be set to a specific command host. - """ - - # This defines a command prefix (or an initializer), which should - # precede a text in order for it to be processed as a command. - COMMAND_PREFIX = '/' - - def process_as_command(self, text): - """ - Try to process text as a command. Returns True if it has been - processed as a command and False otherwise. - """ - # pylint: disable=assignment-from-no-return - prefix = text.startswith(self.COMMAND_PREFIX) - length = len(text) > len(self.COMMAND_PREFIX) - if not (prefix and length): - return False - - body = text[len(self.COMMAND_PREFIX):] - body = body.strip() - - parts = body.split(None, 1) - name, arguments = parts if len(parts) > 1 else (parts[0], None) - - flag = self.looks_like_command(text, body, name, arguments) - if flag is not None: - return flag - - self.execute_command(name, arguments) - - return True - - def execute_command(self, name, arguments): - cmd = self.get_command(name) - - args, opts = parse_arguments(arguments) if arguments else ([], []) - args, kwargs = adapt_arguments(cmd, arguments, args, opts) - - if self.command_preprocessor(cmd, name, arguments, args, kwargs): - return - value = cmd(self, *args, **kwargs) - self.command_postprocessor(cmd, name, arguments, args, kwargs, value) - - def command_preprocessor(self, cmd, name, 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. - """ - - def command_postprocessor(self, cmd, name, arguments, args, kwargs, value): - """ - Redefine this method in the subclass to execute custom code - after command gets executed. - """ - - def looks_like_command(self, text, body, name, arguments): - """ - This hook is being called before any processing, but after it - was determined that text looks like a command. - - If returns value other then None - then further processing will - be interrupted and that value will be used to return from - process_as_command. - """ - - def get_command(self, name): - cmd = get_command(self.COMMAND_HOST, name) - if not cmd: - raise NoCommandError("Command does not exist", name=name) - return cmd - - def list_commands(self): - commands = list_commands(self.COMMAND_HOST) - commands = dict(commands) - return sorted(set(commands.values()), key=repr) - - -class Command: - - def __init__(self, handler, *names, **properties): - self.handler = handler - self.names = names - - # Automatically set all the properties passed to a constructor - # by the command decorator. - for key, value in properties.items(): - setattr(self, key, value) - - def __call__(self, *args, **kwargs): - try: - return self.handler(*args, **kwargs) - - # This allows to use a shortcut way of raising an exception - # inside a handler. That is to raise a CommandError without - # command or name attributes set. They will be set to a - # corresponding values right here in case if they was not set by - # the one who raised an exception. - except CommandError as error: - if not error.command and not error.name: - raise CommandError(error.message, self) - raise - - # This one is a little bit too wide, but as Python does not have - # anything more constrained - there is no other choice. Take a - # look here if command complains about invalid arguments while - # they are ok. - except TypeError: - raise CommandError("Command received invalid arguments", self) - - def __repr__(self): - return "<Command %s>" % ', '.join(self.names) - - def __cmp__(self, other): - if self.first_name > other.first_name: - return 1 - if self.first_name < other.first_name: - return -1 - return 0 - - @property - def first_name(self): - return self.names[0] - - @property - def native_name(self): - return self.handler.__name__ - - def extract_documentation(self): - """ - Extract handler's documentation which is a doc-string and - transform it to a usable format. - """ - return getdoc(self.handler) - - def extract_description(self): - """ - Extract handler's description (which is a first line of the - documentation). Try to keep them simple yet meaningful. - """ - documentation = self.extract_documentation() - return documentation.split('\n', 1)[0] if documentation else None - - def extract_specification(self): - """ - Extract handler's arguments specification, as it was defined - preserving their order. - """ - names, var_args, var_kwargs, defaults, _, _, _ = getfullargspec( - self.handler) - - # Behavior of this code need to be checked. Might yield - # incorrect results on some rare occasions. - spec_args = names[:-len(defaults) if defaults else len(names)] - spec_kwargs = list( - zip(names[-len(defaults):], defaults)) if defaults else {} - - # Removing self from arguments specification. Command handler - # should receive the processors as a first argument, which - # should be self by the canonical means. - if spec_args.pop(0) != 'self': - raise DefinitionError("First argument must be self", self) - - return spec_args, spec_kwargs, var_args, var_kwargs - - -def command(*names, **properties): - """ - A decorator for defining commands in a declarative way. Provides - facilities for setting command's names and properties. - - Names should contain a set of names (aliases) by which the command - can be reached. If no names are given - the native name (the one - extracted from the command handler) will be used. - - If native=True is given (default) and names is non-empty - then the - native name of the command will be prepended in addition to the - given names. - - If usage=True is given (default) - then command help will be - appended with autogenerated usage info, based of the command handler - arguments introspection. - - If source=True is given - then the first argument of the command - will receive the source arguments, as a raw, unprocessed string. The - further mapping of arguments and options will not be affected. - - If raw=True is given - then command considered to be raw and should - define positional arguments only. If it defines only one positional - argument - this argument will receive all the raw and unprocessed - arguments. If the command defines more then one positional argument - - then all the arguments except the last one will be processed - normally; the last argument will get what is left after the - processing as raw and unprocessed string. - - If empty=True is given - this will allow to call a raw command - without arguments. - - If extra=True is given - then all the extra arguments passed to a - command will be collected into a sequence and given to the last - positional argument. - - If overlap=True is given - then all the extra arguments will be - mapped as if they were values for the keyword arguments. - - If expand=True is given (default) - then short, one-letter options - will be expanded to a verbose ones, based of the comparison of the - first letter. If more then one option with the same first letter is - given - then only first one will be used in the expansion. - """ - names = list(names) - - native = properties.get('native', True) - - usage = properties.get('usage', True) - source = properties.get('source', False) - raw = properties.get('raw', False) - empty = properties.get('empty', False) - extra = properties.get('extra', False) - overlap = properties.get('overlap', False) - expand = properties.get('expand', True) - - if empty and not raw: - raise DefinitionError("Empty option can be used only with raw commands") - - if extra and overlap: - raise DefinitionError("Extra and overlap options can not be used " - "together") - - properties = { - 'usage': usage, - 'source': source, - 'raw': raw, - 'extra': extra, - 'overlap': overlap, - 'empty': empty, - 'expand': expand - } - - def decorator(handler): - """ - Decorator which receives handler as a first argument and then - wraps it in the command which then returns back. - """ - cmd = Command(handler, *names, **properties) - - # Extract and inject a native name if either no other names are - # specified or native property is enabled, while making - # sure it is going to be the first one in the list. - if not names or native: - names.insert(0, cmd.native_name) - cmd.names = tuple(names) - - return cmd - - # Workaround if we are getting called without parameters. Keep in - # mind that in that case - first item in the names will be the - # handler. - if names and isinstance(names[0], FunctionType): - return decorator(names.pop(0)) - - return decorator - - -def doc(text): - """ - This decorator is used to bind a documentation (a help) to a - command. - """ - def decorator(target): - if isinstance(target, Command): - target.handler.__doc__ = text - else: - target.__doc__ = text - return target - - return decorator diff --git a/gajim/command_system/implementation/__init__.py b/gajim/command_system/implementation/__init__.py deleted file mode 100644 index 1c3d8ab65..000000000 --- a/gajim/command_system/implementation/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@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/>. - -""" -The implementation and auxiliary systems which implement the standard -Gajim commands and also provide an infrastructure for adding custom -commands. -""" diff --git a/gajim/command_system/implementation/custom.py b/gajim/command_system/implementation/custom.py deleted file mode 100644 index af2b9e8c6..000000000 --- a/gajim/command_system/implementation/custom.py +++ /dev/null @@ -1,131 +0,0 @@ -# Copyright (c) 2009-2010, Alexander Cherniuk (ts33kr@gmail.com) -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -""" -This module contains examples of how to create your own commands, by -creating a new command container, bounded to a specific command host, -and defining a set of commands inside of it. - -Keep in mind that this module is not being loaded from anywhere, so the -code in here will not be executed and commands defined here will not be -detected. -""" - -from gajim.common.i18n import _ -from gajim.command_system.framework import CommandContainer -from gajim.command_system.framework import command -from gajim.command_system.framework import doc -from gajim.command_system.implementation.hosts import ChatCommands -from gajim.command_system.implementation.hosts import PrivateChatCommands -from gajim.command_system.implementation.hosts import GroupChatCommands - - -class CustomCommonCommands(CommandContainer): - """ - The AUTOMATIC class variable, set to a positive value, instructs the - command system to automatically discover the command container and - enable it. - - This command container bounds to all three available in the default - implementation command hosts. This means that commands defined in - this container will be available to all: chat, private chat and a - group chat. - """ - - AUTOMATIC = True - HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands - - @command - def dance(self): - """ - First line of the doc string is called a description and will be - programmatically extracted and formatted. - - After that you can give more help, like explanation of the - options. This one will be programmatically extracted and - formatted too. - - After all the documentation - there will be autogenerated (based - on the method signature) usage information appended. You can - turn it off, if you want. - """ - return "I don't dance." - - -class CustomChatCommands(CommandContainer): - """ - This command container bounds only to the ChatCommands command host. - Therefore commands defined inside of the container will be available - only to a chat. - """ - - AUTOMATIC = True - HOSTS = (ChatCommands,) - - @command("squal", "bawl") - def sing(self): - """ - This command has an additional aliases. It means the command will - be available under three names: sing (the native name), squal - (the first alias), bawl (the second alias). - - You can turn off the usage of the native name, if you want, and - specify a name or a set of names, as aliases, under which a - command will be available. - """ - return "Buy yourself a stereo." - - -class CustomPrivateChatCommands(CommandContainer): - """ - This command container bounds only to the PrivateChatCommands - command host. Therefore commands defined inside of the container - will be available only to a private chat. - """ - - AUTOMATIC = True - HOSTS = (PrivateChatCommands,) - - @command - #Example string. Do not translate - @doc(_("The same as using a doc-string, except it supports translation")) - def make_coffee(self): - return "I'm not a coffee machine!" - - -class CustomGroupChatCommands(CommandContainer): - """ - This command container bounds only to the GroupChatCommands command - host. Therefore commands defined inside of the container will be - available only to a group chat. - """ - - AUTOMATIC = True - HOSTS = (GroupChatCommands,) - - @command - def fetch(self): - return "Buy yourself a dog." diff --git a/gajim/command_system/implementation/execute.py b/gajim/command_system/implementation/execute.py deleted file mode 100644 index 58205e768..000000000 --- a/gajim/command_system/implementation/execute.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright (c) 2010, Alexander Cherniuk (ts33kr@gmail.com) -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -""" -Provides facilities to safely execute expressions inside a shell process -and capture the resulting output, in an asynchronous fashion, avoiding -deadlocks. If the process execution time reaches the threshold - it is -forced to terminate. Consists of a tiny framework and a couple of -commands as a frontend. -""" - -from subprocess import Popen, PIPE -from os.path import expanduser - -from gi.repository import GLib - -from gajim.common import app -from gajim.common.i18n import _ -from gajim.command_system.framework import CommandContainer -from gajim.command_system.framework import command -from gajim.command_system.framework import doc -from gajim.command_system.implementation.hosts import ChatCommands -from gajim.command_system.implementation.hosts import PrivateChatCommands -from gajim.command_system.implementation.hosts import GroupChatCommands - - -class Execute(CommandContainer): - AUTOMATIC = True - HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands - - DIRECTORY = "~" - - POLL_INTERVAL = 100 - POLL_COUNT = 5 - - @command("exec", raw=True) - @doc(_("Execute expression inside a shell, show output")) - def execute(self, expression): - Execute.spawn(self, expression) - - @classmethod - def spawn(cls, processor, expression): - command_system_execute = app.settings.get('command_system_execute') - if command_system_execute: - pipes = dict(stdout=PIPE, stderr=PIPE) - directory = expanduser(cls.DIRECTORY) - popen = Popen(expression, shell=True, cwd=directory, **pipes) - cls.monitor(processor, popen) - else: - processor.echo_error( - _('Command disabled. This command can be enabled by ' - 'setting \'command_system_execute\' to True in ACE ' - '(Advanced Configuration Editor).')) - return - - @classmethod - def monitor(cls, processor, popen): - poller = cls.poller(processor, popen) - GLib.timeout_add(cls.POLL_INTERVAL, next, poller) - - @classmethod - def poller(cls, processor, popen): - for _ in range(cls.POLL_COUNT): - yield cls.brush(processor, popen) - cls.overdue(processor, popen) - yield False - - @classmethod - def brush(cls, processor, popen): - if popen.poll() is not None: - cls.terminated(processor, popen) - return False - return True - - @classmethod - def terminated(cls, processor, popen): - stdout, stderr = cls.fetch(popen) - success = popen.returncode == 0 - if success and stdout: - processor.echo(stdout) - elif not success and stderr: - processor.echo_error(stderr) - - @classmethod - def overdue(cls, processor, popen): - popen.terminate() - - @classmethod - def fetch(cls, popen): - data = popen.communicate() - return map(cls.clean, data) - - @staticmethod - def clean(text): - strip = chr(10) + chr(32) - return text.decode().strip(strip) - - -class Show(Execute): - - @command("sh", raw=True) - @doc(_("Execute expression inside a shell, send output")) - def show(self, expression): - Show.spawn(self, expression) - - @classmethod - def terminated(cls, processor, popen): - stdout, stderr = cls.fetch(popen) - success = popen.returncode == 0 - if success and stdout: - processor.send(stdout) - elif not success and stderr: - processor.echo_error(stderr) diff --git a/gajim/command_system/implementation/hosts.py b/gajim/command_system/implementation/hosts.py deleted file mode 100644 index d01005580..000000000 --- a/gajim/command_system/implementation/hosts.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@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/>. - -""" -The module defines a set of command hosts, which are bound to a -different command processors, which are the source of commands. -""" - -from gajim.command_system.framework import CommandHost - - -class ChatCommands(CommandHost): - """ - This command host is bound to the command processor which processes - commands from a chat. - """ - AUTOMATIC = True - - -class PrivateChatCommands(CommandHost): - """ - This command host is bound to the command processor which processes - commands from a private chat. - """ - AUTOMATIC = True - - -class GroupChatCommands(CommandHost): - """ - This command host is bound to the command processor which processes - commands from a group chat. - """ - AUTOMATIC = True diff --git a/gajim/command_system/implementation/middleware.py b/gajim/command_system/implementation/middleware.py deleted file mode 100644 index d1f9bb9c5..000000000 --- a/gajim/command_system/implementation/middleware.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright (c) 2009-2010, Alexander Cherniuk (ts33kr@gmail.com) -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -""" -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 code itself to write basic commands. -""" - -from traceback import print_exc - -# from gi.repository import Pango - -from gajim.common import app -from gajim.common.i18n import _ - -from gajim.command_system.framework import CommandProcessor -from gajim.command_system.errors import CommandError -from gajim.command_system.errors import NoCommandError - - -class ChatCommandProcessor(CommandProcessor): - """ - A basic scaffolding to provide convenient interaction between the - command system and chat controls. It will be merged directly into - the controls, by ChatCommandProcessor being among superclasses of - the controls. - """ - - def process_as_command(self, text): - self.command_succeeded = False - parents = super(ChatCommandProcessor, self) - flag = parents.process_as_command(text) - if flag and self.command_succeeded: - self.clear_input() - return flag - - def execute_command(self, name, arguments): - try: - parents = super(ChatCommandProcessor, self) - parents.execute_command(name, arguments) - except NoCommandError as error: - details = dict(name=error.name, message=error.message) - message = "%(name)s: %(message)s\n" % details - message += "Try using the //%(name)s or /say /%(name)s " % details - message += "construct if you intended to send it as a text." - self.echo_error(message) - except CommandError as error: - self.echo_error("%s: %s" % (error.name, error.message)) - except Exception: - self.echo_error(_("Error during command execution!")) - print_exc() - else: - self.command_succeeded = True - - def looks_like_command(self, text, body, name, arguments): - # Command escape stuff goes 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 body.startswith(self.COMMAND_PREFIX): - self.send(body) - return True - - def command_preprocessor(self, command, name, arguments, args, kwargs): - # If command argument contain h or help option - forward it to - # the /help command. Don't forget to pass self, as all commands - # are unbound. And also don't forget to print output. - if 'h' in kwargs or 'help' in kwargs: - help_ = self.get_command('help') - self.echo(help_(self, name)) - return True - - def command_postprocessor(self, command, name, arguments, args, kwargs, - value): - # If command returns a string - print it to a user. A convenient - # and sufficient in most simple cases shortcut to a using echo. - if value and isinstance(value, str): - self.echo(value) - - -class CommandTools: - """ - Contains a set of basic tools and shortcuts you can use in your - commands to perform some simple operations. These will be merged - directly into the controls, by CommandTools being among superclasses - of the controls. - """ - - def __init__(self): - pass - - def echo(self, text, is_error=False): - """ - Print given text to the user, as a regular command output. - """ - self.conversation_view.add_command_output(text, is_error) - - def echo_error(self, text): - """ - Print given text to the user, as an error command output. - """ - self.echo(text, is_error=True) - - 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. - """ - buffer = self.msg_textview.get_buffer() - buffer.set_text(text) - - def clear_input(self): - """ - Clear input. - """ - self.set_input(str()) - - @property - def connection(self): - """ - Get the current connection object. - """ - return app.connections[self.account] - - @property - def full_jid(self): - """ - Get a full JID of the contact. - """ - return self.contact.jid diff --git a/gajim/command_system/implementation/standard.py b/gajim/command_system/implementation/standard.py deleted file mode 100644 index aa8a2e5f7..000000000 --- a/gajim/command_system/implementation/standard.py +++ /dev/null @@ -1,460 +0,0 @@ -# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@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 for the standard commands. -""" - -from time import localtime -from time import strftime -from datetime import date - -from gi.repository import GLib - -from gajim.common import app -from gajim.common import helpers -from gajim.common.i18n import _ -from gajim.common.const import KindConstant - -from gajim.command_system.errors import CommandError -from gajim.command_system.framework import CommandContainer -from gajim.command_system.framework import command -from gajim.command_system.framework import doc -from gajim.command_system.mapping import generate_usage - -from gajim.command_system.implementation.hosts import ChatCommands -from gajim.command_system.implementation.hosts import PrivateChatCommands -from gajim.command_system.implementation.hosts import GroupChatCommands - - -class StandardCommonCommands(CommandContainer): - """ - This command container contains standard commands which are common - to all - chat, private chat, group chat. - """ - - AUTOMATIC = True - HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands - - @command(overlap=True) - @doc(_("Show help on a given command or a list of available commands if " - "-a is given")) - def help(self, cmd=None, all_=False): - if cmd: - cmd = self.get_command(cmd) - - documentation = _(cmd.extract_documentation()) - usage = generate_usage(cmd) - - text = [] - - if documentation: - text.append(documentation) - if cmd.usage: - text.append(usage) - - return '\n\n'.join(text) - - if all_: - for cmd_ in self.list_commands(): - names = ', '.join(cmd_.names) - description = cmd_.extract_description() - - self.echo("%s - %s" % (names, description)) - else: - help_ = self.get_command('help') - self.echo(help_(self, 'help')) - - @command(raw=True) - @doc(_("Send a message to the contact")) - def say(self, message): - self.send(message) - - @command(raw=True) - @doc(_("Send action (in the third person) to the current chat")) - def me(self, action): - self.send("/me %s" % action) - - @command('lastlog', overlap=True) - @doc(_("Show logged messages which mention given text")) - def grep(self, text, limit=None): - results = app.storage.archive.search_log(self.account, self.contact.jid, text) - - if not results: - raise CommandError(_("%s: Nothing found") % text) - - if limit: - try: - results = results[len(results) - int(limit):] - except ValueError: - raise CommandError(_("Limit must be an integer")) - - for row in results: - contact = row.contact_name - if not contact: - if row.kind == KindConstant.CHAT_MSG_SENT: - contact = app.nicks[self.account] - else: - contact = self.contact.name - - time_obj = localtime(row.time) - date_obj = date.fromtimestamp(row.time) - date_ = strftime('%Y-%m-%d', time_obj) - time_ = strftime('%H:%M:%S', time_obj) - - if date_obj == date.today(): - formatted = "[%s] %s: %s" % (time_, contact, row.message) - else: - formatted = "[%s, %s] %s: %s" % ( - date_, time_, contact, row.message) - - self.echo(formatted) - - @command(raw=True, empty=True) - # Do not translate online, away, chat, xa, dnd - @doc(_(""" - Set the current status - - Status can be given as one of the following values: - online, away, chat, xa, dnd. - """)) - def status(self, status, message): - if status not in ('online', 'away', 'chat', 'xa', 'dnd'): - raise CommandError("Invalid status given") - for connection in app.connections.values(): - if not app.settings.get_account_setting(connection.name, - 'sync_with_global_status'): - continue - if not connection.state.is_available: - continue - connection.change_status(status, message) - - @command(raw=True, empty=True) - @doc(_("Set the current status to away")) - def away(self, message): - if not message: - message = _("Away") - - for connection in app.connections.values(): - if not app.settings.get_account_setting(connection.name, - 'sync_with_global_status'): - continue - if not connection.state.is_available: - continue - connection.change_status('away', message) - - @command('back', raw=True, empty=True) - @doc(_("Set the current status to online")) - def online(self, message): - if not message: - message = _("Available") - - for connection in app.connections.values(): - if not app.settings.get_account_setting(connection.name, - 'sync_with_global_status'): - continue - if not connection.state.is_available: - continue - connection.change_status('online', message) - - @command - @doc(_("Send a disco info request")) - def disco(self): - client = app.get_client(self.account) - if not client.state.is_available: - return - - client.get_module('Discovery').disco_contact(self.contact) - - -class StandardCommonChatCommands(CommandContainer): - """ - This command container contains standard commands, which are common - to a chat and a private chat only. - """ - - AUTOMATIC = True - HOSTS = ChatCommands, PrivateChatCommands - - @command - @doc(_("Clear the text window")) - def clear(self): - self.conversation_view.clear() - - @command - @doc(_("Send a ping to the contact")) - def ping(self): - app.connections[self.account].get_module('Ping').send_ping(self.contact) - - @command - @doc(_("Send DTMF sequence through an open voice chat")) - def dtmf(self, sequence): - if not self.audio_sid: - raise CommandError(_("No open voice chats with the contact")) - for tone in sequence: - if not (tone in ("*", "#") or tone.isdigit()): - raise CommandError(_("%s is not a valid tone") % tone) - gjs = self.connection.get_module('Jingle').get_jingle_session - session = gjs(self.full_jid, self.audio_sid) - content = session.get_content("audio") - content.batch_dtmf(sequence) - - @command - @doc(_("Toggle Voice Chat")) - def audio(self): - if not self.audio_available: - raise CommandError(_("Voice chats are not available")) - # An audio session is toggled by inverting the state of the - # appropriate button. - state = self._audio_button.get_active() - self._audio_button.set_active(not state) - - @command - @doc(_("Toggle Video Chat")) - def video(self): - if not self.video_available: - raise CommandError(_("Video chats are not available")) - # A video session is toggled by inverting the state of the - # appropriate button. - state = self._video_button.get_active() - self._video_button.set_active(not state) - - @command(raw=True) - @doc(_("Send a message to the contact that will attract their attention")) - def attention(self, message): - self.send_message(message, process_commands=False, attention=True) - - -class StandardChatCommands(CommandContainer): - """ - This command container contains standard commands which are unique - to a chat. - """ - - AUTOMATIC = True - HOSTS = (ChatCommands,) - - -class StandardPrivateChatCommands(CommandContainer): - """ - This command container contains standard commands which are unique - to a private chat. - """ - - AUTOMATIC = True - HOSTS = (PrivateChatCommands,) - - -class StandardGroupChatCommands(CommandContainer): - """ - This command container contains standard commands which are unique - to a group chat. - """ - - AUTOMATIC = True - HOSTS = (GroupChatCommands,) - - @command - @doc(_("Clear the text window")) - def clear(self): - self.conversation_view.clear() - - @command(raw=True) - @doc(_("Change your nickname in a group chat")) - def nick(self, new_nick): - try: - new_nick = helpers.parse_resource(new_nick) - except Exception: - raise CommandError(_("Invalid nickname")) - # FIXME: Check state of MUC - self.connection.get_module('MUC').change_nick( - self.room_jid, new_nick) - self.new_nick = new_nick - - @command('query', raw=True) - @doc(_("Open a private chat window with a specified participant")) - def chat(self, nick): - client = app.get_client(self.account) - groupchat_contact = client.get_module('Contacts').get_contact( - self.room_jid, groupchat=True) - nicks = groupchat_contact.get_user_nicknames() - if nick in nicks: - self.send_pm(nick) - else: - raise CommandError(_("Nickname not found")) - - @command('msg', raw=True) - @doc(_("Open a private chat window with a specified participant and send " - "him a message")) - def message(self, nick, message): - client = app.get_client(self.account) - groupchat_contact = client.get_module('Contacts').get_contact( - self.room_jid, groupchat=True) - nicks = groupchat_contact.get_user_nicknames() - if nick in nicks: - self.send_pm(nick, message) - else: - raise CommandError(_("Nickname not found")) - - @command(raw=True, empty=True) - @doc(_("Display or change a group chat topic")) - def topic(self, new_topic): - if new_topic: - self.connection.get_module('MUC').set_subject( - self.room_jid, new_topic) - else: - return self.subject - - @command(raw=True, empty=True) - @doc(_("Invite a user to a group chat for a reason")) - def invite(self, jid, _reason): - control = app.window.get_control(self.account, self.room_jid) - if control is not None: - control.invite(jid) - - @command(raw=True, empty=True) - @doc(_("Join a group chat given by an XMPP Address")) - def join(self, jid): - if '@' not in jid: - jid = jid + '@' + app.get_server_from_jid(self.room_jid) - - app.app.activate_action( - 'groupchat-join', - GLib.Variant('as', [self.account, jid])) - - @command('part', 'close') - @doc(_("Leave the group chat")) - def leave(self): - # Use idle_add to let command system finish printing - variant = GLib.Variant('as', [self.account, str(self.room_jid)]) - GLib.idle_add(app.window.activate_action, 'remove-chat', variant) - - @command(raw=True, empty=True) - @doc(_(""" - Ban user by a nick or a JID from a groupchat - - If given nickname is not found it will be treated as a JID. - """)) - def ban(self, who, reason=''): - client = app.get_client(self.account) - groupchat_contact = client.get_module('Contacts').get_contact( - self.room_jid, groupchat=True) - nick_list = groupchat_contact.get_user_nicknames() - if who in nick_list: - contact = groupchat_contact.get_resource(who) - who = contact.jid - self.connection.get_module('MUC').set_affiliation( - self.room_jid, - {who: {'affiliation': 'outcast', - 'reason': reason}}) - - @command(raw=True, empty=True) - @doc(_("Kick user from group chat by nickname")) - def kick(self, who, reason): - client = app.get_client(self.account) - groupchat_contact = client.get_module('Contacts').get_contact( - self.room_jid, groupchat=True) - nick_list = groupchat_contact.get_user_nicknames() - if who not in nick_list: - raise CommandError(_("Nickname not found")) - self.connection.get_module('MUC').set_role( - self.room_jid, who, 'none', reason) - - @command(raw=True) - # Do not translate moderator, participant, visitor, none - @doc(_("""Set participant role in group chat. - Role can be given as one of the following values: - moderator, participant, visitor, none""")) - def role(self, who, role): - if role not in ('moderator', 'participant', 'visitor', 'none'): - raise CommandError(_("Invalid role given")) - client = app.get_client(self.account) - groupchat_contact = client.get_module('Contacts').get_contact( - self.room_jid, groupchat=True) - nick_list = groupchat_contact.get_user_nicknames() - if who not in nick_list: - raise CommandError(_("Nickname not found")) - self.connection.get_module('MUC').set_role(self.room_jid, who, role) - - @command(raw=True) - # Do not translate owner, admin, member, outcast, none - @doc(_("""Set participant affiliation in group chat. - Affiliation can be given as one of the following values: - owner, admin, member, outcast, none""")) - def affiliate(self, who, affiliation): - if affiliation not in ('owner', 'admin', 'member', 'outcast', 'none'): - raise CommandError(_("Invalid affiliation given")) - client = app.get_client(self.account) - groupchat_contact = client.get_module('Contacts').get_contact( - self.room_jid, groupchat=True) - nick_list = groupchat_contact.get_user_nicknames() - if who not in nick_list: - raise CommandError(_("Nickname not found")) - - contact = groupchat_contact.get_resource(who) - - self.connection.get_module('MUC').set_affiliation( - self.room_jid, - {contact.jid: {'affiliation': affiliation}}) - - @command - @doc(_("Display names of all group chat participants")) - def names(self, verbose=False): - client = app.get_client(self.account) - groupchat_contact = client.get_module('Contacts').get_contact( - self.room_jid, groupchat=True) - - def get_contact(nick): - return groupchat_contact.get_resource(nick) - - def get_role(nick): - return get_contact(nick).role - - nicks = groupchat_contact.get_user_nicknames() - - nicks = sorted(nicks) - nicks = sorted(nicks, key=get_role) - - if not verbose: - return ", ".join(nicks) - - for nick in nicks: - contact = get_contact(nick) - role = helpers.get_uf_role(contact.role) - affiliation = helpers.get_uf_affiliation(contact.affiliation) - self.echo("%s - %s - %s" % (nick, role, affiliation)) - - @command('ignore', raw=True) - @doc(_("Forbid a participant to send you public or private messages")) - def block(self, who): - self.on_block(None, who) - - @command('unignore', raw=True) - @doc(_("Allow a participant to send you public or private messages")) - def unblock(self, who): - self.on_unblock(None, who) - - @command - @doc(_("Send a ping to the contact")) - def ping(self, nick): - client = app.get_client(self.account) - groupchat_contact = client.get_module('Contacts').get_contact( - self.room_jid, groupchat=True) - nick_list = groupchat_contact.get_user_nicknames() - if nick not in nick_list: - raise CommandError(_("Unknown nickname")) - - client.get_module('Ping').send_ping( - groupchat_contact.get_resource(nick)) diff --git a/gajim/command_system/mapping.py b/gajim/command_system/mapping.py deleted file mode 100644 index c0546a726..000000000 --- a/gajim/command_system/mapping.py +++ /dev/null @@ -1,349 +0,0 @@ -# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@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/>. - -""" -The module contains routines to parse command arguments and map them to -the command handler's positional and keyword arguments. - -Mapping is done in two stages: 1) parse arguments into positional -arguments and options; 2) adapt them to the specific command handler -according to the command properties. -""" - -import re -from operator import itemgetter - -from gajim.common.i18n import _ - -from gajim.command_system.errors import DefinitionError -from gajim.command_system.errors import CommandError - -# Quite complex piece of regular expression logic to parse options and -# arguments. Might need some tweaking along the way. -ARG_PATTERN = re.compile(r'(\'|")?(?P<body>(?(1).+?|\S+))(?(1)\1)') -OPT_PATTERN = re.compile(r'(?<!\w)--?(?P<key>[\w-]+)(?:(?:=|\s)(\'|")?(?P<value>(?(2)[^-]+?|[^-\s]+))(?(2)\2))?') - -# Option keys needs to be encoded to a specific encoding as Python does -# not allow to expand dictionary with raw Unicode strings as keys from a -# **kwargs. -KEY_ENCODING = 'UTF-8' - -# Defines how complete representation of command usage (generated based -# on command handler argument specification) will be rendered. -USAGE_PATTERN = 'Usage: %s %s' - - -def parse_arguments(arguments): - """ - Simple yet effective and sufficient in most cases parser which - parses command arguments and returns them as two lists. - - First list represents positional arguments as (argument, position), - and second representing options as (key, value, position) tuples, - where position is a (start, end) span tuple of where it was found in - the string. - - Options may be given in --long or -short format. As --option=value - or --option value or -option value. Keys without values will get - None as value. - - Arguments and option values that contain spaces may be given as 'one - two three' or "one two three"; that is between single or double - quotes. - """ - args, opts = [], [] - - def intersects_opts(given_start, given_end): - """ - Check if given span intersects with any of options. - """ - for _key, _value, (start, end) in opts: - if given_start >= start and given_end <= end: - return True - return False - - def intersects_args(given_start, given_end): - """ - Check if given span intersects with any of arguments. - """ - for _arg, (start, end) in args: - if given_start >= start and given_end <= end: - return True - return False - - for match in re.finditer(OPT_PATTERN, arguments): - if match: - key = match.group('key') - value = match.group('value') or None - position = match.span() - opts.append((key, value, position)) - - for match in re.finditer(ARG_PATTERN, arguments): - if match: - body = match.group('body') - position = match.span() - args.append((body, position)) - - # Primitive but sufficiently effective way of disposing of - # conflicted sectors. Remove any arguments that intersect with - # options. - for arg, position in args[:]: - if intersects_opts(*position): - args.remove((arg, position)) - - # Primitive but sufficiently effective way of disposing of - # conflicted sectors. Remove any options that intersect with - # arguments. - for key, value, position in opts[:]: - if intersects_args(*position): - opts.remove((key, value, position)) - - return args, opts - - -def adapt_arguments(command, arguments, args, opts): - """ - Adapt args and opts got from the parser to a specific handler by - means of arguments specified on command definition. That is - transform them to *args and **kwargs suitable for passing to a - command handler. - - Dashes (-) in the option names will be converted to underscores. So - you can map --one-more-option to a one_more_option=None. - - If the initial value of a keyword argument is a boolean (False in - most cases) - then this option will be treated as a switch, that is - an option which does not take an argument. If a switch is followed - by an argument - then this argument will be treated just like a - normal positional argument. - """ - spec_args, spec_kwargs, var_args, _var_kwargs = command.extract_specification() - norm_kwargs = dict(spec_kwargs) - - # Quite complex piece of neck-breaking logic to extract raw - # arguments if there is more, then one positional argument specified - # by the command. In case if it's just one argument which is the - # collector - this is fairly easy. But when it's more then one - # argument - the neck-breaking logic of how to retrieve residual - # arguments as a raw, all in one piece string, kicks in. - if command.raw: - if arguments: - spec_fix = 1 if command.source else 0 - spec_len = len(spec_args) - spec_fix - arguments_end = len(arguments) - 1 - - # If there are any optional arguments given they should be - # either an unquoted positional argument or part of the raw - # argument. So we find all optional arguments that can - # possibly be unquoted argument and append them as is to the - # args. - for key, value, (start, end) in opts[:spec_len]: - if value: - end -= len(value) + 1 - args.append((arguments[start:end], (start, end))) - args.append((value, (end, end + len(value) + 1))) - else: - args.append((arguments[start:end], (start, end))) - - # We need in-place sort here because after manipulations - # with options order of arguments might be wrong and we just - # can't have more complex logic to not let that happen. - args.sort(key=itemgetter(1)) - - if spec_len > 1: - try: - _stopper, (start, end) = args[spec_len - 2] - except IndexError: - raise CommandError(_("Missing arguments"), command) - - # The essential point of the whole play. After - # boundaries are being determined (supposedly correct) - # we separate raw part from the rest of arguments, which - # should be normally processed. - raw = arguments[end:] - raw = raw.strip() or None - - if not raw and not command.empty: - raise CommandError(_("Missing arguments"), command) - - # Discard residual arguments and all of the options as - # raw command does not support options and if an option - # is given it is rather a part of a raw argument. - args = args[:spec_len - 1] - opts = [] - - args.append((raw, (end, arguments_end))) - else: - # Substitute all of the arguments with only one, which - # contain raw and unprocessed arguments as a string. And - # discard all the options, as raw command does not - # support them. - args = [(arguments, (0, arguments_end))] - opts = [] - else: - if command.empty: - args.append((None, (0, 0))) - else: - raise CommandError(_("Missing arguments"), command) - - # The first stage of transforming options we have got to a format - # that can be used to associate them with declared keyword - # arguments. Substituting dashes (-) in their names with - # underscores (_). - for index, (key, value, position) in enumerate(opts): - if '-' in key: - opts[index] = (key.replace('-', '_'), value, position) - - # The second stage of transforming options to an associable state. - # Expanding short, one-letter options to a verbose ones, if - # corresponding opt-in has been given. - if command.expand: - expanded = [] - for spec_key in norm_kwargs.keys(): - letter = spec_key[0] if len(spec_key) > 1 else None - if letter and letter not in expanded: - for index, (key, value, position) in enumerate(opts): - if key == letter: - expanded.append(letter) - opts[index] = (spec_key, value, position) - break - - # Detect switches and set their values accordingly. If any of them - # carries a value - append it to args. - for index, (key, value, position) in enumerate(opts): - if isinstance(norm_kwargs.get(key), bool): - opts[index] = (key, True, position) - if value: - args.append((value, position)) - - # Sorting arguments and options (just to be sure) in regarding to - # their positions in the string. - args.sort(key=itemgetter(1)) - opts.sort(key=itemgetter(2)) - - # Stripping down position information supplied with arguments and - # options as it won't be needed again. - args = list(map(lambda t: t[0], args)) - opts = list(map(lambda t: (t[0], t[1]), opts)) - - # If command has extra option enabled - collect all extra arguments - # and pass them to a last positional argument command defines as a - # list. - if command.extra: - if not var_args: - spec_fix = 1 if not command.source else 2 - spec_len = len(spec_args) - spec_fix - extra = args[spec_len:] - args = args[:spec_len] - args.append(extra) - else: - raise DefinitionError("Can not have both, extra and *args") - - # Detect if positional arguments overlap keyword arguments. If so - # and this is allowed by command options - then map them directly to - # their options, so they can get proper further processing. - spec_fix = 1 if command.source else 0 - spec_len = len(spec_args) - spec_fix - if len(args) > spec_len: - if command.overlap: - overlapped = args[spec_len:] - args = args[:spec_len] - for arg, spec_key, _spec_value in zip(overlapped, spec_kwargs): - opts.append((spec_key, arg)) - else: - raise CommandError(_("Too many arguments"), command) - - # Detect every switch and ensure it will not receive any arguments. - # Normally this does not happen unless overlapping is enabled. - for key, value in opts: - initial = norm_kwargs.get(key) - if isinstance(initial, bool): - if not isinstance(value, bool): - raise CommandError( - "%s: Switch can not take an argument" % key, command) - - # Inject the source arguments as a string as a first argument, if - # command has enabled the corresponding option. - if command.source: - args.insert(0, arguments) - - # Return *args and **kwargs in the form suitable for passing to a - # command handler and being expanded. - return tuple(args), dict(opts) - - -def generate_usage(command, complete=True): - """ - Extract handler's arguments specification and wrap them in a - human-readable format usage information. If complete is given - then - USAGE_PATTERN will be used to render the specification completely. - """ - spec_args, spec_kwargs, var_args, var_kwargs = command.extract_specification() - - # Remove some special positional arguments from the specification, - # but store their names so they can be used for usage info - # generation. - _sp_source = spec_args.pop(0) if command.source else None - sp_extra = spec_args.pop() if command.extra else None - - kwargs = [] - letters = [] - - for key, value in spec_kwargs: - letter = key[0] - key = key.replace('_', '-') - - if isinstance(value, bool): - value = str() - else: - value = '=%s' % value - - if letter not in letters: - kwargs.append('-(-%s)%s%s' % (letter, key[1:], value)) - letters.append(letter) - else: - kwargs.append('--%s%s' % (key, value)) - - usage = str() - args = str() - - if command.raw: - spec_len = len(spec_args) - 1 - if spec_len: - args += ('<%s>' % ', '.join(spec_args[:spec_len])) + ' ' - args += ('(|%s|)' if command.empty else '|%s|') % spec_args[-1] - else: - if spec_args: - args += '<%s>' % ', '.join(spec_args) - if var_args or sp_extra: - args += (' ' if spec_args else str()) + '<<%s>>' % ( - var_args or sp_extra) - - usage += args - - if kwargs or var_kwargs: - if kwargs: - usage += (' ' if args else str()) + '[%s]' % ', '.join(kwargs) - if var_kwargs: - usage += (' ' if args else str()) + '[[%s]]' % var_kwargs - - # Native name will be the first one if it is included. Otherwise, - # names will be in the order they were specified. - if len(command.names) > 1: - names = '%s (%s)' % (command.first_name, ', '.join(command.names[1:])) - else: - names = command.first_name - - return USAGE_PATTERN % (names, usage) if complete else usage diff --git a/gajim/command_system/tools.py b/gajim/command_system/tools.py deleted file mode 100644 index cde3e17d9..000000000 --- a/gajim/command_system/tools.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) 2010, Alexander Cherniuk (ts33kr@gmail.com) -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -def remove(sequence, target): - if isinstance(sequence, list): - if target in sequence: - sequence.remove(target) - elif isinstance(sequence, dict): - if target in sequence: - del sequence[target] diff --git a/gajim/common/setting_values.py b/gajim/common/setting_values.py index ccf14f1da..63c293d16 100644 --- a/gajim/common/setting_values.py +++ b/gajim/common/setting_values.py @@ -45,7 +45,6 @@ BoolSettings = Literal[ 'change_roster_title', 'chat_merge_consecutive_nickname', 'check_for_update', - 'command_system_execute', 'confirm_close_muc', 'confirm_close_multiple_tabs', 'confirm_on_window_delete', @@ -191,7 +190,6 @@ APP_SETTINGS = { 'chat_merge_consecutive_nickname': True, 'chat_timestamp_format': '%H:%M', 'check_for_update': True, - 'command_system_execute': False, 'confirm_block': '', 'confirm_close_muc': True, 'confirm_close_multiple_tabs': True, @@ -647,9 +645,6 @@ ADVANCED_SETTINGS = { 'if there are multiple messages from the same sender within a ' 'specific timespan.'), 'chat_timestamp_format': 'https://docs.python.org/3/library/time.html#time.strftime', # noqa: E501 - 'command_system_execute': _( - 'If enabled, Gajim will execute commands ' - '(/show, /sh, /execute, /exec).'), 'confirm_block': _( 'Show a confirmation dialog to block a contact? Empty string ' 'means never show the dialog.'), @@ -7,7 +7,6 @@ exclude = debian_build, test, typings, - gajim/command_system, gajim/common/config.py, gajim/common/optparser.py, gajim/common/socks5.py, |