Skip to content

Commit

Permalink
Use the hidden=True argument on @bot.command to hide commands
Browse files Browse the repository at this point in the history
Before this commit, the only way to hide existing commands from the
/help command was to manage the bot.hide_commands list, but the
approach had some pitfalls: it was easy to override previous entries,
and there was no way for components to insert commands in it.

This new commit deprecates that old behavior, and refactors the whole
commands hiding system: now it's possible to pass hidden=True to
@bot.command (main component) or self.add_command (custom components)
and have the command hidden from the message.

Fixes: GH-19
  • Loading branch information
Pietro Albini committed May 14, 2016
1 parent b004e16 commit 70d88e9
Show file tree
Hide file tree
Showing 14 changed files with 342 additions and 143 deletions.
49 changes: 33 additions & 16 deletions botogram/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ def __init__(self, api_connection):

self.about = ""
self.owner = ""
self.hide_commands = ["start"]

self.before_help = []
self.after_help = []
Expand All @@ -43,6 +42,9 @@ def __init__(self, api_connection):
self._lang = ""
self._lang_inst = None

# Support for the old, deprecated bot.hide_commands
self._hide_commands = []

# Set the default language to english
self.lang = "en"

Expand Down Expand Up @@ -128,10 +130,11 @@ def __(func):
return func
return __

def command(self, name):
def command(self, name, hidden=False):
"""Register a new command"""
def __(func):
self._main_component.add_command(name, func, _from_main=True)
self._main_component.add_command(name, func, hidden,
_from_main=True)
return func
return __

Expand Down Expand Up @@ -193,17 +196,11 @@ def freeze(self):
chains = components.merge_chains(self._main_component,
*self._components)

# Get the list of commands for the bot
commands = self._components[-1]._get_commands()
for component in reversed(self._components[:-1]):
commands.update(component._get_commands())
commands.update(self._main_component._get_commands())

return frozenbot.FrozenBot(self.api, self.about, self.owner,
self.hide_commands, self.before_help,
self._hide_commands, self.before_help,
self.after_help, self.process_backlog,
self.lang, self.itself, self._commands_re,
commands, chains, self._scheduler,
self._commands, chains, self._scheduler,
self._main_component._component_id,
self._bot_id, self._shared_memory)

Expand All @@ -220,15 +217,35 @@ def lang(self, lang):
self._lang_inst = utils.get_language(lang)
self._lang = lang

def _get_commands(self):
@property
def _commands(self):
"""Get all the commands this bot implements"""
result = {}
for component in self._components:
result.update(component._get_commands())
result.update(self._main_component._get_commands())
# This is marked as a property so the available_commands method becomes
# dynamic (it's static on FrozenBot instead)
commands = self._components[-1]._get_commands()
for component in reversed(self._components[:-1]):
commands.update(component._get_commands())
commands.update(self._main_component._get_commands())

result = {}
for name, command in commands.items():
result[name] = command.for_bot(self)
return result

# These functions allows to use the old, deprecated bot.hide_commands

@property
@utils.deprecated("bot.hide_commands", "1.0", "Use @bot.command(\"name\", "
"hidden=True) instead")
def hide_commands(self):
return self._hide_commands

@hide_commands.setter
@utils.deprecated("bot.hide_commands", "1.0", "Use @bot.command(\"name\", "
"hidden=True) instead", back=1)
def hide_commands(self, value):
self._hide_commands = value


def create(api_key, *args, **kwargs):
"""Create a new bot"""
Expand Down
83 changes: 83 additions & 0 deletions botogram/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""
botogram.commands
Core logic for commands
Copyright (c) 2016 Pietro Albini <[email protected]>
Released under the MIT license
"""


class Command:
"""Representation of a single command"""

def __init__(self, hook, _bot=None):
# Get some parameters from the hook
self.name = hook._name
self.hidden = hook._hidden

self._hook = hook
self._component_id = hook.component_id

self._bot = _bot

def __reduce__(self):
return rebuild_command, (self._hook,)

def for_bot(self, bot):
"""Get the command instance for a specific bot"""
return self.__class__(self._hook, _bot=bot)

@property
def raw_docstring(self):
"""Get the raw docstring of this command"""
func = self._hook.func

if hasattr(func, "_botogram_help_message"):
if self._bot is not None:
return self._bot._call(func._botogram_help_message,
self._component_id)
else:
return func._botogram_help_message()
elif func.__doc__:
return func.__doc__

return

@property
def docstring(self):
"""Get the docstring of this command"""
docstring = self.raw_docstring
if docstring is None:
return

result = []
for line in self.raw_docstring.split("\n"):
# Remove leading whitespaces
line = line.strip()

# Allow only a single blackline
if line == "" and len(result) and result[-1] == "":
continue

result.append(line)

# Remove empty lines at the end or at the start of the docstring
for pos in 0, -1:
if result[pos] == "":
result.pop(pos)

return "\n".join(result)

@property
def summary(self):
"""Get a summary of the command"""
docstring = self.docstring
if docstring is None:
return

return docstring.split("\n", 1)[0]


def rebuild_command(hook):
"""Rebuild a Command after being pickled"""
return Command(hook)
10 changes: 7 additions & 3 deletions botogram/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from . import utils
from . import tasks
from . import hooks
from . import commands


class Component:
Expand Down Expand Up @@ -95,7 +96,7 @@ def add_message_matches_hook(self, regex, func, flags=0, multiple=False):
})
self.__processors.append(hook)

def add_command(self, name, func, _from_main=False):
def add_command(self, name, func, hidden=False, _from_main=False):
"""Register a new command"""
if name in self.__commands:
raise NameError("The command /%s already exists" % name)
Expand All @@ -110,8 +111,10 @@ def add_command(self, name, func, _from_main=False):

hook = hooks.CommandHook(func, self, {
"name": name,
"hidden": hidden,
})
self.__commands[name] = hook
command = commands.Command(hook)
self.__commands[name] = command

def add_timer(self, interval, func):
"""Register a new timer"""
Expand Down Expand Up @@ -157,7 +160,8 @@ def _get_chains(self):
"""Get the full hooks chain for this component"""
messages = [
self.__before_processors[:],
[self.__commands[name] for name in sorted(self.__commands.keys())],
[self.__commands[name]._hook
for name in sorted(self.__commands.keys())],
self.__no_commands[:],
self.__processors[:],
]
Expand Down
22 changes: 9 additions & 13 deletions botogram/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
Released under the MIT license
"""

