Skip to content

Bots: Add metadata scheme for bots. #66

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions zulip_bots/zulip_bots/bots/converter/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ class ConverterHandler(object):
the plugin, along with a list of all supported units.
'''

META = {
'name': 'Converter',
'default_commands_enabled': False,
}

def usage(self):
return '''
This plugin allows users to make conversions between
Expand Down
5 changes: 5 additions & 0 deletions zulip_bots/zulip_bots/bots/define/define.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ class DefineHandler(object):
looks for messages starting with '@mention-bot'.
'''

META = {
'name': 'Define',
'default_commands_enabled': False,
}

DEFINITION_API_URL = 'https://owlbot.info/api/v1/dictionary/{}?format=json'
REQUEST_ERROR_MESSAGE = 'Could not load definition.'
EMPTY_WORD_REQUEST_ERROR_MESSAGE = 'Please enter a word to define.'
Expand Down
5 changes: 5 additions & 0 deletions zulip_bots/zulip_bots/bots/encrypt/encrypt.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ class EncryptHandler(object):
It encrypts/decrypts messages starting with @mention-bot.
'''

META = {
'name': 'Encrypt',
'default_commands_enabled': False,
}

def usage(self):
return '''
This bot uses ROT13 encryption for its purposes.
Expand Down
5 changes: 5 additions & 0 deletions zulip_bots/zulip_bots/bots/followup/followup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ class FollowupHandler(object):
external issue tracker as well.
'''

META = {
'name': 'Followup',
'default_commands_enabled': False,
}

def usage(self):
return '''
This plugin will allow users to flag messages
Expand Down
6 changes: 6 additions & 0 deletions zulip_bots/zulip_bots/bots/helloworld/helloworld.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@


class HelloWorldHandler(object):

META = {
'name': 'Hello World',
'default_commands_enabled': False,
}

def usage(self):
return '''
This is a boilerplate bot that responds to a user query with
Expand Down
6 changes: 6 additions & 0 deletions zulip_bots/zulip_bots/bots/help/help.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# See readme.md for instructions on running this code.

class HelpHandler(object):

META = {
'name': 'Help',
'default_commands_enabled': False,
}

def usage(self):
return '''
This plugin will give info about Zulip to
Expand Down
5 changes: 5 additions & 0 deletions zulip_bots/zulip_bots/bots/incrementor/incrementor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

class IncrementorHandler(object):

META = {
'name': 'Incrementor',
'default_commands_enabled': False,
}

def usage(self):
return '''
This is a boilerplate bot that makes use of the
Expand Down
5 changes: 5 additions & 0 deletions zulip_bots/zulip_bots/bots/tictactoe/tictactoe.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,11 @@ class ticTacToeHandler(object):
"@mention-bot".
'''

META = {
'name': 'TicTacToe',
'default_commands_enabled': False,
}

def usage(self):
return '''
You can play tic-tac-toe with the computer now! Make sure your
Expand Down
6 changes: 6 additions & 0 deletions zulip_bots/zulip_bots/bots/virtual_fs/virtual_fs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
import os

class VirtualFsHandler(object):

META = {
'name': 'VirtualFS',
'default_commands_enabled': False,
}

def usage(self):
return get_help()

Expand Down
5 changes: 5 additions & 0 deletions zulip_bots/zulip_bots/bots/weather/weather.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
import json

class WeatherHandler(object):
META = {
'name': 'Weather',
'default_commands_enabled': False,
}

def initialize(self, bot_handler):
self.api_key = bot_handler.get_config_info('weather')['key']
self.response_pattern = 'Weather in {}, {}:\n{:.2f} F / {:.2f} C\n{}'
Expand Down
1 change: 1 addition & 0 deletions zulip_bots/zulip_bots/bots/wikipedia/wikipedia.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class WikipediaHandler(object):
META = {
'name': 'Wikipedia',
'description': 'Searches Wikipedia for a term and returns the top article.',
'default_commands_enabled': False,
}

def usage(self):
Expand Down
9 changes: 9 additions & 0 deletions zulip_bots/zulip_bots/bots/xkcd/xkcd.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import logging
import requests

