Philipp Hörist pushed to branch master at gajim / gajim
Commits:
-
36578a37
by lovetox at 2022-08-11T20:04:46+02:00
14 changed files:
- − gajim/command_system/__init__.py
- − gajim/command_system/dispatcher.py
- − gajim/command_system/errors.py
- − gajim/command_system/framework.py
- − gajim/command_system/implementation/__init__.py
- − gajim/command_system/implementation/custom.py
- − gajim/command_system/implementation/execute.py
- − gajim/command_system/implementation/hosts.py
- − gajim/command_system/implementation/middleware.py
- − gajim/command_system/implementation/standard.py
- − gajim/command_system/mapping.py
- − gajim/command_system/tools.py
- gajim/common/setting_values.py
- setup.cfg
Changes:
| 1 | -# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
|
|
| 2 | -#
|
|
| 3 | -# This program is free software: you can redistribute it and/or modify
|
|
| 4 | -# it under the terms of the GNU General Public License as published by
|
|
| 5 | -# the Free Software Foundation, either version 3 of the License, or
|
|
| 6 | -# (at your option) any later version.
|
|
| 7 | -#
|
|
| 8 | -# This program is distributed in the hope that it will be useful,
|
|
| 9 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
| 10 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
| 11 | -# GNU General Public License for more details.
|
|
| 12 | -#
|
|
| 13 | -# You should have received a copy of the GNU General Public License
|
|
| 14 | -# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
| 15 | - |
|
| 16 | -"""
|
|
| 17 | -The command system providing scalable, clean and convenient architecture
|
|
| 18 | -in combination with declarative way of defining commands and a fair
|
|
| 19 | -amount of automatization for routine processes.
|
|
| 20 | -""" |
| 1 | -# Copyright (c) 2010, Alexander Cherniuk (ts33kr@gmail.com)
|
|
| 2 | -# All rights reserved.
|
|
| 3 | -#
|
|
| 4 | -# Redistribution and use in source and binary forms, with or without
|
|
| 5 | -# modification, are permitted provided that the following conditions
|
|
| 6 | -# are met:
|
|
| 7 | -#
|
|
| 8 | -# * Redistributions of source code must retain the above copyright
|
|
| 9 | -# notice, this list of conditions and the following disclaimer.
|
|
| 10 | -#
|
|
| 11 | -# * Redistributions in binary form must reproduce the above copyright
|
|
| 12 | -# notice, this list of conditions and the following disclaimer in the
|
|
| 13 | -# documentation and/or other materials provided with the distribution.
|
|
| 14 | -#
|
|
| 15 | -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
| 16 | -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
| 17 | -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
| 18 | -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
| 19 | -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
| 20 | -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
| 21 | -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
| 22 | -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
| 23 | -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
| 24 | -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
| 25 | -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
| 26 | - |
|
| 27 | -"""
|
|
| 28 | -Backbone of the command system. Provides smart and controllable
|
|
| 29 | -dispatching mechanism with an auto-discovery functionality. In addition
|
|
| 30 | -to automatic discovery and dispatching, also features manual control
|
|
| 31 | -over the process.
|
|
| 32 | -"""
|
|
| 33 | - |
|
| 34 | -from typing import Any
|
|
| 35 | - |
|
| 36 | -from gajim.command_system.tools import remove
|
|
| 37 | - |
|
| 38 | -COMMANDS: dict[Any, Any] = {}
|
|
| 39 | -CONTAINERS: dict[Any, Any] = {}
|
|
| 40 | - |
|
| 41 | - |
|
| 42 | -def add_host(host):
|
|
| 43 | - CONTAINERS[host] = []
|
|
| 44 | - |
|
| 45 | - |
|
| 46 | -def remove_host(host):
|
|
| 47 | - remove(CONTAINERS, host)
|
|
| 48 | - |
|
| 49 | - |
|
| 50 | -def add_container(container):
|
|
| 51 | - for host in container.HOSTS:
|
|
| 52 | - CONTAINERS[host].append(container)
|
|
| 53 | - |
|
| 54 | - |
|
| 55 | -def remove_container(container):
|
|
| 56 | - for host in container.HOSTS:
|
|
| 57 | - remove(CONTAINERS[host], container)
|
|
| 58 | - |
|
| 59 | - |
|
| 60 | -def add_commands(container):
|
|
| 61 | - commands = COMMANDS.setdefault(container, {})
|
|
| 62 | - for command in traverse_commands(container):
|
|
| 63 | - for name in command.names:
|
|
| 64 | - commands[name] = command
|
|
| 65 | - |
|
| 66 | - |
|
| 67 | -def remove_commands(container):
|
|
| 68 | - remove(COMMANDS, container)
|
|
| 69 | - |
|
| 70 | - |
|
| 71 | -def traverse_commands(container):
|
|
| 72 | - for name in dir(container):
|
|
| 73 | - attribute = getattr(container, name)
|
|
| 74 | - if is_command(attribute):
|
|
| 75 | - yield attribute
|
|
| 76 | - |
|
| 77 | - |
|
| 78 | -def is_command(attribute):
|
|
| 79 | - from gajim.command_system.framework import Command
|
|
| 80 | - return isinstance(attribute, Command)
|
|
| 81 | - |
|
| 82 | - |
|
| 83 | -def is_root(namespace):
|
|
| 84 | - metaclass = namespace.get("__metaclass__", None)
|
|
| 85 | - if not metaclass:
|
|
| 86 | - return False
|
|
| 87 | - return issubclass(metaclass, Dispatchable)
|
|
| 88 | - |
|
| 89 | - |
|
| 90 | -def get_command(host, name):
|
|
| 91 | - for container in CONTAINERS[host]:
|
|
| 92 | - command = COMMANDS[container].get(name)
|
|
| 93 | - if command:
|
|
| 94 | - return command
|
|
| 95 | - |
|
| 96 | - |
|
| 97 | -def list_commands(host):
|
|
| 98 | - for container in CONTAINERS[host]:
|
|
| 99 | - commands = COMMANDS[container]
|
|
| 100 | - for name, command in commands.items():
|
|
| 101 | - yield name, command
|
|
| 102 | - |
|
| 103 | - |
|
| 104 | -class Dispatchable(type):
|
|
| 105 | - # pylint: disable=no-value-for-parameter
|
|
| 106 | - def __init__(cls, name, bases, namespace):
|
|
| 107 | - parents = super(Dispatchable, cls)
|
|
| 108 | - parents.__init__(name, bases, namespace)
|
|
| 109 | - if not is_root(namespace):
|
|
| 110 | - cls.dispatch()
|
|
| 111 | - |
|
| 112 | - def dispatch(cls):
|
|
| 113 | - if cls.AUTOMATIC:
|
|
| 114 | - cls.enable()
|
|
| 115 | - |
|
| 116 | - |
|
| 117 | -class Host(Dispatchable):
|
|
| 118 | - |
|
| 119 | - def enable(cls):
|
|
| 120 | - add_host(cls)
|
|
| 121 | - |
|
| 122 | - def disable(cls):
|
|
| 123 | - remove_host(cls)
|
|
| 124 | - |
|
| 125 | - |
|
| 126 | -class Container(Dispatchable):
|
|
| 127 | - |
|
| 128 | - def enable(cls):
|
|
| 129 | - add_container(cls)
|
|
| 130 | - add_commands(cls)
|
|
| 131 | - |
|
| 132 | - def disable(cls):
|
|
| 133 | - remove_commands(cls)
|
|
| 134 | - remove_container(cls) |
| 1 | -# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
|
|
| 2 | -#
|
|
| 3 | -# This program is free software: you can redistribute it and/or modify
|
|
| 4 | -# it under the terms of the GNU General Public License as published by
|
|
| 5 | -# the Free Software Foundation, either version 3 of the License, or
|
|
| 6 | -# (at your option) any later version.
|
|
| 7 | -#
|
|
| 8 | -# This program is distributed in the hope that it will be useful,
|
|
| 9 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
| 10 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
| 11 | -# GNU General Public License for more details.
|
|
| 12 | -#
|
|
| 13 | -# You should have received a copy of the GNU General Public License
|
|
| 14 | -# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
| 15 | - |
|
| 16 | - |
|
| 17 | -class BaseError(Exception):
|
|
| 18 | - """
|
|
| 19 | - Common base for errors which relate to a specific command.
|
|
| 20 | - Encapsulates everything needed to identify a command, by either its
|
|
| 21 | - object or name.
|
|
| 22 | - """
|
|
| 23 | - |
|
| 24 | - def __init__(self, message, command=None, name=None):
|
|
| 25 | - self.message = message
|
|
| 26 | - |
|
| 27 | - self.command = command
|
|
| 28 | - self.name = name
|
|
| 29 | - |
|
| 30 | - if command and not name:
|
|
| 31 | - self.name = command.first_name
|
|
| 32 | - |
|
| 33 | - super(BaseError, self).__init__()
|
|
| 34 | - |
|
| 35 | - def __str__(self):
|
|
| 36 | - return self.message
|
|
| 37 | - |
|
| 38 | - |
|
| 39 | -class DefinitionError(BaseError):
|
|
| 40 | - """
|
|
| 41 | - Used to indicate errors occurred on command definition.
|
|
| 42 | - """
|
|
| 43 | - |
|
| 44 | - |
|
| 45 | -class CommandError(BaseError):
|
|
| 46 | - """
|
|
| 47 | - Used to indicate errors occurred during command execution.
|
|
| 48 | - """
|
|
| 49 | - |
|
| 50 | - |
|
| 51 | -class NoCommandError(BaseError):
|
|
| 52 | - """
|
|
| 53 | - Used to indicate an inability to find the specified command.
|
|
| 54 | - """ |
| 1 | -# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
|
|
| 2 | -#
|
|
| 3 | -# This program is free software: you can redistribute it and/or modify
|
|
| 4 | -# it under the terms of the GNU General Public License as published by
|
|
| 5 | -# the Free Software Foundation, either version 3 of the License, or
|
|
| 6 | -# (at your option) any later version.
|
|
| 7 | -#
|
|
| 8 | -# This program is distributed in the hope that it will be useful,
|
|
| 9 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
| 10 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
| 11 | -# GNU General Public License for more details.
|
|
| 12 | -#
|
|
| 13 | -# You should have received a copy of the GNU General Public License
|
|
| 14 | -# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
| 15 | - |
|
| 16 | -"""
|
|
| 17 | -Provides a tiny framework with simple, yet powerful and extensible
|
|
| 18 | -architecture to implement commands in a straight and flexible,
|
|
| 19 | -declarative way.
|
|
| 20 | -"""
|
|
| 21 | - |
|
| 22 | -from types import FunctionType
|
|
| 23 | -from inspect import getfullargspec
|
|
| 24 | -from inspect import getdoc
|
|
| 25 | - |
|
| 26 | -from gajim.command_system.dispatcher import Host
|
|
| 27 | -from gajim.command_system.dispatcher import Container
|
|
| 28 | -from gajim.command_system.dispatcher import get_command
|
|
| 29 | -from gajim.command_system.dispatcher import list_commands
|
|
| 30 | -from gajim.command_system.mapping import parse_arguments
|
|
| 31 | -from gajim.command_system.mapping import adapt_arguments
|
|
| 32 | -from gajim.command_system.errors import DefinitionError
|
|
| 33 | -from gajim.command_system.errors import CommandError
|
|
| 34 | -from gajim.command_system.errors import NoCommandError
|
|
| 35 | - |
|
| 36 | - |
|
| 37 | -class CommandHost(metaclass=Host):
|
|
| 38 | - """
|
|
| 39 | - Command host is a hub between numerous command processors and
|
|
| 40 | - command containers. Aimed to participate in a dispatching process in
|
|
| 41 | - order to provide clean and transparent architecture.
|
|
| 42 | - |
|
| 43 | - The AUTOMATIC class variable, which must be defined by a command
|
|
| 44 | - host, specifies whether the command host should be automatically
|
|
| 45 | - dispatched and enabled by the dispatcher or not.
|
|
| 46 | - """
|
|
| 47 | - __metaclass__ = Host
|
|
| 48 | - |
|
| 49 | - |
|
| 50 | -class CommandContainer(metaclass=Container):
|
|
| 51 | - """
|
|
| 52 | - Command container is an entity which holds defined commands,
|
|
| 53 | - allowing them to be dispatched and processed correctly. Each
|
|
| 54 | - command container may be bound to a one or more command hosts.
|
|
| 55 | - |
|
| 56 | - The AUTOMATIC class variable, which must be defined by a command
|
|
| 57 | - processor, specifies whether the command processor should be
|
|
| 58 | - automatically dispatched and enabled by the dispatcher or not.
|
|
| 59 | - |
|
| 60 | - Bounding is controlled by the HOSTS class variable, which must be
|
|
| 61 | - defined by the command container. This variable should contain a
|
|
| 62 | - sequence of hosts to bound to, as a tuple or list.
|
|
| 63 | - """
|
|
| 64 | - __metaclass__ = Container
|
|
| 65 | - |
|
| 66 | - |
|
| 67 | -class CommandProcessor:
|
|
| 68 | - """
|
|
| 69 | - Command processor is an immediate command emitter. It does not
|
|
| 70 | - participate in the dispatching process directly, but must define a
|
|
| 71 | - host to bound to.
|
|
| 72 | - |
|
| 73 | - Bounding is controlled by the COMMAND_HOST variable, which must be
|
|
| 74 | - defined in the body of the command processor. This variable should
|
|
| 75 | - be set to a specific command host.
|
|
| 76 | - """
|
|
| 77 | - |
|
| 78 | - # This defines a command prefix (or an initializer), which should
|
|
| 79 | - # precede a text in order for it to be processed as a command.
|
|
| 80 | - COMMAND_PREFIX = '/'
|
|
| 81 | - |
|
| 82 | - def process_as_command(self, text):
|
|
| 83 | - """
|
|
| 84 | - Try to process text as a command. Returns True if it has been
|
|
| 85 | - processed as a command and False otherwise.
|
|
| 86 | - """
|
|
| 87 | - # pylint: disable=assignment-from-no-return
|
|
| 88 | - prefix = text.startswith(self.COMMAND_PREFIX)
|
|
| 89 | - length = len(text) > len(self.COMMAND_PREFIX)
|
|
| 90 | - if not (prefix and length):
|
|
| 91 | - return False
|
|
| 92 | - |
|
| 93 | - body = text[len(self.COMMAND_PREFIX):]
|
|
| 94 | - body = body.strip()
|
|
| 95 | - |
|
| 96 | - parts = body.split(None, 1)
|
|
| 97 | - name, arguments = parts if len(parts) > 1 else (parts[0], None)
|
|
| 98 | - |
|
| 99 | - flag = self.looks_like_command(text, body, name, arguments)
|
|
| 100 | - if flag is not None:
|
|
| 101 | - return flag
|
|
| 102 | - |
|
| 103 | - self.execute_command(name, arguments)
|
|
| 104 | - |
|
| 105 | - return True
|
|
| 106 | - |
|
| 107 | - def execute_command(self, name, arguments):
|
|
| 108 | - cmd = self.get_command(name)
|
|
| 109 | - |
|
| 110 | - args, opts = parse_arguments(arguments) if arguments else ([], [])
|
|
| 111 | - args, kwargs = adapt_arguments(cmd, arguments, args, opts)
|
|
| 112 | - |
|
| 113 | - if self.command_preprocessor(cmd, name, arguments, args, kwargs):
|
|
| 114 | - return
|
|
| 115 | - value = cmd(self, *args, **kwargs)
|
|
| 116 | - self.command_postprocessor(cmd, name, arguments, args, kwargs, value)
|
|
| 117 | - |
|
| 118 | - def command_preprocessor(self, cmd, name, arguments, args, kwargs):
|
|
| 119 | - """
|
|
| 120 | - Redefine this method in the subclass to execute custom code
|
|
| 121 | - before command gets executed.
|
|
| 122 | - |
|
| 123 | - If returns True then command execution will be interrupted and
|
|
| 124 | - command will not be executed.
|
|
| 125 | - """
|
|
| 126 | - |
|
| 127 | - def command_postprocessor(self, cmd, name, arguments, args, kwargs, value):
|
|
| 128 | - """
|
|
| 129 | - Redefine this method in the subclass to execute custom code
|
|
| 130 | - after command gets executed.
|
|
| 131 | - """
|
|
| 132 | - |
|
| 133 | - def looks_like_command(self, text, body, name, arguments):
|
|
| 134 | - """
|
|
| 135 | - This hook is being called before any processing, but after it
|
|
| 136 | - was determined that text looks like a command.
|
|
| 137 | - |
|
| 138 | - If returns value other then None - then further processing will
|
|
| 139 | - be interrupted and that value will be used to return from
|
|
| 140 | - process_as_command.
|
|
| 141 | - """
|
|
| 142 | - |
|
| 143 | - def get_command(self, name):
|
|
| 144 | - cmd = get_command(self.COMMAND_HOST, name)
|
|
| 145 | - if not cmd:
|
|
| 146 | - raise NoCommandError("Command does not exist", name=name)
|
|
| 147 | - return cmd
|
|
| 148 | - |
|
| 149 | - def list_commands(self):
|
|
| 150 | - commands = list_commands(self.COMMAND_HOST)
|
|
| 151 | - commands = dict(commands)
|
|
| 152 | - return sorted(set(commands.values()), key=repr)
|
|
| 153 | - |
|
| 154 | - |
|
| 155 | -class Command:
|
|
| 156 | - |
|
| 157 | - def __init__(self, handler, *names, **properties):
|
|
| 158 | - self.handler = handler
|
|
| 159 | - self.names = names
|
|
| 160 | - |
|
| 161 | - # Automatically set all the properties passed to a constructor
|
|
| 162 | - # by the command decorator.
|
|
| 163 | - for key, value in properties.items():
|
|
| 164 | - setattr(self, key, value)
|
|
| 165 | - |
|
| 166 | - def __call__(self, *args, **kwargs):
|
|
| 167 | - try:
|
|
| 168 | - return self.handler(*args, **kwargs)
|
|
| 169 | - |
|
| 170 | - # This allows to use a shortcut way of raising an exception
|
|
| 171 | - # inside a handler. That is to raise a CommandError without
|
|
| 172 | - # command or name attributes set. They will be set to a
|
|
| 173 | - # corresponding values right here in case if they was not set by
|
|
| 174 | - # the one who raised an exception.
|
|
| 175 | - except CommandError as error:
|
|
| 176 | - if not error.command and not error.name:
|
|
| 177 | - raise CommandError(error.message, self)
|
|
| 178 | - raise
|
|
| 179 | - |
|
| 180 | - # This one is a little bit too wide, but as Python does not have
|
|
| 181 | - # anything more constrained - there is no other choice. Take a
|
|
| 182 | - # look here if command complains about invalid arguments while
|
|
| 183 | - # they are ok.
|
|
| 184 | - except TypeError:
|
|
| 185 | - raise CommandError("Command received invalid arguments", self)
|
|
| 186 | - |
|
| 187 | - def __repr__(self):
|
|
| 188 | - return "<Command %s>" % ', '.join(self.names)
|
|
| 189 | - |
|
| 190 | - def __cmp__(self, other):
|
|
| 191 | - if self.first_name > other.first_name:
|
|
| 192 | - return 1
|
|
| 193 | - if self.first_name < other.first_name:
|
|
| 194 | - return -1
|
|
| 195 | - return 0
|
|
| 196 | - |
|
| 197 | - @property
|
|
| 198 | - def first_name(self):
|
|
| 199 | - return self.names[0]
|
|
| 200 | - |
|
| 201 | - @property
|
|
| 202 | - def native_name(self):
|
|
| 203 | - return self.handler.__name__
|
|
| 204 | - |
|
| 205 | - def extract_documentation(self):
|
|
| 206 | - """
|
|
| 207 | - Extract handler's documentation which is a doc-string and
|
|
| 208 | - transform it to a usable format.
|
|
| 209 | - """
|
|
| 210 | - return getdoc(self.handler)
|
|
| 211 | - |
|
| 212 | - def extract_description(self):
|
|
| 213 | - """
|
|
| 214 | - Extract handler's description (which is a first line of the
|
|
| 215 | - documentation). Try to keep them simple yet meaningful.
|
|
| 216 | - """
|
|
| 217 | - documentation = self.extract_documentation()
|
|
| 218 | - return documentation.split('\n', 1)[0] if documentation else None
|
|
| 219 | - |
|
| 220 | - def extract_specification(self):
|
|
| 221 | - """
|
|
| 222 | - Extract handler's arguments specification, as it was defined
|
|
| 223 | - preserving their order.
|
|
| 224 | - """
|
|
| 225 | - names, var_args, var_kwargs, defaults, _, _, _ = getfullargspec(
|
|
| 226 | - self.handler)
|
|
| 227 | - |
|
| 228 | - # Behavior of this code need to be checked. Might yield
|
|
| 229 | - # incorrect results on some rare occasions.
|
|
| 230 | - spec_args = names[:-len(defaults) if defaults else len(names)]
|
|
| 231 | - spec_kwargs = list(
|
|
| 232 | - zip(names[-len(defaults):], defaults)) if defaults else {}
|
|
| 233 | - |
|
| 234 | - # Removing self from arguments specification. Command handler
|
|
| 235 | - # should receive the processors as a first argument, which
|
|
| 236 | - # should be self by the canonical means.
|
|
| 237 | - if spec_args.pop(0) != 'self':
|
|
| 238 | - raise DefinitionError("First argument must be self", self)
|
|
| 239 | - |
|
| 240 | - return spec_args, spec_kwargs, var_args, var_kwargs
|
|
| 241 | - |
|
| 242 | - |
|
| 243 | -def command(*names, **properties):
|
|
| 244 | - """
|
|
| 245 | - A decorator for defining commands in a declarative way. Provides
|
|
| 246 | - facilities for setting command's names and properties.
|
|
| 247 | - |
|
| 248 | - Names should contain a set of names (aliases) by which the command
|
|
| 249 | - can be reached. If no names are given - the native name (the one
|
|
| 250 | - extracted from the command handler) will be used.
|
|
| 251 | - |
|
| 252 | - If native=True is given (default) and names is non-empty - then the
|
|
| 253 | - native name of the command will be prepended in addition to the
|
|
| 254 | - given names.
|
|
| 255 | - |
|
| 256 | - If usage=True is given (default) - then command help will be
|
|
| 257 | - appended with autogenerated usage info, based of the command handler
|
|
| 258 | - arguments introspection.
|
|
| 259 | - |
|
| 260 | - If source=True is given - then the first argument of the command
|
|
| 261 | - will receive the source arguments, as a raw, unprocessed string. The
|
|
| 262 | - further mapping of arguments and options will not be affected.
|
|
| 263 | - |
|
| 264 | - If raw=True is given - then command considered to be raw and should
|
|
| 265 | - define positional arguments only. If it defines only one positional
|
|
| 266 | - argument - this argument will receive all the raw and unprocessed
|
|
| 267 | - arguments. If the command defines more then one positional argument
|
|
| 268 | - - then all the arguments except the last one will be processed
|
|
| 269 | - normally; the last argument will get what is left after the
|
|
| 270 | - processing as raw and unprocessed string.
|
|
| 271 | - |
|
| 272 | - If empty=True is given - this will allow to call a raw command
|
|
| 273 | - without arguments.
|
|
| 274 | - |
|
| 275 | - If extra=True is given - then all the extra arguments passed to a
|
|
| 276 | - command will be collected into a sequence and given to the last
|
|
| 277 | - positional argument.
|
|
| 278 | - |
|
| 279 | - If overlap=True is given - then all the extra arguments will be
|
|
| 280 | - mapped as if they were values for the keyword arguments.
|
|
| 281 | - |
|
| 282 | - If expand=True is given (default) - then short, one-letter options
|
|
| 283 | - will be expanded to a verbose ones, based of the comparison of the
|
|
| 284 | - first letter. If more then one option with the same first letter is
|
|
| 285 | - given - then only first one will be used in the expansion.
|
|
| 286 | - """
|
|
| 287 | - names = list(names)
|
|
| 288 | - |
|
| 289 | - native = properties.get('native', True)
|
|
| 290 | - |
|
| 291 | - usage = properties.get('usage', True)
|
|
| 292 | - source = properties.get('source', False)
|
|
| 293 | - raw = properties.get('raw', False)
|
|
| 294 | - empty = properties.get('empty', False)
|
|
| 295 | - extra = properties.get('extra', False)
|
|
| 296 | - overlap = properties.get('overlap', False)
|
|
| 297 | - expand = properties.get('expand', True)
|
|
| 298 | - |
|
| 299 | - if empty and not raw:
|
|
| 300 | - raise DefinitionError("Empty option can be used only with raw commands")
|
|
| 301 | - |
|
| 302 | - if extra and overlap:
|
|
| 303 | - raise DefinitionError("Extra and overlap options can not be used "
|
|
| 304 | - "together")
|
|
| 305 | - |
|
| 306 | - properties = {
|
|
| 307 | - 'usage': usage,
|
|
| 308 | - 'source': source,
|
|
| 309 | - 'raw': raw,
|
|
| 310 | - 'extra': extra,
|
|
| 311 | - 'overlap': overlap,
|
|
| 312 | - 'empty': empty,
|
|
| 313 | - 'expand': expand
|
|
| 314 | - }
|
|
| 315 | - |
|
| 316 | - def decorator(handler):
|
|
| 317 | - """
|
|
| 318 | - Decorator which receives handler as a first argument and then
|
|
| 319 | - wraps it in the command which then returns back.
|
|
| 320 | - """
|
|
| 321 | - cmd = Command(handler, *names, **properties)
|
|
| 322 | - |
|
| 323 | - # Extract and inject a native name if either no other names are
|
|
| 324 | - # specified or native property is enabled, while making
|
|
| 325 | - # sure it is going to be the first one in the list.
|
|
| 326 | - if not names or native:
|
|
| 327 | - names.insert(0, cmd.native_name)
|
|
| 328 | - cmd.names = tuple(names)
|
|
| 329 | - |
|
| 330 | - return cmd
|
|
| 331 | - |
|
| 332 | - # Workaround if we are getting called without parameters. Keep in
|
|
| 333 | - # mind that in that case - first item in the names will be the
|
|
| 334 | - # handler.
|
|
| 335 | - if names and isinstance(names[0], FunctionType):
|
|
| 336 | - return decorator(names.pop(0))
|
|
| 337 | - |
|
| 338 | - return decorator
|
|
| 339 | - |
|
| 340 | - |
|
| 341 | -def doc(text):
|
|
| 342 | - """
|
|
| 343 | - This decorator is used to bind a documentation (a help) to a
|
|
| 344 | - command.
|
|
| 345 | - """
|
|
| 346 | - def decorator(target):
|
|
| 347 | - if isinstance(target, Command):
|
|
| 348 | - target.handler.__doc__ = text
|
|
| 349 | - else:
|
|
| 350 | - target.__doc__ = text
|
|
| 351 | - return target
|
|
| 352 | - |
|
| 353 | - return decorator |
| 1 | -# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
|
|
| 2 | -#
|
|
| 3 | -# This program is free software: you can redistribute it and/or modify
|
|
| 4 | -# it under the terms of the GNU General Public License as published by
|
|
| 5 | -# the Free Software Foundation, either version 3 of the License, or
|
|
| 6 | -# (at your option) any later version.
|
|
| 7 | -#
|
|
| 8 | -# This program is distributed in the hope that it will be useful,
|
|
| 9 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
| 10 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
| 11 | -# GNU General Public License for more details.
|
|
| 12 | -#
|
|
| 13 | -# You should have received a copy of the GNU General Public License
|
|
| 14 | -# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
| 15 | - |
|
| 16 | -"""
|
|
| 17 | -The implementation and auxiliary systems which implement the standard
|
|
| 18 | -Gajim commands and also provide an infrastructure for adding custom
|
|
| 19 | -commands.
|
|
| 20 | -""" |
| 1 | -# Copyright (c) 2009-2010, Alexander Cherniuk (ts33kr@gmail.com)
|
|
| 2 | -# All rights reserved.
|
|
| 3 | -#
|
|
| 4 | -# Redistribution and use in source and binary forms, with or without
|
|
| 5 | -# modification, are permitted provided that the following conditions
|
|
| 6 | -# are met:
|
|
| 7 | -#
|
|
| 8 | -# * Redistributions of source code must retain the above copyright
|
|
| 9 | -# notice, this list of conditions and the following disclaimer.
|
|
| 10 | -#
|
|
| 11 | -# * Redistributions in binary form must reproduce the above copyright
|
|
| 12 | -# notice, this list of conditions and the following disclaimer in the
|
|
| 13 | -# documentation and/or other materials provided with the distribution.
|
|
| 14 | -#
|
|
| 15 | -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
| 16 | -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
| 17 | -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
| 18 | -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
| 19 | -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
| 20 | -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
| 21 | -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
| 22 | -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
| 23 | -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
| 24 | -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
| 25 | -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
| 26 | - |
|
| 27 | -"""
|
|
| 28 | -This module contains examples of how to create your own commands, by
|
|
| 29 | -creating a new command container, bounded to a specific command host,
|
|
| 30 | -and defining a set of commands inside of it.
|
|
| 31 | - |
|
| 32 | -Keep in mind that this module is not being loaded from anywhere, so the
|
|
| 33 | -code in here will not be executed and commands defined here will not be
|
|
| 34 | -detected.
|
|
| 35 | -"""
|
|
| 36 | - |
|
| 37 | -from gajim.common.i18n import _
|
|
| 38 | -from gajim.command_system.framework import CommandContainer
|
|
| 39 | -from gajim.command_system.framework import command
|
|
| 40 | -from gajim.command_system.framework import doc
|
|
| 41 | -from gajim.command_system.implementation.hosts import ChatCommands
|
|
| 42 | -from gajim.command_system.implementation.hosts import PrivateChatCommands
|
|
| 43 | -from gajim.command_system.implementation.hosts import GroupChatCommands
|
|
| 44 | - |
|
| 45 | - |
|
| 46 | -class CustomCommonCommands(CommandContainer):
|
|
| 47 | - """
|
|
| 48 | - The AUTOMATIC class variable, set to a positive value, instructs the
|
|
| 49 | - command system to automatically discover the command container and
|
|
| 50 | - enable it.
|
|
| 51 | - |
|
| 52 | - This command container bounds to all three available in the default
|
|
| 53 | - implementation command hosts. This means that commands defined in
|
|
| 54 | - this container will be available to all: chat, private chat and a
|
|
| 55 | - group chat.
|
|
| 56 | - """
|
|
| 57 | - |
|
| 58 | - AUTOMATIC = True
|
|
| 59 | - HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands
|
|
| 60 | - |
|
| 61 | - @command
|
|
| 62 | - def dance(self):
|
|
| 63 | - """
|
|
| 64 | - First line of the doc string is called a description and will be
|
|
| 65 | - programmatically extracted and formatted.
|
|
| 66 | - |
|
| 67 | - After that you can give more help, like explanation of the
|
|
| 68 | - options. This one will be programmatically extracted and
|
|
| 69 | - formatted too.
|
|
| 70 | - |
|
| 71 | - After all the documentation - there will be autogenerated (based
|
|
| 72 | - on the method signature) usage information appended. You can
|
|
| 73 | - turn it off, if you want.
|
|
| 74 | - """
|
|
| 75 | - return "I don't dance."
|
|
| 76 | - |
|
| 77 | - |
|
| 78 | -class CustomChatCommands(CommandContainer):
|
|
| 79 | - """
|
|
| 80 | - This command container bounds only to the ChatCommands command host.
|
|
| 81 | - Therefore commands defined inside of the container will be available
|
|
| 82 | - only to a chat.
|
|
| 83 | - """
|
|
| 84 | - |
|
| 85 | - AUTOMATIC = True
|
|
| 86 | - HOSTS = (ChatCommands,)
|
|
| 87 | - |
|
| 88 | - @command("squal", "bawl")
|
|
| 89 | - def sing(self):
|
|
| 90 | - """
|
|
| 91 | - This command has an additional aliases. It means the command will
|
|
| 92 | - be available under three names: sing (the native name), squal
|
|
| 93 | - (the first alias), bawl (the second alias).
|
|
| 94 | - |
|
| 95 | - You can turn off the usage of the native name, if you want, and
|
|
| 96 | - specify a name or a set of names, as aliases, under which a
|
|
| 97 | - command will be available.
|
|
| 98 | - """
|
|
| 99 | - return "Buy yourself a stereo."
|
|
| 100 | - |
|
| 101 | - |
|
| 102 | -class CustomPrivateChatCommands(CommandContainer):
|
|
| 103 | - """
|
|
| 104 | - This command container bounds only to the PrivateChatCommands
|
|
| 105 | - command host. Therefore commands defined inside of the container
|
|
| 106 | - will be available only to a private chat.
|
|
| 107 | - """
|
|
| 108 | - |
|
| 109 | - AUTOMATIC = True
|
|
| 110 | - HOSTS = (PrivateChatCommands,)
|
|
| 111 | - |
|
| 112 | - @command
|
|
| 113 | - #Example string. Do not translate
|
|
| 114 | - @doc(_("The same as using a doc-string, except it supports translation"))
|
|
| 115 | - def make_coffee(self):
|
|
| 116 | - return "I'm not a coffee machine!"
|
|
| 117 | - |
|
| 118 | - |
|
| 119 | -class CustomGroupChatCommands(CommandContainer):
|
|
| 120 | - """
|
|
| 121 | - This command container bounds only to the GroupChatCommands command
|
|
| 122 | - host. Therefore commands defined inside of the container will be
|
|
| 123 | - available only to a group chat.
|
|
| 124 | - """
|
|
| 125 | - |
|
| 126 | - AUTOMATIC = True
|
|
| 127 | - HOSTS = (GroupChatCommands,)
|
|
| 128 | - |
|
| 129 | - @command
|
|
| 130 | - def fetch(self):
|
|
| 131 | - return "Buy yourself a dog." |
| 1 | -# Copyright (c) 2010, Alexander Cherniuk (ts33kr@gmail.com)
|
|
| 2 | -# All rights reserved.
|
|
| 3 | -#
|
|
| 4 | -# Redistribution and use in source and binary forms, with or without
|
|
| 5 | -# modification, are permitted provided that the following conditions
|
|
| 6 | -# are met:
|
|
| 7 | -#
|
|
| 8 | -# * Redistributions of source code must retain the above copyright
|
|
| 9 | -# notice, this list of conditions and the following disclaimer.
|
|
| 10 | -#
|
|
| 11 | -# * Redistributions in binary form must reproduce the above copyright
|
|
| 12 | -# notice, this list of conditions and the following disclaimer in the
|
|
| 13 | -# documentation and/or other materials provided with the distribution.
|
|
| 14 | -#
|
|
| 15 | -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
| 16 | -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
| 17 | -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
| 18 | -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
| 19 | -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
| 20 | -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
| 21 | -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
| 22 | -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
| 23 | -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
| 24 | -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
| 25 | -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
| 26 | - |
|
| 27 | -"""
|
|
| 28 | -Provides facilities to safely execute expressions inside a shell process
|
|
| 29 | -and capture the resulting output, in an asynchronous fashion, avoiding
|
|
| 30 | -deadlocks. If the process execution time reaches the threshold - it is
|
|
| 31 | -forced to terminate. Consists of a tiny framework and a couple of
|
|
| 32 | -commands as a frontend.
|
|
| 33 | -"""
|
|
| 34 | - |
|
| 35 | -from subprocess import Popen, PIPE
|
|
| 36 | -from os.path import expanduser
|
|
| 37 | - |
|
| 38 | -from gi.repository import GLib
|
|
| 39 | - |
|
| 40 | -from gajim.common import app
|
|
| 41 | -from gajim.common.i18n import _
|
|
| 42 | -from gajim.command_system.framework import CommandContainer
|
|
| 43 | -from gajim.command_system.framework import command
|
|
| 44 | -from gajim.command_system.framework import doc
|
|
| 45 | -from gajim.command_system.implementation.hosts import ChatCommands
|
|
| 46 | -from gajim.command_system.implementation.hosts import PrivateChatCommands
|
|
| 47 | -from gajim.command_system.implementation.hosts import GroupChatCommands
|
|
| 48 | - |
|
| 49 | - |
|
| 50 | -class Execute(CommandContainer):
|
|
| 51 | - AUTOMATIC = True
|
|
| 52 | - HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands
|
|
| 53 | - |
|
| 54 | - DIRECTORY = "~"
|
|
| 55 | - |
|
| 56 | - POLL_INTERVAL = 100
|
|
| 57 | - POLL_COUNT = 5
|
|
| 58 | - |
|
| 59 | - @command("exec", raw=True)
|
|
| 60 | - @doc(_("Execute expression inside a shell, show output"))
|
|
| 61 | - def execute(self, expression):
|
|
| 62 | - Execute.spawn(self, expression)
|
|
| 63 | - |
|
| 64 | - @classmethod
|
|
| 65 | - def spawn(cls, processor, expression):
|
|
| 66 | - command_system_execute = app.settings.get('command_system_execute')
|
|
| 67 | - if command_system_execute:
|
|
| 68 | - pipes = dict(stdout=PIPE, stderr=PIPE)
|
|
| 69 | - directory = expanduser(cls.DIRECTORY)
|
|
| 70 | - popen = Popen(expression, shell=True, cwd=directory, **pipes)
|
|
| 71 | - cls.monitor(processor, popen)
|
|
| 72 | - else:
|
|
| 73 | - processor.echo_error(
|
|
| 74 | - _('Command disabled. This command can be enabled by '
|
|
| 75 | - 'setting \'command_system_execute\' to True in ACE '
|
|
| 76 | - '(Advanced Configuration Editor).'))
|
|
| 77 | - return
|
|
| 78 | - |
|
| 79 | - @classmethod
|
|
| 80 | - def monitor(cls, processor, popen):
|
|
| 81 | - poller = cls.poller(processor, popen)
|
|
| 82 | - GLib.timeout_add(cls.POLL_INTERVAL, next, poller)
|
|
| 83 | - |
|
| 84 | - @classmethod
|
|
| 85 | - def poller(cls, processor, popen):
|
|
| 86 | - for _ in range(cls.POLL_COUNT):
|
|
| 87 | - yield cls.brush(processor, popen)
|
|
| 88 | - cls.overdue(processor, popen)
|
|
| 89 | - yield False
|
|
| 90 | - |
|
| 91 | - @classmethod
|
|
| 92 | - def brush(cls, processor, popen):
|
|
| 93 | - if popen.poll() is not None:
|
|
| 94 | - cls.terminated(processor, popen)
|
|
| 95 | - return False
|
|
| 96 | - return True
|
|
| 97 | - |
|
| 98 | - @classmethod
|
|
| 99 | - def terminated(cls, processor, popen):
|
|
| 100 | - stdout, stderr = cls.fetch(popen)
|
|
| 101 | - success = popen.returncode == 0
|
|
| 102 | - if success and stdout:
|
|
| 103 | - processor.echo(stdout)
|
|
| 104 | - elif not success and stderr:
|
|
| 105 | - processor.echo_error(stderr)
|
|
| 106 | - |
|
| 107 | - @classmethod
|
|
| 108 | - def overdue(cls, processor, popen):
|
|
| 109 | - popen.terminate()
|
|
| 110 | - |
|
| 111 | - @classmethod
|
|
| 112 | - def fetch(cls, popen):
|
|
| 113 | - data = popen.communicate()
|
|
| 114 | - return map(cls.clean, data)
|
|
| 115 | - |
|
| 116 | - @staticmethod
|
|
| 117 | - def clean(text):
|
|
| 118 | - strip = chr(10) + chr(32)
|
|
| 119 | - return text.decode().strip(strip)
|
|
| 120 | - |
|
| 121 | - |
|
| 122 | -class Show(Execute):
|
|
| 123 | - |
|
| 124 | - @command("sh", raw=True)
|
|
| 125 | - @doc(_("Execute expression inside a shell, send output"))
|
|
| 126 | - def show(self, expression):
|
|
| 127 | - Show.spawn(self, expression)
|
|
| 128 | - |
|
| 129 | - @classmethod
|
|
| 130 | - def terminated(cls, processor, popen):
|
|
| 131 | - stdout, stderr = cls.fetch(popen)
|
|
| 132 | - success = popen.returncode == 0
|
|
| 133 | - if success and stdout:
|
|
| 134 | - processor.send(stdout)
|
|
| 135 | - elif not success and stderr:
|
|
| 136 | - processor.echo_error(stderr) |
| 1 | -# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
|
|
| 2 | -#
|
|
| 3 | -# This program is free software: you can redistribute it and/or modify
|
|
| 4 | -# it under the terms of the GNU General Public License as published by
|
|
| 5 | -# the Free Software Foundation, either version 3 of the License, or
|
|
| 6 | -# (at your option) any later version.
|
|
| 7 | -#
|
|
| 8 | -# This program is distributed in the hope that it will be useful,
|
|
| 9 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
| 10 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
| 11 | -# GNU General Public License for more details.
|
|
| 12 | -#
|
|
| 13 | -# You should have received a copy of the GNU General Public License
|
|
| 14 | -# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
| 15 | - |
|
| 16 | -"""
|
|
| 17 | -The module defines a set of command hosts, which are bound to a
|
|
| 18 | -different command processors, which are the source of commands.
|
|
| 19 | -"""
|
|
| 20 | - |
|
| 21 | -from gajim.command_system.framework import CommandHost
|
|
| 22 | - |
|
| 23 | - |
|
| 24 | -class ChatCommands(CommandHost):
|
|
| 25 | - """
|
|
| 26 | - This command host is bound to the command processor which processes
|
|
| 27 | - commands from a chat.
|
|
| 28 | - """
|
|
| 29 | - AUTOMATIC = True
|
|
| 30 | - |
|
| 31 | - |
|
| 32 | -class PrivateChatCommands(CommandHost):
|
|
| 33 | - """
|
|
| 34 | - This command host is bound to the command processor which processes
|
|
| 35 | - commands from a private chat.
|
|
| 36 | - """
|
|
| 37 | - AUTOMATIC = True
|
|
| 38 | - |
|
| 39 | - |
|
| 40 | -class GroupChatCommands(CommandHost):
|
|
| 41 | - """
|
|
| 42 | - This command host is bound to the command processor which processes
|
|
| 43 | - commands from a group chat.
|
|
| 44 | - """
|
|
| 45 | - AUTOMATIC = True |
| 1 | -# Copyright (c) 2009-2010, Alexander Cherniuk (ts33kr@gmail.com)
|
|
| 2 | -# All rights reserved.
|
|
| 3 | -#
|
|
| 4 | -# Redistribution and use in source and binary forms, with or without
|
|
| 5 | -# modification, are permitted provided that the following conditions
|
|
| 6 | -# are met:
|
|
| 7 | -#
|
|
| 8 | -# * Redistributions of source code must retain the above copyright
|
|
| 9 | -# notice, this list of conditions and the following disclaimer.
|
|
| 10 | -#
|
|
| 11 | -# * Redistributions in binary form must reproduce the above copyright
|
|
| 12 | -# notice, this list of conditions and the following disclaimer in the
|
|
| 13 | -# documentation and/or other materials provided with the distribution.
|
|
| 14 | -#
|
|
| 15 | -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
| 16 | -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
| 17 | -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
| 18 | -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
| 19 | -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
| 20 | -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
| 21 | -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
| 22 | -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
| 23 | -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
| 24 | -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
| 25 | -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
| 26 | - |
|
| 27 | -"""
|
|
| 28 | -Provides a glue to tie command system framework and the actual code
|
|
| 29 | -where it would be dropped in. Defines a little bit of scaffolding to
|
|
| 30 | -support interaction between the two and a few utility methods so you
|
|
| 31 | -don't need to dig up the code itself to write basic commands.
|
|
| 32 | -"""
|
|
| 33 | - |
|
| 34 | -from traceback import print_exc
|
|
| 35 | - |
|
| 36 | -# from gi.repository import Pango
|
|
| 37 | - |
|
| 38 | -from gajim.common import app
|
|
| 39 | -from gajim.common.i18n import _
|
|
| 40 | - |
|
| 41 | -from gajim.command_system.framework import CommandProcessor
|
|
| 42 | -from gajim.command_system.errors import CommandError
|
|
| 43 | -from gajim.command_system.errors import NoCommandError
|
|
| 44 | - |
|
| 45 | - |
|
| 46 | -class ChatCommandProcessor(CommandProcessor):
|
|
| 47 | - """
|
|
| 48 | - A basic scaffolding to provide convenient interaction between the
|
|
| 49 | - command system and chat controls. It will be merged directly into
|
|
| 50 | - the controls, by ChatCommandProcessor being among superclasses of
|
|
| 51 | - the controls.
|
|
| 52 | - """
|
|
| 53 | - |
|
| 54 | - def process_as_command(self, text):
|
|
| 55 | - self.command_succeeded = False
|
|
| 56 | - parents = super(ChatCommandProcessor, self)
|
|
| 57 | - flag = parents.process_as_command(text)
|
|
| 58 | - if flag and self.command_succeeded:
|
|
| 59 | - self.clear_input()
|
|
| 60 | - return flag
|
|
| 61 | - |
|
| 62 | - def execute_command(self, name, arguments):
|
|
| 63 | - try:
|
|
| 64 | - parents = super(ChatCommandProcessor, self)
|
|
| 65 | - parents.execute_command(name, arguments)
|
|
| 66 | - except NoCommandError as error:
|
|
| 67 | - details = dict(name=error.name, message=error.message)
|
|
| 68 | - message = "%(name)s: %(message)s\n" % details
|
|
| 69 | - message += "Try using the //%(name)s or /say /%(name)s " % details
|
|
| 70 | - message += "construct if you intended to send it as a text."
|
|
| 71 | - self.echo_error(message)
|
|
| 72 | - except CommandError as error:
|
|
| 73 | - self.echo_error("%s: %s" % (error.name, error.message))
|
|
| 74 | - except Exception:
|
|
| 75 | - self.echo_error(_("Error during command execution!"))
|
|
| 76 | - print_exc()
|
|
| 77 | - else:
|
|
| 78 | - self.command_succeeded = True
|
|
| 79 | - |
|
| 80 | - def looks_like_command(self, text, body, name, arguments):
|
|
| 81 | - # Command escape stuff goes here. If text was prepended by the
|
|
| 82 | - # command prefix twice, like //not_a_command (if prefix is set
|
|
| 83 | - # to /) then it will be escaped, that is sent just as a regular
|
|
| 84 | - # message with one (only one) prefix removed, so message will be
|
|
| 85 | - # /not_a_command.
|
|
| 86 | - if body.startswith(self.COMMAND_PREFIX):
|
|
| 87 | - self.send(body)
|
|
| 88 | - return True
|
|
| 89 | - |
|
| 90 | - def command_preprocessor(self, command, name, arguments, args, kwargs):
|
|
| 91 | - # If command argument contain h or help option - forward it to
|
|
| 92 | - # the /help command. Don't forget to pass self, as all commands
|
|
| 93 | - # are unbound. And also don't forget to print output.
|
|
| 94 | - if 'h' in kwargs or 'help' in kwargs:
|
|
| 95 | - help_ = self.get_command('help')
|
|
| 96 | - self.echo(help_(self, name))
|
|
| 97 | - return True
|
|
| 98 | - |
|
| 99 | - def command_postprocessor(self, command, name, arguments, args, kwargs,
|
|
| 100 | - value):
|
|
| 101 | - # If command returns a string - print it to a user. A convenient
|
|
| 102 | - # and sufficient in most simple cases shortcut to a using echo.
|
|
| 103 | - if value and isinstance(value, str):
|
|
| 104 | - self.echo(value)
|
|
| 105 | - |
|
| 106 | - |
|
| 107 | -class CommandTools:
|
|
| 108 | - """
|
|
| 109 | - Contains a set of basic tools and shortcuts you can use in your
|
|
| 110 | - commands to perform some simple operations. These will be merged
|
|
| 111 | - directly into the controls, by CommandTools being among superclasses
|
|
| 112 | - of the controls.
|
|
| 113 | - """
|
|
| 114 | - |
|
| 115 | - def __init__(self):
|
|
| 116 | - pass
|
|
| 117 | - |
|
| 118 | - def echo(self, text, is_error=False):
|
|
| 119 | - """
|
|
| 120 | - Print given text to the user, as a regular command output.
|
|
| 121 | - """
|
|
| 122 | - self.conversation_view.add_command_output(text, is_error)
|
|
| 123 | - |
|
| 124 | - def echo_error(self, text):
|
|
| 125 | - """
|
|
| 126 | - Print given text to the user, as an error command output.
|
|
| 127 | - """
|
|
| 128 | - self.echo(text, is_error=True)
|
|
| 129 | - |
|
| 130 | - def send(self, text):
|
|
| 131 | - """
|
|
| 132 | - Send a message to the contact.
|
|
| 133 | - """
|
|
| 134 | - self.send_message(text, process_commands=False)
|
|
| 135 | - |
|
| 136 | - def set_input(self, text):
|
|
| 137 | - """
|
|
| 138 | - Set given text into the input.
|
|
| 139 | - """
|
|
| 140 | - buffer = self.msg_textview.get_buffer()
|
|
| 141 | - buffer.set_text(text)
|
|
| 142 | - |
|
| 143 | - def clear_input(self):
|
|
| 144 | - """
|
|
| 145 | - Clear input.
|
|
| 146 | - """
|
|
| 147 | - self.set_input(str())
|
|
| 148 | - |
|
| 149 | - @property
|
|
| 150 | - def connection(self):
|
|
| 151 | - """
|
|
| 152 | - Get the current connection object.
|
|
| 153 | - """
|
|
| 154 | - return app.connections[self.account]
|
|
| 155 | - |
|
| 156 | - @property
|
|
| 157 | - def full_jid(self):
|
|
| 158 | - """
|
|
| 159 | - Get a full JID of the contact.
|
|
| 160 | - """
|
|
| 161 | - return self.contact.jid |
| 1 | -# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
|
|
| 2 | -#
|
|
| 3 | -# This program is free software: you can redistribute it and/or modify
|
|
| 4 | -# it under the terms of the GNU General Public License as published by
|
|
| 5 | -# the Free Software Foundation, either version 3 of the License, or
|
|
| 6 | -# (at your option) any later version.
|
|
| 7 | -#
|
|
| 8 | -# This program is distributed in the hope that it will be useful,
|
|
| 9 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
| 10 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
| 11 | -# GNU General Public License for more details.
|
|
| 12 | -#
|
|
| 13 | -# You should have received a copy of the GNU General Public License
|
|
| 14 | -# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
| 15 | - |
|
| 16 | -"""
|
|
| 17 | -Provides an actual implementation for the standard commands.
|
|
| 18 | -"""
|
|
| 19 | - |
|
| 20 | -from time import localtime
|
|
| 21 | -from time import strftime
|
|
| 22 | -from datetime import date
|
|
| 23 | - |
|
| 24 | -from gi.repository import GLib
|
|
| 25 | - |
|
| 26 | -from gajim.common import app
|
|
| 27 | -from gajim.common import helpers
|
|
| 28 | -from gajim.common.i18n import _
|
|
| 29 | -from gajim.common.const import KindConstant
|
|
| 30 | - |
|
| 31 | -from gajim.command_system.errors import CommandError
|
|
| 32 | -from gajim.command_system.framework import CommandContainer
|
|
| 33 | -from gajim.command_system.framework import command
|
|
| 34 | -from gajim.command_system.framework import doc
|
|
| 35 | -from gajim.command_system.mapping import generate_usage
|
|
| 36 | - |
|
| 37 | -from gajim.command_system.implementation.hosts import ChatCommands
|
|
| 38 | -from gajim.command_system.implementation.hosts import PrivateChatCommands
|
|
| 39 | -from gajim.command_system.implementation.hosts import GroupChatCommands
|
|
| 40 | - |
|
| 41 | - |
|
| 42 | -class StandardCommonCommands(CommandContainer):
|
|
| 43 | - """
|
|
| 44 | - This command container contains standard commands which are common
|
|
| 45 | - to all - chat, private chat, group chat.
|
|
| 46 | - """
|
|
| 47 | - |
|
| 48 | - AUTOMATIC = True
|
|
| 49 | - HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands
|
|
| 50 | - |
|
| 51 | - @command(overlap=True)
|
|
| 52 | - @doc(_("Show help on a given command or a list of available commands if "
|
|
| 53 | - "-a is given"))
|
|
| 54 | - def help(self, cmd=None, all_=False):
|
|
| 55 | - if cmd:
|
|
| 56 | - cmd = self.get_command(cmd)
|
|
| 57 | - |
|
| 58 | - documentation = _(cmd.extract_documentation())
|
|
| 59 | - usage = generate_usage(cmd)
|
|
| 60 | - |
|
| 61 | - text = []
|
|
| 62 | - |
|
| 63 | - if documentation:
|
|
| 64 | - text.append(documentation)
|
|
| 65 | - if cmd.usage:
|
|
| 66 | - text.append(usage)
|
|
| 67 | - |
|
| 68 | - return '\n\n'.join(text)
|
|
| 69 | - |
|
| 70 | - if all_:
|
|
| 71 | - for cmd_ in self.list_commands():
|
|
| 72 | - names = ', '.join(cmd_.names)
|
|
| 73 | - description = cmd_.extract_description()
|
|
| 74 | - |
|
| 75 | - self.echo("%s - %s" % (names, description))
|
|
| 76 | - else:
|
|
| 77 | - help_ = self.get_command('help')
|
|
| 78 | - self.echo(help_(self, 'help'))
|
|
| 79 | - |
|
| 80 | - @command(raw=True)
|
|
| 81 | - @doc(_("Send a message to the contact"))
|
|
| 82 | - def say(self, message):
|
|
| 83 | - self.send(message)
|
|
| 84 | - |
|
| 85 | - @command(raw=True)
|
|
| 86 | - @doc(_("Send action (in the third person) to the current chat"))
|
|
| 87 | - def me(self, action):
|
|
| 88 | - self.send("/me %s" % action)
|
|
| 89 | - |
|
| 90 | - @command('lastlog', overlap=True)
|
|
| 91 | - @doc(_("Show logged messages which mention given text"))
|
|
| 92 | - def grep(self, text, limit=None):
|
|
| 93 | - results = app.storage.archive.search_log(self.account, self.contact.jid, text)
|
|
| 94 | - |
|
| 95 | - if not results:
|
|
| 96 | - raise CommandError(_("%s: Nothing found") % text)
|
|
| 97 | - |
|
| 98 | - if limit:
|
|
| 99 | - try:
|
|
| 100 | - results = results[len(results) - int(limit):]
|
|
| 101 | - except ValueError:
|
|
| 102 | - raise CommandError(_("Limit must be an integer"))
|
|
| 103 | - |
|
| 104 | - for row in results:
|
|
| 105 | - contact = row.contact_name
|
|
| 106 | - if not contact:
|
|
| 107 | - if row.kind == KindConstant.CHAT_MSG_SENT:
|
|
| 108 | - contact = app.nicks[self.account]
|
|
| 109 | - else:
|
|
| 110 | - contact = self.contact.name
|
|
| 111 | - |
|
| 112 | - time_obj = localtime(row.time)
|
|
| 113 | - date_obj = date.fromtimestamp(row.time)
|
|
| 114 | - date_ = strftime('%Y-%m-%d', time_obj)
|
|
| 115 | - time_ = strftime('%H:%M:%S', time_obj)
|
|
| 116 | - |
|
| 117 | - if date_obj == date.today():
|
|
| 118 | - formatted = "[%s] %s: %s" % (time_, contact, row.message)
|
|
| 119 | - else:
|
|
| 120 | - formatted = "[%s, %s] %s: %s" % (
|
|
| 121 | - date_, time_, contact, row.message)
|
|
| 122 | - |
|
| 123 | - self.echo(formatted)
|
|
| 124 | - |
|
| 125 | - @command(raw=True, empty=True)
|
|
| 126 | - # Do not translate online, away, chat, xa, dnd
|
|
| 127 | - @doc(_("""
|
|
| 128 | - Set the current status
|
|
| 129 | - |
|
| 130 | - Status can be given as one of the following values:
|
|
| 131 | - online, away, chat, xa, dnd.
|
|
| 132 | - """))
|
|
| 133 | - def status(self, status, message):
|
|
| 134 | - if status not in ('online', 'away', 'chat', 'xa', 'dnd'):
|
|
| 135 | - raise CommandError("Invalid status given")
|
|
| 136 | - for connection in app.connections.values():
|
|
| 137 | - if not app.settings.get_account_setting(connection.name,
|
|
| 138 | - 'sync_with_global_status'):
|
|
| 139 | - continue
|
|
| 140 | - if not connection.state.is_available:
|
|
| 141 | - continue
|
|
| 142 | - connection.change_status(status, message)
|
|
| 143 | - |
|
| 144 | - @command(raw=True, empty=True)
|
|
| 145 | - @doc(_("Set the current status to away"))
|
|
| 146 | - def away(self, message):
|
|
| 147 | - if not message:
|
|
| 148 | - message = _("Away")
|
|
| 149 | - |
|
| 150 | - for connection in app.connections.values():
|
|
| 151 | - if not app.settings.get_account_setting(connection.name,
|
|
| 152 | - 'sync_with_global_status'):
|
|
| 153 | - continue
|
|
| 154 | - if not connection.state.is_available:
|
|
| 155 | - continue
|
|
| 156 | - connection.change_status('away', message)
|
|
| 157 | - |
|
| 158 | - @command('back', raw=True, empty=True)
|
|
| 159 | - @doc(_("Set the current status to online"))
|
|
| 160 | - def online(self, message):
|
|
| 161 | - if not message:
|
|
| 162 | - message = _("Available")
|
|
| 163 | - |
|
| 164 | - for connection in app.connections.values():
|
|
| 165 | - if not app.settings.get_account_setting(connection.name,
|
|
| 166 | - 'sync_with_global_status'):
|
|
| 167 | - continue
|
|
| 168 | - if not connection.state.is_available:
|
|
| 169 | - continue
|
|
| 170 | - connection.change_status('online', message)
|
|
| 171 | - |
|
| 172 | - @command
|
|
| 173 | - @doc(_("Send a disco info request"))
|
|
| 174 | - def disco(self):
|
|
| 175 | - client = app.get_client(self.account)
|
|
| 176 | - if not client.state.is_available:
|
|
| 177 | - return
|
|
| 178 | - |
|
| 179 | - client.get_module('Discovery').disco_contact(self.contact)
|
|
| 180 | - |
|
| 181 | - |
|
| 182 | -class StandardCommonChatCommands(CommandContainer):
|
|
| 183 | - """
|
|
| 184 | - This command container contains standard commands, which are common
|
|
| 185 | - to a chat and a private chat only.
|
|
| 186 | - """
|
|
| 187 | - |
|
| 188 | - AUTOMATIC = True
|
|
| 189 | - HOSTS = ChatCommands, PrivateChatCommands
|
|
| 190 | - |
|
| 191 | - @command
|
|
| 192 | - @doc(_("Clear the text window"))
|
|
| 193 | - def clear(self):
|
|
| 194 | - self.conversation_view.clear()
|
|
| 195 | - |
|
| 196 | - @command
|
|
| 197 | - @doc(_("Send a ping to the contact"))
|
|
| 198 | - def ping(self):
|
|
| 199 | - app.connections[self.account].get_module('Ping').send_ping(self.contact)
|
|
| 200 | - |
|
| 201 | - @command
|
|
| 202 | - @doc(_("Send DTMF sequence through an open voice chat"))
|
|
| 203 | - def dtmf(self, sequence):
|
|
| 204 | - if not self.audio_sid:
|
|
| 205 | - raise CommandError(_("No open voice chats with the contact"))
|
|
| 206 | - for tone in sequence:
|
|
| 207 | - if not (tone in ("*", "#") or tone.isdigit()):
|
|
| 208 | - raise CommandError(_("%s is not a valid tone") % tone)
|
|
| 209 | - gjs = self.connection.get_module('Jingle').get_jingle_session
|
|
| 210 | - session = gjs(self.full_jid, self.audio_sid)
|
|
| 211 | - content = session.get_content("audio")
|
|
| 212 | - content.batch_dtmf(sequence)
|
|
| 213 | - |
|
| 214 | - @command
|
|
| 215 | - @doc(_("Toggle Voice Chat"))
|
|
| 216 | - def audio(self):
|
|
| 217 | - if not self.audio_available:
|
|
| 218 | - raise CommandError(_("Voice chats are not available"))
|
|
| 219 | - # An audio session is toggled by inverting the state of the
|
|
| 220 | - # appropriate button.
|
|
| 221 | - state = self._audio_button.get_active()
|
|
| 222 | - self._audio_button.set_active(not state)
|
|
| 223 | - |
|
| 224 | - @command
|
|
| 225 | - @doc(_("Toggle Video Chat"))
|
|
| 226 | - def video(self):
|
|
| 227 | - if not self.video_available:
|
|
| 228 | - raise CommandError(_("Video chats are not available"))
|
|
| 229 | - # A video session is toggled by inverting the state of the
|
|
| 230 | - # appropriate button.
|
|
| 231 | - state = self._video_button.get_active()
|
|
| 232 | - self._video_button.set_active(not state)
|
|
| 233 | - |
|
| 234 | - @command(raw=True)
|
|
| 235 | - @doc(_("Send a message to the contact that will attract their attention"))
|
|
| 236 | - def attention(self, message):
|
|
| 237 | - self.send_message(message, process_commands=False, attention=True)
|
|
| 238 | - |
|
| 239 | - |
|
| 240 | -class StandardChatCommands(CommandContainer):
|
|
| 241 | - """
|
|
| 242 | - This command container contains standard commands which are unique
|
|
| 243 | - to a chat.
|
|
| 244 | - """
|
|
| 245 | - |
|
| 246 | - AUTOMATIC = True
|
|
| 247 | - HOSTS = (ChatCommands,)
|
|
| 248 | - |
|
| 249 | - |
|
| 250 | -class StandardPrivateChatCommands(CommandContainer):
|
|
| 251 | - """
|
|
| 252 | - This command container contains standard commands which are unique
|
|
| 253 | - to a private chat.
|
|
| 254 | - """
|
|
| 255 | - |
|
| 256 | - AUTOMATIC = True
|
|
| 257 | - HOSTS = (PrivateChatCommands,)
|
|
| 258 | - |
|
| 259 | - |
|
| 260 | -class StandardGroupChatCommands(CommandContainer):
|
|
| 261 | - """
|
|
| 262 | - This command container contains standard commands which are unique
|
|
| 263 | - to a group chat.
|
|
| 264 | - """
|
|
| 265 | - |
|
| 266 | - AUTOMATIC = True
|
|
| 267 | - HOSTS = (GroupChatCommands,)
|
|
| 268 | - |
|
| 269 | - @command
|
|
| 270 | - @doc(_("Clear the text window"))
|
|
| 271 | - def clear(self):
|
|
| 272 | - self.conversation_view.clear()
|
|
| 273 | - |
|
| 274 | - @command(raw=True)
|
|
| 275 | - @doc(_("Change your nickname in a group chat"))
|
|
| 276 | - def nick(self, new_nick):
|
|
| 277 | - try:
|
|
| 278 | - new_nick = helpers.parse_resource(new_nick)
|
|
| 279 | - except Exception:
|
|
| 280 | - raise CommandError(_("Invalid nickname"))
|
|
| 281 | - # FIXME: Check state of MUC
|
|
| 282 | - self.connection.get_module('MUC').change_nick(
|
|
| 283 | - self.room_jid, new_nick)
|
|
| 284 | - self.new_nick = new_nick
|
|
| 285 | - |
|
| 286 | - @command('query', raw=True)
|
|
| 287 | - @doc(_("Open a private chat window with a specified participant"))
|
|
| 288 | - def chat(self, nick):
|
|
| 289 | - client = app.get_client(self.account)
|
|
| 290 | - groupchat_contact = client.get_module('Contacts').get_contact(
|
|
| 291 | - self.room_jid, groupchat=True)
|
|
| 292 | - nicks = groupchat_contact.get_user_nicknames()
|
|
| 293 | - if nick in nicks:
|
|
| 294 | - self.send_pm(nick)
|
|
| 295 | - else:
|
|
| 296 | - raise CommandError(_("Nickname not found"))
|
|
| 297 | - |
|
| 298 | - @command('msg', raw=True)
|
|
| 299 | - @doc(_("Open a private chat window with a specified participant and send "
|
|
| 300 | - "him a message"))
|
|
| 301 | - def message(self, nick, message):
|
|
| 302 | - client = app.get_client(self.account)
|
|
| 303 | - groupchat_contact = client.get_module('Contacts').get_contact(
|
|
| 304 | - self.room_jid, groupchat=True)
|
|
| 305 | - nicks = groupchat_contact.get_user_nicknames()
|
|
| 306 | - if nick in nicks:
|
|
| 307 | - self.send_pm(nick, message)
|
|
| 308 | - else:
|
|
| 309 | - raise CommandError(_("Nickname not found"))
|
|
| 310 | - |
|
| 311 | - @command(raw=True, empty=True)
|
|
| 312 | - @doc(_("Display or change a group chat topic"))
|
|
| 313 | - def topic(self, new_topic):
|
|
| 314 | - if new_topic:
|
|
| 315 | - self.connection.get_module('MUC').set_subject(
|
|
| 316 | - self.room_jid, new_topic)
|
|
| 317 | - else:
|
|
| 318 | - return self.subject
|
|
| 319 | - |
|
| 320 | - @command(raw=True, empty=True)
|
|
| 321 | - @doc(_("Invite a user to a group chat for a reason"))
|
|
| 322 | - def invite(self, jid, _reason):
|
|
| 323 | - control = app.window.get_control(self.account, self.room_jid)
|
|
| 324 | - if control is not None:
|
|
| 325 | - control.invite(jid)
|
|
| 326 | - |
|
| 327 | - @command(raw=True, empty=True)
|
|
| 328 | - @doc(_("Join a group chat given by an XMPP Address"))
|
|
| 329 | - def join(self, jid):
|
|
| 330 | - if '@' not in jid:
|
|
| 331 | - jid = jid + '@' + app.get_server_from_jid(self.room_jid)
|
|
| 332 | - |
|
| 333 | - app.app.activate_action(
|
|
| 334 | - 'groupchat-join',
|
|
| 335 | - GLib.Variant('as', [self.account, jid]))
|
|
| 336 | - |
|
| 337 | - @command('part', 'close')
|
|
| 338 | - @doc(_("Leave the group chat"))
|
|
| 339 | - def leave(self):
|
|
| 340 | - # Use idle_add to let command system finish printing
|
|
| 341 | - variant = GLib.Variant('as', [self.account, str(self.room_jid)])
|
|
| 342 | - GLib.idle_add(app.window.activate_action, 'remove-chat', variant)
|
|
| 343 | - |
|
| 344 | - @command(raw=True, empty=True)
|
|
| 345 | - @doc(_("""
|
|
| 346 | - Ban user by a nick or a JID from a groupchat
|
|
| 347 | - |
|
| 348 | - If given nickname is not found it will be treated as a JID.
|
|
| 349 | - """))
|
|
| 350 | - def ban(self, who, reason=''):
|
|
| 351 | - client = app.get_client(self.account)
|
|
| 352 | - groupchat_contact = client.get_module('Contacts').get_contact(
|
|
| 353 | - self.room_jid, groupchat=True)
|
|
| 354 | - nick_list = groupchat_contact.get_user_nicknames()
|
|
| 355 | - if who in nick_list:
|
|
| 356 | - contact = groupchat_contact.get_resource(who)
|
|
| 357 | - who = contact.jid
|
|
| 358 | - self.connection.get_module('MUC').set_affiliation(
|
|
| 359 | - self.room_jid,
|
|
| 360 | - {who: {'affiliation': 'outcast',
|
|
| 361 | - 'reason': reason}})
|
|
| 362 | - |
|
| 363 | - @command(raw=True, empty=True)
|
|
| 364 | - @doc(_("Kick user from group chat by nickname"))
|
|
| 365 | - def kick(self, who, reason):
|
|
| 366 | - client = app.get_client(self.account)
|
|
| 367 | - groupchat_contact = client.get_module('Contacts').get_contact(
|
|
| 368 | - self.room_jid, groupchat=True)
|
|
| 369 | - nick_list = groupchat_contact.get_user_nicknames()
|
|
| 370 | - if who not in nick_list:
|
|
| 371 | - raise CommandError(_("Nickname not found"))
|
|
| 372 | - self.connection.get_module('MUC').set_role(
|
|
| 373 | - self.room_jid, who, 'none', reason)
|
|
| 374 | - |
|
| 375 | - @command(raw=True)
|
|
| 376 | - # Do not translate moderator, participant, visitor, none
|
|
| 377 | - @doc(_("""Set participant role in group chat.
|
|
| 378 | - Role can be given as one of the following values:
|
|
| 379 | - moderator, participant, visitor, none"""))
|
|
| 380 | - def role(self, who, role):
|
|
| 381 | - if role not in ('moderator', 'participant', 'visitor', 'none'):
|
|
| 382 | - raise CommandError(_("Invalid role given"))
|
|
| 383 | - client = app.get_client(self.account)
|
|
| 384 | - groupchat_contact = client.get_module('Contacts').get_contact(
|
|
| 385 | - self.room_jid, groupchat=True)
|
|
| 386 | - nick_list = groupchat_contact.get_user_nicknames()
|
|
| 387 | - if who not in nick_list:
|
|
| 388 | - raise CommandError(_("Nickname not found"))
|
|
| 389 | - self.connection.get_module('MUC').set_role(self.room_jid, who, role)
|
|
| 390 | - |
|
| 391 | - @command(raw=True)
|
|
| 392 | - # Do not translate owner, admin, member, outcast, none
|
|
| 393 | - @doc(_("""Set participant affiliation in group chat.
|
|
| 394 | - Affiliation can be given as one of the following values:
|
|
| 395 | - owner, admin, member, outcast, none"""))
|
|
| 396 | - def affiliate(self, who, affiliation):
|
|
| 397 | - if affiliation not in ('owner', 'admin', 'member', 'outcast', 'none'):
|
|
| 398 | - raise CommandError(_("Invalid affiliation given"))
|
|
| 399 | - client = app.get_client(self.account)
|
|
| 400 | - groupchat_contact = client.get_module('Contacts').get_contact(
|
|
| 401 | - self.room_jid, groupchat=True)
|
|
| 402 | - nick_list = groupchat_contact.get_user_nicknames()
|
|
| 403 | - if who not in nick_list:
|
|
| 404 | - raise CommandError(_("Nickname not found"))
|
|
| 405 | - |
|
| 406 | - contact = groupchat_contact.get_resource(who)
|
|
| 407 | - |
|
| 408 | - self.connection.get_module('MUC').set_affiliation(
|
|
| 409 | - self.room_jid,
|
|
| 410 | - {contact.jid: {'affiliation': affiliation}})
|
|
| 411 | - |
|
| 412 | - @command
|
|
| 413 | - @doc(_("Display names of all group chat participants"))
|
|
| 414 | - def names(self, verbose=False):
|
|
| 415 | - client = app.get_client(self.account)
|
|
| 416 | - groupchat_contact = client.get_module('Contacts').get_contact(
|
|
| 417 | - self.room_jid, groupchat=True)
|
|
| 418 | - |
|
| 419 | - def get_contact(nick):
|
|
| 420 | - return groupchat_contact.get_resource(nick)
|
|
| 421 | - |
|
| 422 | - def get_role(nick):
|
|
| 423 | - return get_contact(nick).role
|
|
| 424 | - |
|
| 425 | - nicks = groupchat_contact.get_user_nicknames()
|
|
| 426 | - |
|
| 427 | - nicks = sorted(nicks)
|
|
| 428 | - nicks = sorted(nicks, key=get_role)
|
|
| 429 | - |
|
| 430 | - if not verbose:
|
|
| 431 | - return ", ".join(nicks)
|
|
| 432 | - |
|
| 433 | - for nick in nicks:
|
|
| 434 | - contact = get_contact(nick)
|
|
| 435 | - role = helpers.get_uf_role(contact.role)
|
|
| 436 | - affiliation = helpers.get_uf_affiliation(contact.affiliation)
|
|
| 437 | - self.echo("%s - %s - %s" % (nick, role, affiliation))
|
|
| 438 | - |
|
| 439 | - @command('ignore', raw=True)
|
|
| 440 | - @doc(_("Forbid a participant to send you public or private messages"))
|
|
| 441 | - def block(self, who):
|
|
| 442 | - self.on_block(None, who)
|
|
| 443 | - |
|
| 444 | - @command('unignore', raw=True)
|
|
| 445 | - @doc(_("Allow a participant to send you public or private messages"))
|
|
| 446 | - def unblock(self, who):
|
|
| 447 | - self.on_unblock(None, who)
|
|
| 448 | - |
|
| 449 | - @command
|
|
| 450 | - @doc(_("Send a ping to the contact"))
|
|
| 451 | - def ping(self, nick):
|
|
| 452 | - client = app.get_client(self.account)
|
|
| 453 | - groupchat_contact = client.get_module('Contacts').get_contact(
|
|
| 454 | - self.room_jid, groupchat=True)
|
|
| 455 | - nick_list = groupchat_contact.get_user_nicknames()
|
|
| 456 | - if nick not in nick_list:
|
|
| 457 | - raise CommandError(_("Unknown nickname"))
|
|
| 458 | - |
|
| 459 | - client.get_module('Ping').send_ping(
|
|
| 460 | - groupchat_contact.get_resource(nick)) |
| 1 | -# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
|
|
| 2 | -#
|
|
| 3 | -# This program is free software: you can redistribute it and/or modify
|
|
| 4 | -# it under the terms of the GNU General Public License as published by
|
|
| 5 | -# the Free Software Foundation, either version 3 of the License, or
|
|
| 6 | -# (at your option) any later version.
|
|
| 7 | -#
|
|
| 8 | -# This program is distributed in the hope that it will be useful,
|
|
| 9 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
| 10 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
| 11 | -# GNU General Public License for more details.
|
|
| 12 | -#
|
|
| 13 | -# You should have received a copy of the GNU General Public License
|
|
| 14 | -# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
| 15 | - |
|
| 16 | -"""
|
|
| 17 | -The module contains routines to parse command arguments and map them to
|
|
| 18 | -the command handler's positional and keyword arguments.
|
|
| 19 | - |
|
| 20 | -Mapping is done in two stages: 1) parse arguments into positional
|
|
| 21 | -arguments and options; 2) adapt them to the specific command handler
|
|
| 22 | -according to the command properties.
|
|
| 23 | -"""
|
|
| 24 | - |
|
| 25 | -import re
|
|
| 26 | -from operator import itemgetter
|
|
| 27 | - |
|
| 28 | -from gajim.common.i18n import _
|
|
| 29 | - |
|
| 30 | -from gajim.command_system.errors import DefinitionError
|
|
| 31 | -from gajim.command_system.errors import CommandError
|
|
| 32 | - |
|
| 33 | -# Quite complex piece of regular expression logic to parse options and
|
|
| 34 | -# arguments. Might need some tweaking along the way.
|
|
| 35 | -ARG_PATTERN = re.compile(r'(\'|")?(?P<body>(?(1).+?|\S+))(?(1)\1)')
|
|
| 36 | -OPT_PATTERN = re.compile(r'(?<!\w)--?(?P<key>[\w-]+)(?:(?:=|\s)(\'|")?(?P<value>(?(2)[^-]+?|[^-\s]+))(?(2)\2))?')
|
|
| 37 | - |
|
| 38 | -# Option keys needs to be encoded to a specific encoding as Python does
|
|
| 39 | -# not allow to expand dictionary with raw Unicode strings as keys from a
|
|
| 40 | -# **kwargs.
|
|
| 41 | -KEY_ENCODING = 'UTF-8'
|
|
| 42 | - |
|
| 43 | -# Defines how complete representation of command usage (generated based
|
|
| 44 | -# on command handler argument specification) will be rendered.
|
|
| 45 | -USAGE_PATTERN = 'Usage: %s %s'
|
|
| 46 | - |
|
| 47 | - |
|
| 48 | -def parse_arguments(arguments):
|
|
| 49 | - """
|
|
| 50 | - Simple yet effective and sufficient in most cases parser which
|
|
| 51 | - parses command arguments and returns them as two lists.
|
|
| 52 | - |
|
| 53 | - First list represents positional arguments as (argument, position),
|
|
| 54 | - and second representing options as (key, value, position) tuples,
|
|
| 55 | - where position is a (start, end) span tuple of where it was found in
|
|
| 56 | - the string.
|
|
| 57 | - |
|
| 58 | - Options may be given in --long or -short format. As --option=value
|
|
| 59 | - or --option value or -option value. Keys without values will get
|
|
| 60 | - None as value.
|
|
| 61 | - |
|
| 62 | - Arguments and option values that contain spaces may be given as 'one
|
|
| 63 | - two three' or "one two three"; that is between single or double
|
|
| 64 | - quotes.
|
|
| 65 | - """
|
|
| 66 | - args, opts = [], []
|
|
| 67 | - |
|
| 68 | - def intersects_opts(given_start, given_end):
|
|
| 69 | - """
|
|
| 70 | - Check if given span intersects with any of options.
|
|
| 71 | - """
|
|
| 72 | - for _key, _value, (start, end) in opts:
|
|
| 73 | - if given_start >= start and given_end <= end:
|
|
| 74 | - return True
|
|
| 75 | - return False
|
|
| 76 | - |
|
| 77 | - def intersects_args(given_start, given_end):
|
|
| 78 | - """
|
|
| 79 | - Check if given span intersects with any of arguments.
|
|
| 80 | - """
|
|
| 81 | - for _arg, (start, end) in args:
|
|
| 82 | - if given_start >= start and given_end <= end:
|
|
| 83 | - return True
|
|
| 84 | - return False
|
|
| 85 | - |
|
| 86 | - for match in re.finditer(OPT_PATTERN, arguments):
|
|
| 87 | - if match:
|
|
| 88 | - key = match.group('key')
|
|
| 89 | - value = match.group('value') or None
|
|
| 90 | - position = match.span()
|
|
| 91 | - opts.append((key, value, position))
|
|
| 92 | - |
|
| 93 | - for match in re.finditer(ARG_PATTERN, arguments):
|
|
| 94 | - if match:
|
|
| 95 | - body = match.group('body')
|
|
| 96 | - position = match.span()
|
|
| 97 | - args.append((body, position))
|
|
| 98 | - |
|
| 99 | - # Primitive but sufficiently effective way of disposing of
|
|
| 100 | - # conflicted sectors. Remove any arguments that intersect with
|
|
| 101 | - # options.
|
|
| 102 | - for arg, position in args[:]:
|
|
| 103 | - if intersects_opts(*position):
|
|
| 104 | - args.remove((arg, position))
|
|
| 105 | - |
|
| 106 | - # Primitive but sufficiently effective way of disposing of
|
|
| 107 | - # conflicted sectors. Remove any options that intersect with
|
|
| 108 | - # arguments.
|
|
| 109 | - for key, value, position in opts[:]:
|
|
| 110 | - if intersects_args(*position):
|
|
| 111 | - opts.remove((key, value, position))
|
|
| 112 | - |
|
| 113 | - return args, opts
|
|
| 114 | - |
|
| 115 | - |
|
| 116 | -def adapt_arguments(command, arguments, args, opts):
|
|
| 117 | - """
|
|
| 118 | - Adapt args and opts got from the parser to a specific handler by
|
|
| 119 | - means of arguments specified on command definition. That is
|
|
| 120 | - transform them to *args and **kwargs suitable for passing to a
|
|
| 121 | - command handler.
|
|
| 122 | - |
|
| 123 | - Dashes (-) in the option names will be converted to underscores. So
|
|
| 124 | - you can map --one-more-option to a one_more_option=None.
|
|
| 125 | - |
|
| 126 | - If the initial value of a keyword argument is a boolean (False in
|
|
| 127 | - most cases) - then this option will be treated as a switch, that is
|
|
| 128 | - an option which does not take an argument. If a switch is followed
|
|
| 129 | - by an argument - then this argument will be treated just like a
|
|
| 130 | - normal positional argument.
|
|
| 131 | - """
|
|
| 132 | - spec_args, spec_kwargs, var_args, _var_kwargs = command.extract_specification()
|
|
| 133 | - norm_kwargs = dict(spec_kwargs)
|
|
| 134 | - |
|
| 135 | - # Quite complex piece of neck-breaking logic to extract raw
|
|
| 136 | - # arguments if there is more, then one positional argument specified
|
|
| 137 | - # by the command. In case if it's just one argument which is the
|
|
| 138 | - # collector - this is fairly easy. But when it's more then one
|
|
| 139 | - # argument - the neck-breaking logic of how to retrieve residual
|
|
| 140 | - # arguments as a raw, all in one piece string, kicks in.
|
|
| 141 | - if command.raw:
|
|
| 142 | - if arguments:
|
|
| 143 | - spec_fix = 1 if command.source else 0
|
|
| 144 | - spec_len = len(spec_args) - spec_fix
|
|
| 145 | - arguments_end = len(arguments) - 1
|
|
| 146 | - |
|
| 147 | - # If there are any optional arguments given they should be
|
|
| 148 | - # either an unquoted positional argument or part of the raw
|
|
| 149 | - # argument. So we find all optional arguments that can
|
|
| 150 | - # possibly be unquoted argument and append them as is to the
|
|
| 151 | - # args.
|
|
| 152 | - for key, value, (start, end) in opts[:spec_len]:
|
|
| 153 | - if value:
|
|
| 154 | - end -= len(value) + 1
|
|
| 155 | - args.append((arguments[start:end], (start, end)))
|
|
| 156 | - args.append((value, (end, end + len(value) + 1)))
|
|
| 157 | - else:
|
|
| 158 | - args.append((arguments[start:end], (start, end)))
|
|
| 159 | - |
|
| 160 | - # We need in-place sort here because after manipulations
|
|
| 161 | - # with options order of arguments might be wrong and we just
|
|
| 162 | - # can't have more complex logic to not let that happen.
|
|
| 163 | - args.sort(key=itemgetter(1))
|
|
| 164 | - |
|
| 165 | - if spec_len > 1:
|
|
| 166 | - try:
|
|
| 167 | - _stopper, (start, end) = args[spec_len - 2]
|
|
| 168 | - except IndexError:
|
|
| 169 | - raise CommandError(_("Missing arguments"), command)
|
|
| 170 | - |
|
| 171 | - # The essential point of the whole play. After
|
|
| 172 | - # boundaries are being determined (supposedly correct)
|
|
| 173 | - # we separate raw part from the rest of arguments, which
|
|
| 174 | - # should be normally processed.
|
|
| 175 | - raw = arguments[end:]
|
|
| 176 | - raw = raw.strip() or None
|
|
| 177 | - |
|
| 178 | - if not raw and not command.empty:
|
|
| 179 | - raise CommandError(_("Missing arguments"), command)
|
|
| 180 | - |
|
| 181 | - # Discard residual arguments and all of the options as
|
|
| 182 | - # raw command does not support options and if an option
|
|
| 183 | - # is given it is rather a part of a raw argument.
|
|
| 184 | - args = args[:spec_len - 1]
|
|
| 185 | - opts = []
|
|
| 186 | - |
|
| 187 | - args.append((raw, (end, arguments_end)))
|
|
| 188 | - else:
|
|
| 189 | - # Substitute all of the arguments with only one, which
|
|
| 190 | - # contain raw and unprocessed arguments as a string. And
|
|
| 191 | - # discard all the options, as raw command does not
|
|
| 192 | - # support them.
|
|
| 193 | - args = [(arguments, (0, arguments_end))]
|
|
| 194 | - opts = []
|
|
| 195 | - else:
|
|
| 196 | - if command.empty:
|
|
| 197 | - args.append((None, (0, 0)))
|
|
| 198 | - else:
|
|
| 199 | - raise CommandError(_("Missing arguments"), command)
|
|
| 200 | - |
|
| 201 | - # The first stage of transforming options we have got to a format
|
|
| 202 | - # that can be used to associate them with declared keyword
|
|
| 203 | - # arguments. Substituting dashes (-) in their names with
|
|
| 204 | - # underscores (_).
|
|
| 205 | - for index, (key, value, position) in enumerate(opts):
|
|
| 206 | - if '-' in key:
|
|
| 207 | - opts[index] = (key.replace('-', '_'), value, position)
|
|
| 208 | - |
|
| 209 | - # The second stage of transforming options to an associable state.
|
|
| 210 | - # Expanding short, one-letter options to a verbose ones, if
|
|
| 211 | - # corresponding opt-in has been given.
|
|
| 212 | - if command.expand:
|
|
| 213 | - expanded = []
|
|
| 214 | - for spec_key in norm_kwargs.keys():
|
|
| 215 | - letter = spec_key[0] if len(spec_key) > 1 else None
|
|
| 216 | - if letter and letter not in expanded:
|
|
| 217 | - for index, (key, value, position) in enumerate(opts):
|
|
| 218 | - if key == letter:
|
|
| 219 | - expanded.append(letter)
|
|
| 220 | - opts[index] = (spec_key, value, position)
|
|
| 221 | - break
|
|
| 222 | - |
|
| 223 | - # Detect switches and set their values accordingly. If any of them
|
|
| 224 | - # carries a value - append it to args.
|
|
| 225 | - for index, (key, value, position) in enumerate(opts):
|
|
| 226 | - if isinstance(norm_kwargs.get(key), bool):
|
|
| 227 | - opts[index] = (key, True, position)
|
|
| 228 | - if value:
|
|
| 229 | - args.append((value, position))
|
|
| 230 | - |
|
| 231 | - # Sorting arguments and options (just to be sure) in regarding to
|
|
| 232 | - # their positions in the string.
|
|
| 233 | - args.sort(key=itemgetter(1))
|
|
| 234 | - opts.sort(key=itemgetter(2))
|
|
| 235 | - |
|
| 236 | - # Stripping down position information supplied with arguments and
|
|
| 237 | - # options as it won't be needed again.
|
|
| 238 | - args = list(map(lambda t: t[0], args))
|
|
| 239 | - opts = list(map(lambda t: (t[0], t[1]), opts))
|
|
| 240 | - |
|
| 241 | - # If command has extra option enabled - collect all extra arguments
|
|
| 242 | - # and pass them to a last positional argument command defines as a
|
|
| 243 | - # list.
|
|
| 244 | - if command.extra:
|
|
| 245 | - if not var_args:
|
|
| 246 | - spec_fix = 1 if not command.source else 2
|
|
| 247 | - spec_len = len(spec_args) - spec_fix
|
|
| 248 | - extra = args[spec_len:]
|
|
| 249 | - args = args[:spec_len]
|
|
| 250 | - args.append(extra)
|
|
| 251 | - else:
|
|
| 252 | - raise DefinitionError("Can not have both, extra and *args")
|
|
| 253 | - |
|
| 254 | - # Detect if positional arguments overlap keyword arguments. If so
|
|
| 255 | - # and this is allowed by command options - then map them directly to
|
|
| 256 | - # their options, so they can get proper further processing.
|
|
| 257 | - spec_fix = 1 if command.source else 0
|
|
| 258 | - spec_len = len(spec_args) - spec_fix
|
|
| 259 | - if len(args) > spec_len:
|
|
| 260 | - if command.overlap:
|
|
| 261 | - overlapped = args[spec_len:]
|
|
| 262 | - args = args[:spec_len]
|
|
| 263 | - for arg, spec_key, _spec_value in zip(overlapped, spec_kwargs):
|
|
| 264 | - opts.append((spec_key, arg))
|
|
| 265 | - else:
|
|
| 266 | - raise CommandError(_("Too many arguments"), command)
|
|
| 267 | - |
|
| 268 | - # Detect every switch and ensure it will not receive any arguments.
|
|
| 269 | - # Normally this does not happen unless overlapping is enabled.
|
|
| 270 | - for key, value in opts:
|
|
| 271 | - initial = norm_kwargs.get(key)
|
|
| 272 | - if isinstance(initial, bool):
|
|
| 273 | - if not isinstance(value, bool):
|
|
| 274 | - raise CommandError(
|
|
| 275 | - "%s: Switch can not take an argument" % key, command)
|
|
| 276 | - |
|
| 277 | - # Inject the source arguments as a string as a first argument, if
|
|
| 278 | - # command has enabled the corresponding option.
|
|
| 279 | - if command.source:
|
|
| 280 | - args.insert(0, arguments)
|
|
| 281 | - |
|
| 282 | - # Return *args and **kwargs in the form suitable for passing to a
|
|
| 283 | - # command handler and being expanded.
|
|
| 284 | - return tuple(args), dict(opts)
|
|
| 285 | - |
|
| 286 | - |
|
| 287 | -def generate_usage(command, complete=True):
|
|
| 288 | - """
|
|
| 289 | - Extract handler's arguments specification and wrap them in a
|
|
| 290 | - human-readable format usage information. If complete is given - then
|
|
| 291 | - USAGE_PATTERN will be used to render the specification completely.
|
|
| 292 | - """
|
|
| 293 | - spec_args, spec_kwargs, var_args, var_kwargs = command.extract_specification()
|
|
| 294 | - |
|
| 295 | - # Remove some special positional arguments from the specification,
|
|
| 296 | - # but store their names so they can be used for usage info
|
|
| 297 | - # generation.
|
|
| 298 | - _sp_source = spec_args.pop(0) if command.source else None
|
|
| 299 | - sp_extra = spec_args.pop() if command.extra else None
|
|
| 300 | - |
|
| 301 | - kwargs = []
|
|
| 302 | - letters = []
|
|
| 303 | - |
|
| 304 | - for key, value in spec_kwargs:
|
|
| 305 | - letter = key[0]
|
|
| 306 | - key = key.replace('_', '-')
|
|
| 307 | - |
|
| 308 | - if isinstance(value, bool):
|
|
| 309 | - value = str()
|
|
| 310 | - else:
|
|
| 311 | - value = '=%s' % value
|
|
| 312 | - |
|
| 313 | - if letter not in letters:
|
|
| 314 | - kwargs.append('-(-%s)%s%s' % (letter, key[1:], value))
|
|
| 315 | - letters.append(letter)
|
|
| 316 | - else:
|
|
| 317 | - kwargs.append('--%s%s' % (key, value))
|
|
| 318 | - |
|
| 319 | - usage = str()
|
|
| 320 | - args = str()
|
|
| 321 | - |
|
| 322 | - if command.raw:
|
|
| 323 | - spec_len = len(spec_args) - 1
|
|
| 324 | - if spec_len:
|
|
| 325 | - args += ('<%s>' % ', '.join(spec_args[:spec_len])) + ' '
|
|
| 326 | - args += ('(|%s|)' if command.empty else '|%s|') % spec_args[-1]
|
|
| 327 | - else:
|
|
| 328 | - if spec_args:
|
|
| 329 | - args += '<%s>' % ', '.join(spec_args)
|
|
| 330 | - if var_args or sp_extra:
|
|
| 331 | - args += (' ' if spec_args else str()) + '<<%s>>' % (
|
|
| 332 | - var_args or sp_extra)
|
|
| 333 | - |
|
| 334 | - usage += args
|
|
| 335 | - |
|
| 336 | - if kwargs or var_kwargs:
|
|
| 337 | - if kwargs:
|
|
| 338 | - usage += (' ' if args else str()) + '[%s]' % ', '.join(kwargs)
|
|
| 339 | - if var_kwargs:
|
|
| 340 | - usage += (' ' if args else str()) + '[[%s]]' % var_kwargs
|
|
| 341 | - |
|
| 342 | - # Native name will be the first one if it is included. Otherwise,
|
|
| 343 | - # names will be in the order they were specified.
|
|
| 344 | - if len(command.names) > 1:
|
|
| 345 | - names = '%s (%s)' % (command.first_name, ', '.join(command.names[1:]))
|
|
| 346 | - else:
|
|
| 347 | - names = command.first_name
|
|
| 348 | - |
|
| 349 | - return USAGE_PATTERN % (names, usage) if complete else usage |
| 1 | -# Copyright (c) 2010, Alexander Cherniuk (ts33kr@gmail.com)
|
|
| 2 | -# All rights reserved.
|
|
| 3 | -#
|
|
| 4 | -# Redistribution and use in source and binary forms, with or without
|
|
| 5 | -# modification, are permitted provided that the following conditions
|
|
| 6 | -# are met:
|
|
| 7 | -#
|
|
| 8 | -# * Redistributions of source code must retain the above copyright
|
|
| 9 | -# notice, this list of conditions and the following disclaimer.
|
|
| 10 | -#
|
|
| 11 | -# * Redistributions in binary form must reproduce the above copyright
|
|
| 12 | -# notice, this list of conditions and the following disclaimer in the
|
|
| 13 | -# documentation and/or other materials provided with the distribution.
|
|
| 14 | -#
|
|
| 15 | -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
| 16 | -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
| 17 | -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
| 18 | -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
| 19 | -# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
| 20 | -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
| 21 | -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
| 22 | -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
| 23 | -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
| 24 | -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
| 25 | -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
| 26 | - |
|
| 27 | - |
|
| 28 | -def remove(sequence, target):
|
|
| 29 | - if isinstance(sequence, list):
|
|
| 30 | - if target in sequence:
|
|
| 31 | - sequence.remove(target)
|
|
| 32 | - elif isinstance(sequence, dict):
|
|
| 33 | - if target in sequence:
|
|
| 34 | - del sequence[target] |
| ... | ... | @@ -45,7 +45,6 @@ class _ACCOUNT_DEFAULT: |
| 45 | 45 | 'change_roster_title',
|
| 46 | 46 | 'chat_merge_consecutive_nickname',
|
| 47 | 47 | 'check_for_update',
|
| 48 | - 'command_system_execute',
|
|
| 49 | 48 | 'confirm_close_muc',
|
| 50 | 49 | 'confirm_close_multiple_tabs',
|
| 51 | 50 | 'confirm_on_window_delete',
|
| ... | ... | @@ -191,7 +190,6 @@ class _ACCOUNT_DEFAULT: |
| 191 | 190 | 'chat_merge_consecutive_nickname': True,
|
| 192 | 191 | 'chat_timestamp_format': '%H:%M',
|
| 193 | 192 | 'check_for_update': True,
|
| 194 | - 'command_system_execute': False,
|
|
| 195 | 193 | 'confirm_block': '',
|
| 196 | 194 | 'confirm_close_muc': True,
|
| 197 | 195 | 'confirm_close_multiple_tabs': True,
|
| ... | ... | @@ -647,9 +645,6 @@ class WorkspaceSettings(TypedDict): |
| 647 | 645 | 'if there are multiple messages from the same sender within a '
|
| 648 | 646 | 'specific timespan.'),
|
| 649 | 647 | 'chat_timestamp_format': 'https://docs.python.org/3/library/time.html#time.strftime', # noqa: E501
|
| 650 | - 'command_system_execute': _(
|
|
| 651 | - 'If enabled, Gajim will execute commands '
|
|
| 652 | - '(/show, /sh, /execute, /exec).'),
|
|
| 653 | 648 | 'confirm_block': _(
|
| 654 | 649 | 'Show a confirmation dialog to block a contact? Empty string '
|
| 655 | 650 | 'means never show the dialog.'),
|
| ... | ... | @@ -7,7 +7,6 @@ exclude = |
| 7 | 7 | debian_build,
|
| 8 | 8 | test,
|
| 9 | 9 | typings,
|
| 10 | - gajim/command_system,
|
|
| 11 | 10 | gajim/common/config.py,
|
| 12 | 11 | gajim/common/optparser.py,
|
| 13 | 12 | gajim/common/socks5.py,
|