from . import utils
from . import components
from . import decorators

Expand All @@ -17,7 +16,7 @@ class DefaultComponent(components.Component):
component_name = "botogram"

def __init__(self):
self.add_command("start", self.start_command)
self.add_command("start", self.start_command, hidden=True)
self.add_command("help", self.help_command)

self._add_no_commands_hook(self.no_commands_hook)
Expand All @@ -43,7 +42,7 @@ def _start_command_help(bot):
# /help command

def help_command(self, bot, chat, args):
commands = bot._get_commands()
commands = {cmd.name: cmd for cmd in bot.available_commands()}
if len(args) > 1:
message = [bot._("<b>Error!</b> The <code>/help</code> command "
"allows up to one argument.")]
Expand Down Expand Up @@ -77,14 +76,10 @@ def _help_generic_message(self, bot, commands):
if len(commands) > 0:
message.append(bot._("<b>This bot supports those commands:</b>"))
for name in sorted(commands.keys()):
# Allow to hide commands in the help message
if name in bot.hide_commands:
continue

func = commands[name]
docstring = utils.docstring_of(func, bot, format=True) \
.split("\n", 1)[0]
message.append("/%s <code>-</code> %s" % (name, docstring))
summary = commands[name].summary
if summary is None:
summary = "<i>%s</i>" % bot._("No description available.")
message.append("/%s <code>-</code> %s" % (name, summary))
message.append("")
message.append(bot._("You can also use <code>/help &lt;command&gt;"
"</code> to get help about a specific "
Expand All @@ -109,8 +104,9 @@ def _help_command_message(self, bot, commands, command):
"""Generate a command's help message"""
message = []

func = commands[command]
docstring = utils.docstring_of(func, bot, format=True)
docstring = commands[command].docstring
if docstring is None:
docstring = "<i>%s</i>" % bot._("No description available.")
message.append("/%s <code>-</code> %s" % (command, docstring))

# Show the owner informations
Expand Down
27 changes: 20 additions & 7 deletions botogram/frozenbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def __init__(self, api, about, owner, hide_commands, before_help,
self.api = api
self.about = about
self.owner = owner
self.hide_commands = hide_commands
self._hide_commands = hide_commands
self.before_help = before_help
self.after_help = after_help
self.process_backlog = process_backlog
Expand All @@ -43,7 +43,8 @@ def __init__(self, api, about, owner, hide_commands, before_help,
self._shared_memory = shared_memory
self._scheduler = scheduler
self._chains = chains
self._commands = commands
self._commands = {name: command.for_bot(self)
for name, command in commands.items()}

# Setup the logger
self.logger = logbook.Logger('botogram bot')
Expand All @@ -60,7 +61,7 @@ def __init__(self, api, about, owner, hide_commands, before_help,

def __reduce__(self):
args = (
self.api, self.about, self.owner, self.hide_commands,
self.api, self.about, self.owner, self._hide_commands,
self.before_help, self.after_help, self.process_backlog,
self.lang, self.itself, self._commands_re, self._commands,
self._chains, self._scheduler, self._main_component_id,
Expand Down Expand Up @@ -104,7 +105,7 @@ def message_matches(self, regex, flags=0, multiple=False):
"""Add a message matches hook"""
raise FrozenBotError("Can't add hooks to a bot at runtime")

def command(self, name):
def command(self, name, hidden=False):
"""Register a new command"""
raise FrozenBotError("Can't add commands to a bot at runtime")

Expand Down Expand Up @@ -222,9 +223,13 @@ def _(self, message, **args):

# And some internal methods used by botogram

def _get_commands(self):
"""Get all the commands this bot implements"""
return self._commands
def available_commands(self, all=False):
"""Get a list of the commands this bot implements"""
for command in self._commands.values():
# Remove `or command.name in self.hide_commands` in botogram 1.0
is_hidden = command.hidden or command.name in self._hide_commands
if all or not is_hidden:
yield command

def _call(self, func, component=None, **available):
"""Wrapper for calling user-provided functions"""
Expand All @@ -241,6 +246,14 @@ def lazy_shared():

return utils.call(func, **available)

# This function allows to use the old, deprecated bot.hide_commands

@utils.deprecated("bot.hide_commands", "1.0", "Use @bot.command(\"name\", "
"hidden=True) instead")
@property
def hide_commands(self):
return self._hide_commands


# Those are shortcuts to send messages directly to someone
# Create dynamic methods for each of the send methods. They're *really*
Expand Down
5 changes: 5 additions & 0 deletions botogram/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@ def _after_init(self, args):
self._regex = re.compile(r'^\/' + args["name"] + r'(@[a-zA-Z0-9_]+)?'
r'( .*)?$')

self._name = args["name"]
self._hidden = False
if "hidden" in args:
self._hidden = args["hidden"]

def _call(self, bot, update):
message = update.message
text = message.text.replace("\n", " ").replace("\t", " ")
Expand Down
Loading

0 comments on commit 70d88e9

Please sign in to comment.