from collections import OrderedDict

XKCD_TEMPLATE_URL = 'https://xkcd.com/%s/info.0.json'
LATEST_XKCD_URL = 'https://xkcd.com/info.0.json'

Expand All @@ -17,6 +19,13 @@ class XkcdHandler(object):
META = {
'name': 'XKCD',
'description': 'Fetches comic strips from https://xkcd.com.',
'default_commands_enabled': True,
'commands': OrderedDict([
('', ""), # Allow bot to handle blank commands; no help text
('latest', "Show the latest comic strip"),
('random', "Show a random comic strip"),
('<comic id>', "Show a comic strip with a specific 'comic id'"),
]) # NOTE: help not listed here, so default command used
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this bot, with this PR, help is now handled internally by the lib, so the default command is used instead as it stands. If 'help' was listed in commands then the bot wouldn't handle it and the help text would be as specified in the tuple.

}

def usage(self):
Expand Down
6 changes: 6 additions & 0 deletions zulip_bots/zulip_bots/bots/yoda/yoda.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ class YodaSpeakHandler(object):
This bot will allow users to translate a sentence into 'Yoda speak'.
It looks for messages starting with '@mention-bot'.
'''

META = {
'name': 'Yoda',
'default_commands_enabled': False,
}

def initialize(self, bot_handler):
self.api_key = bot_handler.get_config_info('yoda')['api_key']

Expand Down
79 changes: 74 additions & 5 deletions zulip_bots/zulip_bots/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

from zulip import Client

from collections import OrderedDict

def exit_gracefully(signum, frame):
# type: (int, Optional[Any]) -> None
sys.exit(0)
Expand Down Expand Up @@ -168,6 +170,66 @@ def is_private_message_from_another_user(message_dict, current_user_id):
return current_user_id != message_dict['sender_id']
return False

def setup_default_commands(bot_details, message_handler):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose this is the nucleus of the PR.

def default_empty_response():
return "You sent the bot an empty message; perhaps try 'about', 'help' or 'usage'."

def default_about_response():
if bot_details['description'] == "":
return "**{name}**".format(**bot_details)
return "**{name}**: {description}".format(**bot_details)

def default_commands_response():
return "**Commands**: {} {}".format(
" ".join(name for name in command_defaults if name),
" ".join(name for name in bot_details['commands'] if name))

def commands_list():
return ("\n".join("**{}** - {}".format(name, options['help'])
for name, options in command_defaults.items() if name) +
"\n" +
"\n".join("**{}** - {}".format(name, description)
for name, description in bot_details['commands'].items() if name))

def default_help_response():
return "{}\n{}\n{}".format(default_about_response(),
message_handler.usage(), commands_list())

command_defaults = OrderedDict([ # Variable definition required for callbacks above
('', {'action': default_empty_response,
'help': "[BLANK MESSAGE NOT SHOWN]"}),
('about', {'action': default_about_response,
'help': "The type and use of this bot"}),
('usage', {'action': lambda: message_handler.usage(),
'help': "Bot-provided usage text"}),
('commands', {'action': default_commands_response,
'help': "A short list of supported commands"}),
('help', {'action': default_help_response,
'help': "This help text"}),
])
return command_defaults

def updated_default_commands(default_commands, bot_details):
if not bot_details['default_commands_enabled']:
return OrderedDict()
exclude_list = bot_details['commands'] or ('commands', 'help')
updated = OrderedDict((name, option) for name, option in default_commands.items()
if name not in exclude_list)
# Update bot_details if updated is empty
if len(updated) == 0:
bot_details['default_commands_enabled'] = False
return updated

def get_bot_details(bot_class, bot_name):
Copy link
Contributor

@rht rht Oct 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTm for the commit ffc977a

bot_details = {
'name': bot_name.capitalize(),
'description': "",
'commands': {},
'default_commands_enabled': True,
}
bot_details.update(getattr(bot_class, 'META', {}))
return bot_details

def run_message_handler_for_bot(lib_module, quiet, config_file, bot_name):
# type: (Any, bool, str, str) -> Any
#
Expand All @@ -186,11 +248,11 @@ def run_message_handler_for_bot(lib_module, quiet, config_file, bot_name):
message_handler.initialize(bot_handler=restricted_client)

# Set default bot_details, then override from class, if provided
bot_details = {
'name': bot_name.capitalize(),
'description': "",
}
bot_details.update(getattr(lib_module.handler_class, 'META', {}))
bot_details = get_bot_details(message_handler, bot_name)

# Initialise default commands, then override & sync with bot_details
default_commands = setup_default_commands(bot_details, message_handler)
updated_defaults = updated_default_commands(default_commands, bot_details)

if not quiet:
print("Running {} Bot:".format(bot_details['name']))
Expand All @@ -216,6 +278,13 @@ def handle_message(message, flags):
return

if is_private_message or is_mentioned:
# Handle any default commands first
for command in updated_defaults:
if command == message['content']:
restricted_client.send_reply(message,
updated_defaults[command]['action']())
return
# ...then pass anything else to bot to deal with
message_handler.handle_message(
message=message,
bot_handler=restricted_client
Expand Down
20 changes: 17 additions & 3 deletions zulip_bots/zulip_bots/test_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@

from mock import MagicMock, patch

from zulip_bots.lib import StateHandler
from zulip_bots.lib import StateHandler, get_bot_details, setup_default_commands, updated_default_commands

import zulip_bots.lib
from six.moves import zip

Expand Down Expand Up @@ -94,8 +95,21 @@ def check_expected_responses(self, expectations, expected_method='send_reply',

def call_request(self, message, expected_method, response):
# type: (Dict[str, Any], str, Dict[str, Any]) -> None
# Send message to the concerned bot
self.message_handler.handle_message(message, self.mock_bot_handler)
bot_details = get_bot_details(self.message_handler, self.bot_name)

# Initialise default commands, then override & sync with bot_details
default_commands = setup_default_commands(bot_details, self.message_handler)
updated_defaults = updated_default_commands(default_commands, bot_details)

# Check if command handled in library first; if not, then delegate to bot
handled = False
for command in updated_defaults:
if command == message['content']:
self.MockClass().send_reply(message,
updated_defaults[command]['action']())
handled = True
if not handled:
self.message_handler.handle_message(message, self.mock_bot_handler)

# Check if the bot is sending a message via `send_message` function.
# Where response is a dictionary here.
Expand Down
24 changes: 20 additions & 4 deletions zulip_bots/zulip_bots/zulip_bot_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from mock import MagicMock, patch
from zulip_bots.lib import StateHandler
from zulip_bots.lib import ExternalBotHandler
from zulip_bots.lib import get_bot_details, setup_default_commands, updated_default_commands
from zulip_bots.provision import provision_bot
from zulip_bots.run import import_module_from_source

Expand Down Expand Up @@ -76,17 +77,32 @@ def main():
print("This module does not appear to have a bot handler_class specified.")
sys.exit(1)

bot_details = get_bot_details(message_handler, args.bot)

default_commands = setup_default_commands(bot_details, message_handler)
updated_defaults = updated_default_commands(default_commands, bot_details)

with patch('zulip.Client') as mock_client:
mock_bot_handler = ExternalBotHandler(mock_client, bot_dir) # type: Any
mock_bot_handler.send_reply = MagicMock()
mock_bot_handler.send_message = MagicMock()
mock_bot_handler.update_message = MagicMock()
if hasattr(message_handler, 'initialize') and callable(message_handler.initialize):
message_handler.initialize(mock_bot_handler)
message_handler.handle_message(
message=message,
bot_handler=mock_bot_handler
)

# Check if command handled in library first; if not, then delegate to bot
handled = False
for command in updated_defaults:
if command == message['content']:
mock_bot_handler.send_reply(message,
updated_defaults[command]['action']())
handled = True
if not handled:
message_handler.handle_message(
message=message,
bot_handler=mock_bot_handler
)

print("On sending {} bot the message \"{}\"".format(bot_name, args.message))
# send_reply and send_message have slightly arguments; the
# following takes that into account.
Expand Down