black: Reformat skipping string normalization.

This commit is contained in:
PIG208 2021-05-28 17:03:46 +08:00 committed by Tim Abbott
parent 5580c68ae5
commit fba21bb00d
178 changed files with 6562 additions and 4469 deletions

View file

@ -52,7 +52,7 @@ package_info = dict(
entry_points={
'console_scripts': [
'zulip-run-bot=zulip_bots.run:main',
'zulip-terminal=zulip_bots.terminal:main'
'zulip-terminal=zulip_bots.terminal:main',
],
},
include_package_data=True,
@ -71,6 +71,7 @@ setuptools_info = dict(
try:
from setuptools import find_packages, setup
package_info.update(setuptools_info)
package_info['packages'] = find_packages()
package_info['package_data'] = package_data
@ -85,11 +86,13 @@ except ImportError:
try:
module = import_module(module_name) # type: Any
if version is not None:
assert(LooseVersion(module.__version__) >= LooseVersion(version))
assert LooseVersion(module.__version__) >= LooseVersion(version)
except (ImportError, AssertionError):
if version is not None:
print("{name}>={version} is not installed.".format(
name=module_name, version=version), file=sys.stderr)
print(
"{name}>={version} is not installed.".format(name=module_name, version=version),
file=sys.stderr,
)
else:
print("{name} is not installed.".format(name=module_name), file=sys.stderr)
sys.exit(1)

View file

@ -12,22 +12,29 @@ class BaremetricsHandler:
self.config_info = bot_handler.get_config_info('baremetrics')
self.api_key = self.config_info['api_key']
self.auth_header = {
'Authorization': 'Bearer ' + self.api_key
}
self.auth_header = {'Authorization': 'Bearer ' + self.api_key}
self.commands = ['help',
'list-commands',
'account-info',
'list-sources',
'list-plans <source_id>',
'list-customers <source_id>',
'list-subscriptions <source_id>',
'create-plan <source_id> <oid> <name> <currency> <amount> <interval> <interval_count>']
self.commands = [
'help',
'list-commands',
'account-info',
'list-sources',
'list-plans <source_id>',
'list-customers <source_id>',
'list-subscriptions <source_id>',
'create-plan <source_id> <oid> <name> <currency> <amount> <interval> <interval_count>',
]
self.descriptions = ['Display bot info', 'Display the list of available commands', 'Display the account info',
'List the sources', 'List the plans for the source', 'List the customers in the source',
'List the subscriptions in the source', 'Create a plan in the given source']
self.descriptions = [
'Display bot info',
'Display the list of available commands',
'Display the account info',
'List the sources',
'List the plans for the source',
'List the customers in the source',
'List the subscriptions in the source',
'Create a plan in the given source',
]
self.check_api_key(bot_handler)
@ -114,13 +121,16 @@ class BaremetricsHandler:
account_data = account_response.json()
account_data = account_data['account']
template = ['**Your account information:**',
'Id: {id}',
'Company: {company}',
'Default Currency: {currency}']
template = [
'**Your account information:**',
'Id: {id}',
'Company: {company}',
'Default Currency: {currency}',
]
return "\n".join(template).format(currency=account_data['default_currency']['name'],
**account_data)
return "\n".join(template).format(
currency=account_data['default_currency']['name'], **account_data
)
def get_sources(self) -> str:
url = 'https://api.baremetrics.com/v1/sources'
@ -131,9 +141,9 @@ class BaremetricsHandler:
response = '**Listing sources:** \n'
for index, source in enumerate(sources_data):
response += ('{_count}.ID: {id}\n'
'Provider: {provider}\n'
'Provider ID: {provider_id}\n\n').format(_count=index + 1, **source)
response += (
'{_count}.ID: {id}\n' 'Provider: {provider}\n' 'Provider ID: {provider_id}\n\n'
).format(_count=index + 1, **source)
return response
@ -144,19 +154,20 @@ class BaremetricsHandler:
plans_data = plans_response.json()
plans_data = plans_data['plans']
template = '\n'.join(['{_count}.Name: {name}',
'Active: {active}',
'Interval: {interval}',
'Interval Count: {interval_count}',
'Amounts:'])
template = '\n'.join(
[
'{_count}.Name: {name}',
'Active: {active}',
'Interval: {interval}',
'Interval Count: {interval_count}',
'Amounts:',
]
)
response = ['**Listing plans:**']
for index, plan in enumerate(plans_data):
response += (
[template.format(_count=index + 1, **plan)]
+ [
' - {amount} {currency}'.format(**amount)
for amount in plan['amounts']
]
+ [' - {amount} {currency}'.format(**amount) for amount in plan['amounts']]
+ ['']
)
@ -170,13 +181,17 @@ class BaremetricsHandler:
customers_data = customers_data['customers']
# FIXME BUG here? mismatch of name and display name?
template = '\n'.join(['{_count}.Name: {display_name}',
'Display Name: {name}',
'OID: {oid}',
'Active: {is_active}',
'Email: {email}',
'Notes: {notes}',
'Current Plans:'])
template = '\n'.join(
[
'{_count}.Name: {display_name}',
'Display Name: {name}',
'OID: {oid}',
'Active: {is_active}',
'Email: {email}',
'Notes: {notes}',
'Current Plans:',
]
)
response = ['**Listing customers:**']
for index, customer in enumerate(customers_data):
response += (
@ -194,13 +209,17 @@ class BaremetricsHandler:
subscriptions_data = subscriptions_response.json()
subscriptions_data = subscriptions_data['subscriptions']
template = '\n'.join(['{_count}.Customer Name: {name}',
'Customer Display Name: {display_name}',
'Customer OID: {oid}',
'Customer Email: {email}',
'Active: {_active}',
'Plan Name: {_plan_name}',
'Plan Amounts:'])
template = '\n'.join(
[
'{_count}.Customer Name: {name}',
'Customer Display Name: {display_name}',
'Customer OID: {oid}',
'Customer Email: {email}',
'Active: {_active}',
'Plan Name: {_plan_name}',
'Plan Amounts:',
]
)
response = ['**Listing subscriptions:**']
for index, subscription in enumerate(subscriptions_data):
response += (
@ -209,7 +228,7 @@ class BaremetricsHandler:
_count=index + 1,
_active=subscription['active'],
_plan_name=subscription['plan']['name'],
**subscription['customer']
**subscription['customer'],
)
]
+ [
@ -228,7 +247,7 @@ class BaremetricsHandler:
'currency': parameters[3],
'amount': int(parameters[4]),
'interval': parameters[5],
'interval_count': int(parameters[6])
'interval_count': int(parameters[6]),
} # type: Any
url = 'https://api.baremetrics.com/v1/{}/plans'.format(parameters[0])
@ -238,4 +257,5 @@ class BaremetricsHandler:
else:
return 'Invalid Arguments Error.'
handler_class = BaremetricsHandler

View file

@ -8,68 +8,83 @@ class TestBaremetricsBot(BotTestCase, DefaultTests):
bot_name = "baremetrics"
def test_bot_responds_to_empty_message(self) -> None:
with self.mock_config_info({'api_key': 'TEST'}), \
patch('requests.get'):
with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'):
self.verify_reply('', 'No Command Specified')
def test_help_query(self) -> None:
with self.mock_config_info({'api_key': 'TEST'}), \
patch('requests.get'):
self.verify_reply('help', '''
with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'):
self.verify_reply(
'help',
'''
This bot gives updates about customer behavior, financial performance, and analytics
for an organization using the Baremetrics Api.\n
Enter `list-commands` to show the list of available commands.
Version 1.0
''')
''',
)
def test_list_commands_command(self) -> None:
with self.mock_config_info({'api_key': 'TEST'}), \
patch('requests.get'):
self.verify_reply('list-commands', '**Available Commands:** \n'
' - help : Display bot info\n'
' - list-commands : Display the list of available commands\n'
' - account-info : Display the account info\n'
' - list-sources : List the sources\n'
' - list-plans <source_id> : List the plans for the source\n'
' - list-customers <source_id> : List the customers in the source\n'
' - list-subscriptions <source_id> : List the subscriptions in the '
'source\n'
' - create-plan <source_id> <oid> <name> <currency> <amount> <interval> '
'<interval_count> : Create a plan in the given source\n')
with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'):
self.verify_reply(
'list-commands',
'**Available Commands:** \n'
' - help : Display bot info\n'
' - list-commands : Display the list of available commands\n'
' - account-info : Display the account info\n'
' - list-sources : List the sources\n'
' - list-plans <source_id> : List the plans for the source\n'
' - list-customers <source_id> : List the customers in the source\n'
' - list-subscriptions <source_id> : List the subscriptions in the '
'source\n'
' - create-plan <source_id> <oid> <name> <currency> <amount> <interval> '
'<interval_count> : Create a plan in the given source\n',
)
def test_account_info_command(self) -> None:
with self.mock_config_info({'api_key': 'TEST'}):
with self.mock_http_conversation('account_info'):
self.verify_reply('account-info', '**Your account information:**\nId: 376418\nCompany: NA\nDefault '
'Currency: United States Dollar')
self.verify_reply(
'account-info',
'**Your account information:**\nId: 376418\nCompany: NA\nDefault '
'Currency: United States Dollar',
)
def test_list_sources_command(self) -> None:
with self.mock_config_info({'api_key': 'TEST'}):
with self.mock_http_conversation('list_sources'):
self.verify_reply('list-sources', '**Listing sources:** \n1.ID: 5f7QC5NC0Ywgcu\nProvider: '
'baremetrics\nProvider ID: None\n\n')
self.verify_reply(
'list-sources',
'**Listing sources:** \n1.ID: 5f7QC5NC0Ywgcu\nProvider: '
'baremetrics\nProvider ID: None\n\n',
)
def test_list_plans_command(self) -> None:
r = '**Listing plans:**\n1.Name: Plan 1\nActive: True\nInterval: year\nInterval Count: 1\nAmounts:\n' \
' - 450000 USD\n\n2.Name: Plan 2\nActive: True\nInterval: year\nInterval Count: 1\nAmounts:\n' \
r = (
'**Listing plans:**\n1.Name: Plan 1\nActive: True\nInterval: year\nInterval Count: 1\nAmounts:\n'
' - 450000 USD\n\n2.Name: Plan 2\nActive: True\nInterval: year\nInterval Count: 1\nAmounts:\n'
' - 450000 USD\n'
)
with self.mock_config_info({'api_key': 'TEST'}):
with self.mock_http_conversation('list_plans'):
self.verify_reply('list-plans TEST', r)
def test_list_customers_command(self) -> None:
r = '**Listing customers:**\n1.Name: Customer 1\nDisplay Name: Customer 1\nOID: customer_1\nActive: True\n' \
r = (
'**Listing customers:**\n1.Name: Customer 1\nDisplay Name: Customer 1\nOID: customer_1\nActive: True\n'
'Email: customer_1@baremetrics.com\nNotes: Here are some notes\nCurrent Plans:\n - Plan 1\n'
)
with self.mock_config_info({'api_key': 'TEST'}):
with self.mock_http_conversation('list_customers'):
self.verify_reply('list-customers TEST', r)
def test_list_subscriptions_command(self) -> None:
r = '**Listing subscriptions:**\n1.Customer Name: Customer 1\nCustomer Display Name: Customer 1\n' \
'Customer OID: customer_1\nCustomer Email: customer_1@baremetrics.com\nActive: True\n' \
r = (
'**Listing subscriptions:**\n1.Customer Name: Customer 1\nCustomer Display Name: Customer 1\n'
'Customer OID: customer_1\nCustomer Email: customer_1@baremetrics.com\nActive: True\n'
'Plan Name: Plan 1\nPlan Amounts:\n - 1000 $\n'
)
with self.mock_config_info({'api_key': 'TEST'}):
with self.mock_http_conversation('list_subscriptions'):
@ -84,34 +99,30 @@ class TestBaremetricsBot(BotTestCase, DefaultTests):
bot_test_instance.initialize(StubBotHandler())
def test_invalid_command(self) -> None:
with self.mock_config_info({'api_key': 'TEST'}), \
patch('requests.get'):
with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'):
self.verify_reply('abcd', 'Invalid Command.')
def test_missing_params(self) -> None:
with self.mock_config_info({'api_key': 'TEST'}), \
patch('requests.get'):
with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'):
self.verify_reply('list-plans', 'Missing Params.')
def test_key_error(self) -> None:
with self.mock_config_info({'api_key': 'TEST'}), \
patch('requests.get'):
with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'):
with self.mock_http_conversation('test_key_error'):
self.verify_reply('list-plans TEST', 'Invalid Response From API.')
def test_create_plan_command(self) -> None:
with self.mock_config_info({'api_key': 'TEST'}), \
patch('requests.get'):
with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'):
with self.mock_http_conversation('create_plan'):
self.verify_reply('create-plan TEST 1 TEST USD 123 TEST 123', 'Plan Created.')
def test_create_plan_error_command(self) -> None:
with self.mock_config_info({'api_key': 'TEST'}), \
patch('requests.get'):
with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'):
with self.mock_http_conversation('create_plan_error'):
self.verify_reply('create-plan TEST 1 TEST USD 123 TEST 123', 'Invalid Arguments Error.')
self.verify_reply(
'create-plan TEST 1 TEST USD 123 TEST 123', 'Invalid Arguments Error.'
)
def test_create_plan_argnum_error_command(self) -> None:
with self.mock_config_info({'api_key': 'TEST'}), \
patch('requests.get'):
with self.mock_config_info({'api_key': 'TEST'}), patch('requests.get'):
self.verify_reply('create-plan alpha beta', 'Invalid number of arguments.')

View file

@ -16,6 +16,7 @@ following the syntax shown below :smile:.\n \
\n* `comment`**:** Add a comment [**NOTE:** Optional field, default is *None*]\
'''
def get_beeminder_response(message_content: str, config_info: Dict[str, str]) -> str:
username = config_info['username']
goalname = config_info['goalname']
@ -25,37 +26,36 @@ def get_beeminder_response(message_content: str, config_info: Dict[str, str]) ->
if message_content == '' or message_content == 'help':
return help_message
url = "https://www.beeminder.com/api/v1/users/{}/goals/{}/datapoints.json".format(username, goalname)
url = "https://www.beeminder.com/api/v1/users/{}/goals/{}/datapoints.json".format(
username, goalname
)
message_pieces = message_content.split(',')
for i in range(len(message_pieces)):
message_pieces[i] = message_pieces[i].strip()
if (len(message_pieces) == 1):
payload = {
"value": message_pieces[0],
"auth_token": auth_token
}
elif (len(message_pieces) == 2):
if (message_pieces[1].isdigit()):
if len(message_pieces) == 1:
payload = {"value": message_pieces[0], "auth_token": auth_token}
elif len(message_pieces) == 2:
if message_pieces[1].isdigit():
payload = {
"daystamp": message_pieces[0],
"value": message_pieces[1],
"auth_token": auth_token
"auth_token": auth_token,
}
else:
payload = {
"value": message_pieces[0],
"comment": message_pieces[1],
"auth_token": auth_token
"auth_token": auth_token,
}
elif (len(message_pieces) == 3):
elif len(message_pieces) == 3:
payload = {
"daystamp": message_pieces[0],
"value": message_pieces[1],
"comment": message_pieces[2],
"auth_token": auth_token
"auth_token": auth_token,
}
elif (len(message_pieces) > 3):
elif len(message_pieces) > 3:
return "Make sure you follow the syntax.\n You can take a look \
at syntax by: @mention-botname help"
@ -63,13 +63,17 @@ at syntax by: @mention-botname help"
r = requests.post(url, json=payload)
if r.status_code != 200:
if r.status_code == 401: # Handles case of invalid key and missing key
if r.status_code == 401: # Handles case of invalid key and missing key
return "Error. Check your key!"
else:
return "Error occured : {}".format(r.status_code) # Occures in case of unprocessable entity
return "Error occured : {}".format(
r.status_code
) # Occures in case of unprocessable entity
else:
datapoint_link = "https://www.beeminder.com/{}/{}".format(username, goalname)
return "[Datapoint]({}) created.".format(datapoint_link) # Handles the case of successful datapoint creation
return "[Datapoint]({}) created.".format(
datapoint_link
) # Handles the case of successful datapoint creation
except ConnectionError as e:
logging.exception(str(e))
return "Uh-Oh, couldn't process the request \
@ -87,7 +91,9 @@ class BeeminderHandler:
# Check for valid auth_token
auth_token = self.config_info['auth_token']
try:
r = requests.get("https://www.beeminder.com/api/v1/users/me.json", params={'auth_token': auth_token})
r = requests.get(
"https://www.beeminder.com/api/v1/users/me.json", params={'auth_token': auth_token}
)
if r.status_code == 401:
bot_handler.quit('Invalid key!')
except ConnectionError as e:
@ -100,4 +106,5 @@ class BeeminderHandler:
response = get_beeminder_response(message['content'], self.config_info)
bot_handler.send_reply(message, response)
handler_class = BeeminderHandler

View file

@ -7,11 +7,7 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests, StubBotHandler, get_b
class TestBeeminderBot(BotTestCase, DefaultTests):
bot_name = "beeminder"
normal_config = {
"auth_token": "XXXXXX",
"username": "aaron",
"goalname": "goal"
}
normal_config = {"auth_token": "XXXXXX", "username": "aaron", "goalname": "goal"}
help_message = '''
You can add datapoints towards your beeminder goals \
@ -24,88 +20,94 @@ following the syntax shown below :smile:.\n \
'''
def test_bot_responds_to_empty_message(self) -> None:
with self.mock_config_info(self.normal_config), \
self.mock_http_conversation('test_valid_auth_token'):
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
'test_valid_auth_token'
):
self.verify_reply('', self.help_message)
def test_help_message(self) -> None:
with self.mock_config_info(self.normal_config), \
self.mock_http_conversation('test_valid_auth_token'):
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
'test_valid_auth_token'
):
self.verify_reply('help', self.help_message)
def test_message_with_daystamp_and_value(self) -> None:
bot_response = '[Datapoint](https://www.beeminder.com/aaron/goal) created.'
with self.mock_config_info(self.normal_config), \
self.mock_http_conversation('test_valid_auth_token'), \
self.mock_http_conversation('test_message_with_daystamp_and_value'):
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
'test_valid_auth_token'
), self.mock_http_conversation('test_message_with_daystamp_and_value'):
self.verify_reply('20180602, 2', bot_response)
def test_message_with_value_and_comment(self) -> None:
bot_response = '[Datapoint](https://www.beeminder.com/aaron/goal) created.'
with self.mock_config_info(self.normal_config), \
self.mock_http_conversation('test_valid_auth_token'), \
self.mock_http_conversation('test_message_with_value_and_comment'):
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
'test_valid_auth_token'
), self.mock_http_conversation('test_message_with_value_and_comment'):
self.verify_reply('2, hi there!', bot_response)
def test_message_with_daystamp_and_value_and_comment(self) -> None:
bot_response = '[Datapoint](https://www.beeminder.com/aaron/goal) created.'
with self.mock_config_info(self.normal_config), \
self.mock_http_conversation('test_valid_auth_token'), \
self.mock_http_conversation('test_message_with_daystamp_and_value_and_comment'):
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
'test_valid_auth_token'
), self.mock_http_conversation('test_message_with_daystamp_and_value_and_comment'):
self.verify_reply('20180602, 2, hi there!', bot_response)
def test_syntax_error(self) -> None:
with self.mock_config_info(self.normal_config), \
self.mock_http_conversation('test_valid_auth_token'):
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
'test_valid_auth_token'
):
bot_response = "Make sure you follow the syntax.\n You can take a look \
at syntax by: @mention-botname help"
self.verify_reply("20180303, 50, comment, redundant comment", bot_response)
def test_connection_error_when_handle_message(self) -> None:
with self.mock_config_info(self.normal_config), \
self.mock_http_conversation('test_valid_auth_token'), \
patch('requests.post', side_effect=ConnectionError()), \
patch('logging.exception'):
self.verify_reply('?$!', 'Uh-Oh, couldn\'t process the request \
right now.\nPlease try again later')
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
'test_valid_auth_token'
), patch('requests.post', side_effect=ConnectionError()), patch('logging.exception'):
self.verify_reply(
'?$!',
'Uh-Oh, couldn\'t process the request \
right now.\nPlease try again later',
)
def test_invalid_when_handle_message(self) -> None:
get_bot_message_handler(self.bot_name)
StubBotHandler()
with self.mock_config_info({'auth_token': 'someInvalidKey',
'username': 'aaron',
'goalname': 'goal'}), \
patch('requests.get', side_effect=ConnectionError()), \
self.mock_http_conversation('test_invalid_when_handle_message'), \
patch('logging.exception'):
with self.mock_config_info(
{'auth_token': 'someInvalidKey', 'username': 'aaron', 'goalname': 'goal'}
), patch('requests.get', side_effect=ConnectionError()), self.mock_http_conversation(
'test_invalid_when_handle_message'
), patch(
'logging.exception'
):
self.verify_reply('5', 'Error. Check your key!')
def test_error(self) -> None:
bot_request = 'notNumber'
bot_response = "Error occured : 422"
with self.mock_config_info(self.normal_config), \
self.mock_http_conversation('test_valid_auth_token'), \
self.mock_http_conversation('test_error'):
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
'test_valid_auth_token'
), self.mock_http_conversation('test_error'):
self.verify_reply(bot_request, bot_response)
def test_invalid_when_initialize(self) -> None:
bot = get_bot_message_handler(self.bot_name)
bot_handler = StubBotHandler()
with self.mock_config_info({'auth_token': 'someInvalidKey',
'username': 'aaron',
'goalname': 'goal'}), \
self.mock_http_conversation('test_invalid_when_initialize'), \
self.assertRaises(bot_handler.BotQuitException):
with self.mock_config_info(
{'auth_token': 'someInvalidKey', 'username': 'aaron', 'goalname': 'goal'}
), self.mock_http_conversation('test_invalid_when_initialize'), self.assertRaises(
bot_handler.BotQuitException
):
bot.initialize(bot_handler)
def test_connection_error_during_initialize(self) -> None:
bot = get_bot_message_handler(self.bot_name)
bot_handler = StubBotHandler()
with self.mock_config_info(self.normal_config), \
patch('requests.get', side_effect=ConnectionError()), \
patch('logging.exception') as mock_logging:
with self.mock_config_info(self.normal_config), patch(
'requests.get', side_effect=ConnectionError()
), patch('logging.exception') as mock_logging:
bot.initialize(bot_handler)
self.assertTrue(mock_logging.called)

View file

@ -8,12 +8,11 @@ import chess.uci
from zulip_bots.lib import BotHandler
START_REGEX = re.compile('start with other user$')
START_COMPUTER_REGEX = re.compile(
'start as (?P<user_color>white|black) with computer'
)
START_COMPUTER_REGEX = re.compile('start as (?P<user_color>white|black) with computer')
MOVE_REGEX = re.compile('do (?P<move_san>.+)$')
RESIGN_REGEX = re.compile('resign$')
class ChessHandler:
def usage(self) -> str:
return (
@ -29,20 +28,14 @@ class ChessHandler:
self.config_info = bot_handler.get_config_info('chess')
try:
self.engine = chess.uci.popen_engine(
self.config_info['stockfish_location']
)
self.engine = chess.uci.popen_engine(self.config_info['stockfish_location'])
self.engine.uci()
except FileNotFoundError:
# It is helpful to allow for fake Stockfish locations if the bot
# runner is testing or knows they won't be using an engine.
print('That Stockfish doesn\'t exist. Continuing.')
def handle_message(
self,
message: Dict[str, str],
bot_handler: BotHandler
) -> None:
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
content = message['content']
if content == '':
@ -60,7 +53,8 @@ class ChessHandler:
if bot_handler.storage.contains('is_with_computer'):
is_with_computer = (
# `bot_handler`'s `storage` only accepts `str` values.
bot_handler.storage.get('is_with_computer') == str(True)
bot_handler.storage.get('is_with_computer')
== str(True)
)
if bot_handler.storage.contains('last_fen'):
@ -70,31 +64,17 @@ class ChessHandler:
self.start(message, bot_handler)
elif start_computer_regex_match:
self.start_computer(
message,
bot_handler,
start_computer_regex_match.group('user_color') == 'white'
message, bot_handler, start_computer_regex_match.group('user_color') == 'white'
)
elif move_regex_match:
if is_with_computer:
self.move_computer(
message,
bot_handler,
last_fen,
move_regex_match.group('move_san')
message, bot_handler, last_fen, move_regex_match.group('move_san')
)
else:
self.move(
message,
bot_handler,
last_fen,
move_regex_match.group('move_san')
)
self.move(message, bot_handler, last_fen, move_regex_match.group('move_san'))
elif resign_regex_match:
self.resign(
message,
bot_handler,
last_fen
)
self.resign(message, bot_handler, last_fen)
def start(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
"""Starts a game with another user, with the current user as white.
@ -105,10 +85,7 @@ class ChessHandler:
- bot_handler: The Zulip Bots bot handler object.
"""
new_board = chess.Board()
bot_handler.send_reply(
message,
make_start_reponse(new_board)
)
bot_handler.send_reply(message, make_start_reponse(new_board))
# `bot_handler`'s `storage` only accepts `str` values.
bot_handler.storage.put('is_with_computer', str(False))
@ -116,10 +93,7 @@ class ChessHandler:
bot_handler.storage.put('last_fen', new_board.fen())
def start_computer(
self,
message: Dict[str, str],
bot_handler: BotHandler,
is_white_user: bool
self, message: Dict[str, str], bot_handler: BotHandler, is_white_user: bool
) -> None:
"""Starts a game with the computer. Replies to the bot handler.
@ -135,10 +109,7 @@ class ChessHandler:
new_board = chess.Board()
if is_white_user:
bot_handler.send_reply(
message,
make_start_computer_reponse(new_board)
)
bot_handler.send_reply(message, make_start_computer_reponse(new_board))
# `bot_handler`'s `storage` only accepts `str` values.
bot_handler.storage.put('is_with_computer', str(True))
@ -152,10 +123,7 @@ class ChessHandler:
)
def validate_board(
self,
message: Dict[str, str],
bot_handler: BotHandler,
fen: str
self, message: Dict[str, str], bot_handler: BotHandler, fen: str
) -> Optional[chess.Board]:
"""Validates a board based on its FEN string. Replies to the bot
handler if there is an error with the board.
@ -171,10 +139,7 @@ class ChessHandler:
try:
last_board = chess.Board(fen)
except ValueError:
bot_handler.send_reply(
message,
make_copied_wrong_response()
)
bot_handler.send_reply(message, make_copied_wrong_response())
return None
return last_board
@ -185,7 +150,7 @@ class ChessHandler:
bot_handler: BotHandler,
last_board: chess.Board,
move_san: str,
is_computer: object
is_computer: object,
) -> Optional[chess.Move]:
"""Validates a move based on its SAN string and the current board.
Replies to the bot handler if there is an error with the move.
@ -205,29 +170,17 @@ class ChessHandler:
try:
move = last_board.parse_san(move_san)
except ValueError:
bot_handler.send_reply(
message,
make_not_legal_response(
last_board,
move_san
)
)
bot_handler.send_reply(message, make_not_legal_response(last_board, move_san))
return None
if move not in last_board.legal_moves:
bot_handler.send_reply(
message,
make_not_legal_response(last_board, move_san)
)
bot_handler.send_reply(message, make_not_legal_response(last_board, move_san))
return None
return move
def check_game_over(
self,
message: Dict[str, str],
bot_handler: BotHandler,
new_board: chess.Board
self, message: Dict[str, str], bot_handler: BotHandler, new_board: chess.Board
) -> bool:
"""Checks if a game is over due to
- checkmate,
@ -254,38 +207,24 @@ class ChessHandler:
game_over_output = ''
if new_board.is_checkmate():
game_over_output = make_loss_response(
new_board,
'was checkmated'
)
game_over_output = make_loss_response(new_board, 'was checkmated')
elif new_board.is_stalemate():
game_over_output = make_draw_response('stalemate')
elif new_board.is_insufficient_material():
game_over_output = make_draw_response(
'insufficient material'
)
game_over_output = make_draw_response('insufficient material')
elif new_board.can_claim_fifty_moves():
game_over_output = make_draw_response(
'50 moves without a capture or pawn move'
)
game_over_output = make_draw_response('50 moves without a capture or pawn move')
elif new_board.can_claim_threefold_repetition():
game_over_output = make_draw_response('3-fold repetition')
bot_handler.send_reply(
message,
game_over_output
)
bot_handler.send_reply(message, game_over_output)
return True
return False
def move(
self,
message: Dict[str, str],
bot_handler: BotHandler,
last_fen: str,
move_san: str
self, message: Dict[str, str], bot_handler: BotHandler, last_fen: str, move_san: str
) -> None:
"""Makes a move for a user in a game with another user. Replies to
the bot handler.
@ -301,13 +240,7 @@ class ChessHandler:
if not last_board:
return
move = self.validate_move(
message,
bot_handler,
last_board,
move_san,
False
)
move = self.validate_move(message, bot_handler, last_board, move_san, False)
if not move:
return
@ -318,19 +251,12 @@ class ChessHandler:
if self.check_game_over(message, bot_handler, new_board):
return
bot_handler.send_reply(
message,
make_move_reponse(last_board, new_board, move)
)
bot_handler.send_reply(message, make_move_reponse(last_board, new_board, move))
bot_handler.storage.put('last_fen', new_board.fen())
def move_computer(
self,
message: Dict[str, str],
bot_handler: BotHandler,
last_fen: str,
move_san: str
self, message: Dict[str, str], bot_handler: BotHandler, last_fen: str, move_san: str
) -> None:
"""Preforms a move for a user in a game with the computer and then
makes the computer's move. Replies to the bot handler. Unlike `move`,
@ -350,13 +276,7 @@ class ChessHandler:
if not last_board:
return
move = self.validate_move(
message,
bot_handler,
last_board,
move_san,
True
)
move = self.validate_move(message, bot_handler, last_board, move_san, True)
if not move:
return
@ -367,40 +287,22 @@ class ChessHandler:
if self.check_game_over(message, bot_handler, new_board):
return
computer_move = calculate_computer_move(
new_board,
self.engine
)
computer_move = calculate_computer_move(new_board, self.engine)
new_board_after_computer_move = copy.copy(new_board)
new_board_after_computer_move.push(computer_move)
if self.check_game_over(
message,
bot_handler,
new_board_after_computer_move
):
if self.check_game_over(message, bot_handler, new_board_after_computer_move):
return
bot_handler.send_reply(
message,
make_move_reponse(
new_board,
new_board_after_computer_move,
computer_move
)
message, make_move_reponse(new_board, new_board_after_computer_move, computer_move)
)
bot_handler.storage.put(
'last_fen',
new_board_after_computer_move.fen()
)
bot_handler.storage.put('last_fen', new_board_after_computer_move.fen())
def move_computer_first(
self,
message: Dict[str, str],
bot_handler: BotHandler,
last_fen: str
self, message: Dict[str, str], bot_handler: BotHandler, last_fen: str
) -> None:
"""Preforms a move for the computer without having the user go first in
a game with the computer. Replies to the bot handler. Like
@ -415,44 +317,24 @@ class ChessHandler:
"""
last_board = self.validate_board(message, bot_handler, last_fen)
computer_move = calculate_computer_move(
last_board,
self.engine
)
computer_move = calculate_computer_move(last_board, self.engine)
new_board_after_computer_move = copy.copy(last_board) # type: chess.Board
new_board_after_computer_move.push(computer_move)
if self.check_game_over(
message,
bot_handler,
new_board_after_computer_move
):
if self.check_game_over(message, bot_handler, new_board_after_computer_move):
return
bot_handler.send_reply(
message,
make_move_reponse(
last_board,
new_board_after_computer_move,
computer_move
)
message, make_move_reponse(last_board, new_board_after_computer_move, computer_move)
)
bot_handler.storage.put(
'last_fen',
new_board_after_computer_move.fen()
)
bot_handler.storage.put('last_fen', new_board_after_computer_move.fen())
# `bot_handler`'s `storage` only accepts `str` values.
bot_handler.storage.put('is_with_computer', str(True))
def resign(
self,
message: Dict[str, str],
bot_handler: BotHandler,
last_fen: str
) -> None:
def resign(self, message: Dict[str, str], bot_handler: BotHandler, last_fen: str) -> None:
"""Resigns the game for the current player.
Parameters:
@ -465,13 +347,12 @@ class ChessHandler:
if not last_board:
return
bot_handler.send_reply(
message,
make_loss_response(last_board, 'resigned')
)
bot_handler.send_reply(message, make_loss_response(last_board, 'resigned'))
handler_class = ChessHandler
def calculate_computer_move(board: chess.Board, engine: Any) -> chess.Move:
"""Calculates the computer's move.
@ -485,6 +366,7 @@ def calculate_computer_move(board: chess.Board, engine: Any) -> chess.Move:
best_move_and_ponder_move = engine.go(movetime=(3000))
return best_move_and_ponder_move[0]
def make_draw_response(reason: str) -> str:
"""Makes a response string for a draw.
@ -496,6 +378,7 @@ def make_draw_response(reason: str) -> str:
"""
return 'It\'s a draw because of {}!'.format(reason)
def make_loss_response(board: chess.Board, reason: str) -> str:
"""Makes a response string for a loss (or win).
@ -506,16 +389,14 @@ def make_loss_response(board: chess.Board, reason: str) -> str:
Returns: The loss response string.
"""
return (
'*{}* {}. **{}** wins!\n\n'
'{}'
).format(
return ('*{}* {}. **{}** wins!\n\n' '{}').format(
'White' if board.turn else 'Black',
reason,
'Black' if board.turn else 'White',
make_str(board, board.turn)
make_str(board, board.turn),
)
def make_not_legal_response(board: chess.Board, move_san: str) -> str:
"""Makes a response string for a not-legal move.
@ -525,17 +406,11 @@ def make_not_legal_response(board: chess.Board, move_san: str) -> str:
Returns: The not-legal-move response string.
"""
return (
'Sorry, the move *{}* isn\'t legal.\n\n'
'{}'
'\n\n\n'
'{}'
).format(
move_san,
make_str(board, board.turn),
make_footer()
return ('Sorry, the move *{}* isn\'t legal.\n\n' '{}' '\n\n\n' '{}').format(
move_san, make_str(board, board.turn), make_footer()
)
def make_copied_wrong_response() -> str:
"""Makes a response string for a FEN string that was copied wrong.
@ -546,6 +421,7 @@ def make_copied_wrong_response() -> str:
'Please try to copy the response again from the last message!'
)
def make_start_reponse(board: chess.Board) -> str:
"""Makes a response string for the first response of a game with another
user.
@ -563,11 +439,8 @@ def make_start_reponse(board: chess.Board) -> str:
'Now it\'s **{}**\'s turn.'
'\n\n\n'
'{}'
).format(
make_str(board, True),
'white' if board.turn else 'black',
make_footer()
)
).format(make_str(board, True), 'white' if board.turn else 'black', make_footer())
def make_start_computer_reponse(board: chess.Board) -> str:
"""Makes a response string for the first response of a game with a
@ -587,17 +460,10 @@ def make_start_computer_reponse(board: chess.Board) -> str:
'Now it\'s **{}**\'s turn.'
'\n\n\n'
'{}'
).format(
make_str(board, True),
'white' if board.turn else 'black',
make_footer()
)
).format(make_str(board, True), 'white' if board.turn else 'black', make_footer())
def make_move_reponse(
last_board: chess.Board,
new_board: chess.Board,
move: chess.Move
) -> str:
def make_move_reponse(last_board: chess.Board, new_board: chess.Board, move: chess.Move) -> str:
"""Makes a response string for after a move is made.
Parameters:
@ -623,9 +489,10 @@ def make_move_reponse(
last_board.san(move),
make_str(new_board, new_board.turn),
'white' if new_board.turn else 'black',
make_footer()
make_footer(),
)
def make_footer() -> str:
"""Makes a footer to be appended to the bottom of other, actionable
responses.
@ -637,6 +504,7 @@ def make_footer() -> str:
'response.*'
)
def make_str(board: chess.Board, is_white_on_bottom: bool) -> str:
"""Converts a board object into a string to be used in Markdown. Backticks
are added around the string to preserve formatting.
@ -654,14 +522,14 @@ def make_str(board: chess.Board, is_white_on_bottom: bool) -> str:
replaced_str = replace_with_unicode(default_str)
replaced_and_guided_str = guide_with_numbers(replaced_str)
properly_flipped_str = (
replaced_and_guided_str if is_white_on_bottom
else replaced_and_guided_str[::-1]
replaced_and_guided_str if is_white_on_bottom else replaced_and_guided_str[::-1]
)
trimmed_str = trim_whitespace_before_newline(properly_flipped_str)
monospaced_str = '```\n{}\n```'.format(trimmed_str)
return monospaced_str
def guide_with_numbers(board_str: str) -> str:
"""Adds numbers and letters on the side of a string without them made out
of a board.
@ -702,12 +570,11 @@ def guide_with_numbers(board_str: str) -> str:
row_str = (' '.join(row_list) + ' 1').replace('\n ', '\n')
# a, b, c, d, e, f, g, and h are easy to add in.
row_and_col_str = (
' a b c d e f g h \n' + row_str + '\n a b c d e f g h '
)
row_and_col_str = ' a b c d e f g h \n' + row_str + '\n a b c d e f g h '
return row_and_col_str
def replace_with_unicode(board_str: str) -> str:
"""Replaces the default characters in a board object's string output with
Unicode chess characters, e.g., '' instead of 'R.'
@ -737,6 +604,7 @@ def replace_with_unicode(board_str: str) -> str:
return replaced_str
def trim_whitespace_before_newline(str_to_trim: str) -> str:
"""Removes any spaces before a newline in a string.

View file

@ -114,9 +114,11 @@ To make your next move, respond to Chess Bot with
def test_main(self) -> None:
with self.mock_config_info({'stockfish_location': '/foo/bar'}):
self.verify_dialog([
('start with other user', self.START_RESPONSE),
('do e4', self.DO_E4_RESPONSE),
('do Ke4', self.DO_KE4_RESPONSE),
('resign', self.RESIGN_RESPONSE),
])
self.verify_dialog(
[
('start with other user', self.START_RESPONSE),
('do e4', self.DO_E4_RESPONSE),
('do Ke4', self.DO_KE4_RESPONSE),
('resign', self.RESIGN_RESPONSE),
]
)

View file

@ -46,8 +46,10 @@ class ConnectFourBotHandler(GameAdapter):
def __init__(self) -> None:
game_name = 'Connect Four'
bot_name = 'connect_four'
move_help_message = '* To make your move during a game, type\n' \
'```move <column-number>``` or ```<column-number>```'
move_help_message = (
'* To make your move during a game, type\n'
'```move <column-number>``` or ```<column-number>```'
)
move_regex = '(move ([1-7])$)|(([1-7])$)'
model = ConnectFourModel
gameMessageHandler = ConnectFourMessageHandler
@ -61,7 +63,7 @@ class ConnectFourBotHandler(GameAdapter):
model,
gameMessageHandler,
rules,
max_players=2
max_players=2,
)

View file

@ -17,7 +17,7 @@ class ConnectFourModel:
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0],
]
self.current_board = self.blank_board
@ -27,10 +27,7 @@ class ConnectFourModel:
def get_column(self, col):
# We use this in tests.
return [
self.current_board[i][col]
for i in range(6)
]
return [self.current_board[i][col] for i in range(6)]
def validate_move(self, column_number):
if column_number < 0 or column_number > 6:
@ -76,8 +73,12 @@ class ConnectFourModel:
for row in range(0, 6):
for column in range(0, 4):
horizontal_sum = board[row][column] + board[row][column + 1] + \
board[row][column + 2] + board[row][column + 3]
horizontal_sum = (
board[row][column]
+ board[row][column + 1]
+ board[row][column + 2]
+ board[row][column + 3]
)
if horizontal_sum == -4:
return -1
elif horizontal_sum == 4:
@ -90,8 +91,12 @@ class ConnectFourModel:
for row in range(0, 3):
for column in range(0, 7):
vertical_sum = board[row][column] + board[row + 1][column] + \
board[row + 2][column] + board[row + 3][column]
vertical_sum = (
board[row][column]
+ board[row + 1][column]
+ board[row + 2][column]
+ board[row + 3][column]
)
if vertical_sum == -4:
return -1
elif vertical_sum == 4:
@ -106,8 +111,12 @@ class ConnectFourModel:
# Major Diagonl Sum
for row in range(0, 3):
for column in range(0, 4):
major_diagonal_sum = board[row][column] + board[row + 1][column + 1] + \
board[row + 2][column + 2] + board[row + 3][column + 3]
major_diagonal_sum = (
board[row][column]
+ board[row + 1][column + 1]
+ board[row + 2][column + 2]
+ board[row + 3][column + 3]
)
if major_diagonal_sum == -4:
return -1
elif major_diagonal_sum == 4:
@ -116,8 +125,12 @@ class ConnectFourModel:
# Minor Diagonal Sum
for row in range(3, 6):
for column in range(0, 4):
minor_diagonal_sum = board[row][column] + board[row - 1][column + 1] + \
board[row - 2][column + 2] + board[row - 3][column + 3]
minor_diagonal_sum = (
board[row][column]
+ board[row - 1][column + 1]
+ board[row - 2][column + 2]
+ board[row - 3][column + 3]
)
if minor_diagonal_sum == -4:
return -1
elif minor_diagonal_sum == 4:
@ -132,9 +145,11 @@ class ConnectFourModel:
if top_row_multiple != 0:
return 'draw'
winner = get_horizontal_wins(self.current_board) + \
get_vertical_wins(self.current_board) + \
get_diagonal_wins(self.current_board)
winner = (
get_horizontal_wins(self.current_board)
+ get_vertical_wins(self.current_board)
+ get_diagonal_wins(self.current_board)
)
if winner == 1:
return first_player

View file

@ -9,20 +9,19 @@ class TestConnectFourBot(BotTestCase, DefaultTests):
bot_name = 'connect_four'
def make_request_message(
self,
content: str,
user: str = 'foo@example.com',
user_name: str = 'foo'
self, content: str, user: str = 'foo@example.com', user_name: str = 'foo'
) -> Dict[str, str]:
message = dict(
sender_email=user,
content=content,
sender_full_name=user_name
)
message = dict(sender_email=user, content=content, sender_full_name=user_name)
return message
# Function that serves similar purpose to BotTestCase.verify_dialog, but allows for multiple responses to be handled
def verify_response(self, request: str, expected_response: str, response_number: int, user: str = 'foo@example.com') -> None:
def verify_response(
self,
request: str,
expected_response: str,
response_number: int,
user: str = 'foo@example.com',
) -> None:
'''
This function serves a similar purpose
to BotTestCase.verify_dialog, but allows
@ -36,11 +35,7 @@ class TestConnectFourBot(BotTestCase, DefaultTests):
bot.handle_message(message, bot_handler)
responses = [
message
for (method, message)
in bot_handler.transcript
]
responses = [message for (method, message) in bot_handler.transcript]
first_response = responses[response_number]
self.assertEqual(expected_response, first_response['content'])
@ -73,7 +68,9 @@ class TestConnectFourBot(BotTestCase, DefaultTests):
self.verify_response('help', self.help_message(), 0)
def test_game_message_handler_responses(self) -> None:
board = ':one: :two: :three: :four: :five: :six: :seven:\n\n' + '\
board = (
':one: :two: :three: :four: :five: :six: :seven:\n\n'
+ '\
:white_circle: :white_circle: :white_circle: :white_circle: \
:white_circle: :white_circle: :white_circle: \n\n\
:white_circle: :white_circle: :white_circle: :white_circle: \
@ -86,16 +83,18 @@ class TestConnectFourBot(BotTestCase, DefaultTests):
:white_circle: :white_circle: \n\n\
:blue_circle: :red_circle: :white_circle: :white_circle: :white_circle: \
:white_circle: :white_circle: '
)
bot, bot_handler = self._get_handlers()
self.assertEqual(bot.gameMessageHandler.parse_board(
self.almost_win_board), board)
self.assertEqual(bot.gameMessageHandler.parse_board(self.almost_win_board), board)
self.assertEqual(bot.gameMessageHandler.get_player_color(1), ':red_circle:')
self.assertEqual(
bot.gameMessageHandler.get_player_color(1), ':red_circle:')
self.assertEqual(bot.gameMessageHandler.alert_move_message(
'foo', 'move 6'), 'foo moved in column 6')
self.assertEqual(bot.gameMessageHandler.game_start_message(
), 'Type `move <column-number>` or `<column-number>` to place a token.\n\
The first player to get 4 in a row wins!\n Good Luck!')
bot.gameMessageHandler.alert_move_message('foo', 'move 6'), 'foo moved in column 6'
)
self.assertEqual(
bot.gameMessageHandler.game_start_message(),
'Type `move <column-number>` or `<column-number>` to place a token.\n\
The first player to get 4 in a row wins!\n Good Luck!',
)
blank_board = [
[0, 0, 0, 0, 0, 0, 0],
@ -103,7 +102,8 @@ The first player to get 4 in a row wins!\n Good Luck!')
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]]
[0, 0, 0, 0, 0, 0, 0],
]
almost_win_board = [
[0, 0, 0, 0, 0, 0, 0],
@ -111,7 +111,8 @@ The first player to get 4 in a row wins!\n Good Luck!')
[0, 0, 0, 0, 0, 0, 0],
[1, -1, 0, 0, 0, 0, 0],
[1, -1, 0, 0, 0, 0, 0],
[1, -1, 0, 0, 0, 0, 0]]
[1, -1, 0, 0, 0, 0, 0],
]
almost_draw_board = [
[1, -1, 1, -1, 1, -1, 0],
@ -119,13 +120,12 @@ The first player to get 4 in a row wins!\n Good Luck!')
[0, 0, 0, 0, 0, 0, -1],
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, -1],
[0, 0, 0, 0, 0, 0, 1]]
[0, 0, 0, 0, 0, 0, 1],
]
def test_connect_four_logic(self) -> None:
def confirmAvailableMoves(
good_moves: List[int],
bad_moves: List[int],
board: List[List[int]]
good_moves: List[int], bad_moves: List[int], board: List[List[int]]
) -> None:
connectFourModel.update_board(board)
@ -139,18 +139,16 @@ The first player to get 4 in a row wins!\n Good Luck!')
column_number: int,
token_number: int,
initial_board: List[List[int]],
final_board: List[List[int]]
final_board: List[List[int]],
) -> None:
connectFourModel.update_board(initial_board)
test_board = connectFourModel.make_move(
'move ' + str(column_number), token_number)
test_board = connectFourModel.make_move('move ' + str(column_number), token_number)
self.assertEqual(test_board, final_board)
def confirmGameOver(board: List[List[int]], result: str) -> None:
connectFourModel.update_board(board)
game_over = connectFourModel.determine_game_over(
['first_player', 'second_player'])
game_over = connectFourModel.determine_game_over(['first_player', 'second_player'])
self.assertEqual(game_over, result)
@ -170,7 +168,8 @@ The first player to get 4 in a row wins!\n Good Luck!')
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]]
[0, 0, 0, 0, 0, 0, 0],
]
full_board = [
[1, 1, 1, 1, 1, 1, 1],
@ -178,7 +177,8 @@ The first player to get 4 in a row wins!\n Good Luck!')
[1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1]]
[1, 1, 1, 1, 1, 1, 1],
]
single_column_board = [
[1, 1, 1, 0, 1, 1, 1],
@ -186,7 +186,8 @@ The first player to get 4 in a row wins!\n Good Luck!')
[1, 1, 1, 0, 1, 1, 1],
[1, 1, 1, 0, 1, 1, 1],
[1, 1, 1, 0, 1, 1, 1],
[1, 1, 1, 0, 1, 1, 1]]
[1, 1, 1, 0, 1, 1, 1],
]
diagonal_board = [
[0, 0, 0, 0, 0, 0, 1],
@ -194,7 +195,8 @@ The first player to get 4 in a row wins!\n Good Luck!')
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1]]
[0, 1, 1, 1, 1, 1, 1],
]
# Winning Board Setups
# Each array if consists of two arrays:
@ -204,190 +206,222 @@ The first player to get 4 in a row wins!\n Good Luck!')
# for simplicity (random -1 and 1s could be added)
horizontal_win_boards = [
[
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 0, 0, 0]],
[[0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]]
[
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[1, 1, 1, 1, 0, 0, 0],
],
[
[0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
],
[
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 1, 1, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
],
],
[
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[-1, -1, -1, -1, 0, 0, 0]],
[[0, 0, 0, -1, -1, -1, -1],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, -1, -1, -1, -1, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]]
]
[
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[-1, -1, -1, -1, 0, 0, 0],
],
[
[0, 0, 0, -1, -1, -1, -1],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
],
[
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, -1, -1, -1, -1, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
],
],
]
vertical_win_boards = [
[
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]]
[
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0],
],
[
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
],
[
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
],
],
[
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, -1],
[0, 0, 0, 0, 0, 0, -1],
[0, 0, 0, 0, 0, 0, -1],
[0, 0, 0, 0, 0, 0, -1],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]]
]
[
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0],
],
[
[0, 0, 0, 0, 0, 0, -1],
[0, 0, 0, 0, 0, 0, -1],
[0, 0, 0, 0, 0, 0, -1],
[0, 0, 0, 0, 0, 0, -1],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
],
[
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
],
],
]
major_diagonal_win_boards = [
[
[[1, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 1]],
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0]]
[
[1, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
],
[
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 1],
],
[
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0],
],
],
[
[[-1, 0, 0, 0, 0, 0, 0],
[0, -1, 0, 0, 0, 0, 0],
[0, 0, -1, 0, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, 0, -1, 0, 0],
[0, 0, 0, 0, 0, -1, 0],
[0, 0, 0, 0, 0, 0, -1]],
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, -1, 0, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, 0, -1, 0, 0],
[0, 0, 0, 0, 0, -1, 0],
[0, 0, 0, 0, 0, 0, 0]]
]
[
[-1, 0, 0, 0, 0, 0, 0],
[0, -1, 0, 0, 0, 0, 0],
[0, 0, -1, 0, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
],
[
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, 0, -1, 0, 0],
[0, 0, 0, 0, 0, -1, 0],
[0, 0, 0, 0, 0, 0, -1],
],
[
[0, 0, 0, 0, 0, 0, 0],
[0, 0, -1, 0, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, 0, -1, 0, 0],
[0, 0, 0, 0, 0, -1, 0],
[0, 0, 0, 0, 0, 0, 0],
],
],
]
minor_diagonal_win_boards = [
[
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]]
[
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0],
],
[
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
],
[
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
],
],
[
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, -1, 0, 0, 0, 0],
[0, -1, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, -1],
[0, 0, 0, 0, 0, -1, 0],
[0, 0, 0, 0, -1, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]],
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, -1, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, -1, 0, 0, 0, 0],
[0, -1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0]]
]
[
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, -1, 0, 0, 0, 0],
[0, -1, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0],
],
[
[0, 0, 0, 0, 0, 0, -1],
[0, 0, 0, 0, 0, -1, 0],
[0, 0, 0, 0, -1, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
],
[
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, -1, 0, 0],
[0, 0, 0, -1, 0, 0, 0],
[0, 0, -1, 0, 0, 0, 0],
[0, -1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
],
],
]
# Test Move Validation Logic
@ -397,8 +431,7 @@ The first player to get 4 in a row wins!\n Good Luck!')
# Test Available Move Logic
connectFourModel.update_board(blank_board)
self.assertEqual(connectFourModel.available_moves(),
[0, 1, 2, 3, 4, 5, 6])
self.assertEqual(connectFourModel.available_moves(), [0, 1, 2, 3, 4, 5, 6])
connectFourModel.update_board(single_column_board)
self.assertEqual(connectFourModel.available_moves(), [3])
@ -407,69 +440,117 @@ The first player to get 4 in a row wins!\n Good Luck!')
self.assertEqual(connectFourModel.available_moves(), [])
# Test Move Logic
confirmMove(1, 0, blank_board,
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0]])
confirmMove(
1,
0,
blank_board,
[
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0],
],
)
confirmMove(1, 1, blank_board,
[[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0]])
confirmMove(
1,
1,
blank_board,
[
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0],
[-1, 0, 0, 0, 0, 0, 0],
],
)
confirmMove(1, 0, diagonal_board,
[[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1]])
confirmMove(
1,
0,
diagonal_board,
[
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1],
],
)
confirmMove(2, 0, diagonal_board,
[[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1]])
confirmMove(
2,
0,
diagonal_board,
[
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1],
],
)
confirmMove(3, 0, diagonal_board,
[[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1]])
confirmMove(
3,
0,
diagonal_board,
[
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1],
],
)
confirmMove(4, 0, diagonal_board,
[[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1]])
confirmMove(
4,
0,
diagonal_board,
[
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1],
],
)
confirmMove(5, 0, diagonal_board,
[[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1]])
confirmMove(
5,
0,
diagonal_board,
[
[0, 0, 0, 0, 0, 0, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1],
],
)
confirmMove(6, 0, diagonal_board,
[[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1]])
confirmMove(
6,
0,
diagonal_board,
[
[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 0, 1, 1],
[0, 0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1, 1],
[0, 0, 1, 1, 1, 1, 1],
[0, 1, 1, 1, 1, 1, 1],
],
)
# Test Game Over Logic:
confirmGameOver(blank_board, '')

View file

@ -15,14 +15,17 @@ def is_float(value: Any) -> bool:
except ValueError:
return False
# Rounds the number 'x' to 'digits' significant digits.
# A normal 'round()' would round the number to an absolute amount of
# fractional decimals, e.g. 0.00045 would become 0.0.
# 'round_to()' rounds only the digits that are not 0.
# 0.00045 would then become 0.0005.
def round_to(x: float, digits: int) -> float:
return round(x, digits-int(floor(log10(abs(x)))))
return round(x, digits - int(floor(log10(abs(x)))))
class ConverterHandler:
'''
@ -49,6 +52,7 @@ class ConverterHandler:
bot_response = get_bot_converter_response(message, bot_handler)
bot_handler.send_reply(message, bot_response)
def get_bot_converter_response(message: Dict[str, str], bot_handler: BotHandler) -> str:
content = message['content']
@ -78,10 +82,10 @@ def get_bot_converter_response(message: Dict[str, str], bot_handler: BotHandler)
for key, exp in utils.PREFIXES.items():
if unit_from.startswith(key):
exponent += exp
unit_from = unit_from[len(key):]
unit_from = unit_from[len(key) :]
if unit_to.startswith(key):
exponent -= exp
unit_to = unit_to[len(key):]
unit_to = unit_to[len(key) :]
uf_to_std = utils.UNITS.get(unit_from, []) # type: List[Any]
ut_to_std = utils.UNITS.get(unit_to, []) # type: List[Any]
@ -97,8 +101,13 @@ def get_bot_converter_response(message: Dict[str, str], bot_handler: BotHandler)
if uf_to_std[2] != ut_to_std[2]:
unit_from = unit_from.capitalize() if uf_to_std[2] == 'kelvin' else unit_from
results.append(
'`' + unit_to.capitalize() + '` and `' + unit_from + '`'
+ ' are not from the same category. ' + utils.QUICK_HELP
'`'
+ unit_to.capitalize()
+ '` and `'
+ unit_from
+ '`'
+ ' are not from the same category. '
+ utils.QUICK_HELP
)
continue
@ -114,10 +123,11 @@ def get_bot_converter_response(message: Dict[str, str], bot_handler: BotHandler)
number_res *= 10 ** exponent
number_res = round_to(number_res, 7)
results.append('{} {} = {} {}'.format(number,
words[convert_index + 2],
number_res,
words[convert_index + 3]))
results.append(
'{} {} = {} {}'.format(
number, words[convert_index + 2], number_res, words[convert_index + 3]
)
)
else:
results.append('Too few arguments given. ' + utils.QUICK_HELP)
@ -128,4 +138,5 @@ def get_bot_converter_response(message: Dict[str, str], bot_handler: BotHandler)
return new_content
handler_class = ConverterHandler

View file

@ -7,25 +7,41 @@ class TestConverterBot(BotTestCase, DefaultTests):
def test_bot(self) -> None:
dialog = [
("", 'Too few arguments given. Enter `@convert help` '
'for help on using the converter.\n'),
("foo bar", 'Too few arguments given. Enter `@convert help` '
'for help on using the converter.\n'),
(
"",
'Too few arguments given. Enter `@convert help` '
'for help on using the converter.\n',
),
(
"foo bar",
'Too few arguments given. Enter `@convert help` '
'for help on using the converter.\n',
),
("2 m cm", "2 m = 200.0 cm\n"),
("12.0 celsius fahrenheit", "12.0 celsius = 53.600054 fahrenheit\n"),
("0.002 kilometer millimile", "0.002 kilometer = 1.2427424 millimile\n"),
("3 megabyte kilobit", "3 megabyte = 24576.0 kilobit\n"),
("foo m cm", "`foo` is not a valid number. " + utils.QUICK_HELP + "\n"),
("@convert help", "1. conversion: Too few arguments given. "
"Enter `@convert help` for help on using the converter.\n"
"2. conversion: " + utils.HELP_MESSAGE + "\n"),
("2 celsius kilometer", "`Meter` and `Celsius` are not from the same category. "
"Enter `@convert help` for help on using the converter.\n"),
("2 foo kilometer", "`foo` is not a valid unit."
" Enter `@convert help` for help on using the converter.\n"),
("2 kilometer foo", "`foo` is not a valid unit."
"Enter `@convert help` for help on using the converter.\n"),
(
"@convert help",
"1. conversion: Too few arguments given. "
"Enter `@convert help` for help on using the converter.\n"
"2. conversion: " + utils.HELP_MESSAGE + "\n",
),
(
"2 celsius kilometer",
"`Meter` and `Celsius` are not from the same category. "
"Enter `@convert help` for help on using the converter.\n",
),
(
"2 foo kilometer",
"`foo` is not a valid unit."
" Enter `@convert help` for help on using the converter.\n",
),
(
"2 kilometer foo",
"`foo` is not a valid unit."
"Enter `@convert help` for help on using the converter.\n",
),
]
self.verify_dialog(dialog)

View file

@ -2,145 +2,153 @@
# An entry consists of the unit's name, a constant number and a constant
# factor that need to be added and multiplied to convert the unit into
# the base unit in the last parameter.
UNITS = {'bit': [0, 1, 'bit'],
'byte': [0, 8, 'bit'],
'cubic-centimeter': [0, 0.000001, 'cubic-meter'],
'cubic-decimeter': [0, 0.001, 'cubic-meter'],
'liter': [0, 0.001, 'cubic-meter'],
'cubic-meter': [0, 1, 'cubic-meter'],
'cubic-inch': [0, 0.000016387064, 'cubic-meter'],
'fluid-ounce': [0, 0.000029574, 'cubic-meter'],
'cubic-foot': [0, 0.028316846592, 'cubic-meter'],
'cubic-yard': [0, 0.764554857984, 'cubic-meter'],
'teaspoon': [0, 0.0000049289216, 'cubic-meter'],
'tablespoon': [0, 0.000014787, 'cubic-meter'],
'cup': [0, 0.00023658823648491, 'cubic-meter'],
'gram': [0, 1, 'gram'],
'kilogram': [0, 1000, 'gram'],
'ton': [0, 1000000, 'gram'],
'ounce': [0, 28.349523125, 'gram'],
'pound': [0, 453.59237, 'gram'],
'kelvin': [0, 1, 'kelvin'],
'celsius': [273.15, 1, 'kelvin'],
'fahrenheit': [255.372222, 0.555555, 'kelvin'],
'centimeter': [0, 0.01, 'meter'],
'decimeter': [0, 0.1, 'meter'],
'meter': [0, 1, 'meter'],
'kilometer': [0, 1000, 'meter'],
'inch': [0, 0.0254, 'meter'],
'foot': [0, 0.3048, 'meter'],
'yard': [0, 0.9144, 'meter'],
'mile': [0, 1609.344, 'meter'],
'nautical-mile': [0, 1852, 'meter'],
'square-centimeter': [0, 0.0001, 'square-meter'],
'square-decimeter': [0, 0.01, 'square-meter'],
'square-meter': [0, 1, 'square-meter'],
'square-kilometer': [0, 1000000, 'square-meter'],
'square-inch': [0, 0.00064516, 'square-meter'],
'square-foot': [0, 0.09290304, 'square-meter'],
'square-yard': [0, 0.83612736, 'square-meter'],
'square-mile': [0, 2589988.110336, 'square-meter'],
'are': [0, 100, 'square-meter'],
'hectare': [0, 10000, 'square-meter'],
'acre': [0, 4046.8564224, 'square-meter']}
UNITS = {
'bit': [0, 1, 'bit'],
'byte': [0, 8, 'bit'],
'cubic-centimeter': [0, 0.000001, 'cubic-meter'],
'cubic-decimeter': [0, 0.001, 'cubic-meter'],
'liter': [0, 0.001, 'cubic-meter'],
'cubic-meter': [0, 1, 'cubic-meter'],
'cubic-inch': [0, 0.000016387064, 'cubic-meter'],
'fluid-ounce': [0, 0.000029574, 'cubic-meter'],
'cubic-foot': [0, 0.028316846592, 'cubic-meter'],
'cubic-yard': [0, 0.764554857984, 'cubic-meter'],
'teaspoon': [0, 0.0000049289216, 'cubic-meter'],
'tablespoon': [0, 0.000014787, 'cubic-meter'],
'cup': [0, 0.00023658823648491, 'cubic-meter'],
'gram': [0, 1, 'gram'],
'kilogram': [0, 1000, 'gram'],
'ton': [0, 1000000, 'gram'],
'ounce': [0, 28.349523125, 'gram'],
'pound': [0, 453.59237, 'gram'],
'kelvin': [0, 1, 'kelvin'],
'celsius': [273.15, 1, 'kelvin'],
'fahrenheit': [255.372222, 0.555555, 'kelvin'],
'centimeter': [0, 0.01, 'meter'],
'decimeter': [0, 0.1, 'meter'],
'meter': [0, 1, 'meter'],
'kilometer': [0, 1000, 'meter'],
'inch': [0, 0.0254, 'meter'],
'foot': [0, 0.3048, 'meter'],
'yard': [0, 0.9144, 'meter'],
'mile': [0, 1609.344, 'meter'],
'nautical-mile': [0, 1852, 'meter'],
'square-centimeter': [0, 0.0001, 'square-meter'],
'square-decimeter': [0, 0.01, 'square-meter'],
'square-meter': [0, 1, 'square-meter'],
'square-kilometer': [0, 1000000, 'square-meter'],
'square-inch': [0, 0.00064516, 'square-meter'],
'square-foot': [0, 0.09290304, 'square-meter'],
'square-yard': [0, 0.83612736, 'square-meter'],
'square-mile': [0, 2589988.110336, 'square-meter'],
'are': [0, 100, 'square-meter'],
'hectare': [0, 10000, 'square-meter'],
'acre': [0, 4046.8564224, 'square-meter'],
}
PREFIXES = {'atto': -18,
'femto': -15,
'pico': -12,
'nano': -9,
'micro': -6,
'milli': -3,
'centi': -2,
'deci': -1,
'deca': 1,
'hecto': 2,
'kilo': 3,
'mega': 6,
'giga': 9,
'tera': 12,
'peta': 15,
'exa': 18}
PREFIXES = {
'atto': -18,
'femto': -15,
'pico': -12,
'nano': -9,
'micro': -6,
'milli': -3,
'centi': -2,
'deci': -1,
'deca': 1,
'hecto': 2,
'kilo': 3,
'mega': 6,
'giga': 9,
'tera': 12,
'peta': 15,
'exa': 18,
}
ALIASES = {'a': 'are',
'ac': 'acre',
'c': 'celsius',
'cm': 'centimeter',
'cm2': 'square-centimeter',
'cm3': 'cubic-centimeter',
'cm^2': 'square-centimeter',
'cm^3': 'cubic-centimeter',
'dm': 'decimeter',
'dm2': 'square-decimeter',
'dm3': 'cubic-decimeter',
'dm^2': 'square-decimeter',
'dm^3': 'cubic-decimeter',
'f': 'fahrenheit',
'fl-oz': 'fluid-ounce',
'ft': 'foot',
'ft2': 'square-foot',
'ft3': 'cubic-foot',
'ft^2': 'square-foot',
'ft^3': 'cubic-foot',
'g': 'gram',
'ha': 'hectare',
'in': 'inch',
'in2': 'square-inch',
'in3': 'cubic-inch',
'in^2': 'square-inch',
'in^3': 'cubic-inch',
'k': 'kelvin',
'kg': 'kilogram',
'km': 'kilometer',
'km2': 'square-kilometer',
'km^2': 'square-kilometer',
'l': 'liter',
'lb': 'pound',
'm': 'meter',
'm2': 'square-meter',
'm3': 'cubic-meter',
'm^2': 'square-meter',
'm^3': 'cubic-meter',
'mi': 'mile',
'mi2': 'square-mile',
'mi^2': 'square-mile',
'nmi': 'nautical-mile',
'oz': 'ounce',
't': 'ton',
'tbsp': 'tablespoon',
'tsp': 'teaspoon',
'y': 'yard',
'y2': 'square-yard',
'y3': 'cubic-yard',
'y^2': 'square-yard',
'y^3': 'cubic-yard'}
ALIASES = {
'a': 'are',
'ac': 'acre',
'c': 'celsius',
'cm': 'centimeter',
'cm2': 'square-centimeter',
'cm3': 'cubic-centimeter',
'cm^2': 'square-centimeter',
'cm^3': 'cubic-centimeter',
'dm': 'decimeter',
'dm2': 'square-decimeter',
'dm3': 'cubic-decimeter',
'dm^2': 'square-decimeter',
'dm^3': 'cubic-decimeter',
'f': 'fahrenheit',
'fl-oz': 'fluid-ounce',
'ft': 'foot',
'ft2': 'square-foot',
'ft3': 'cubic-foot',
'ft^2': 'square-foot',
'ft^3': 'cubic-foot',
'g': 'gram',
'ha': 'hectare',
'in': 'inch',
'in2': 'square-inch',
'in3': 'cubic-inch',
'in^2': 'square-inch',
'in^3': 'cubic-inch',
'k': 'kelvin',
'kg': 'kilogram',
'km': 'kilometer',
'km2': 'square-kilometer',
'km^2': 'square-kilometer',
'l': 'liter',
'lb': 'pound',
'm': 'meter',
'm2': 'square-meter',
'm3': 'cubic-meter',
'm^2': 'square-meter',
'm^3': 'cubic-meter',
'mi': 'mile',
'mi2': 'square-mile',
'mi^2': 'square-mile',
'nmi': 'nautical-mile',
'oz': 'ounce',
't': 'ton',
'tbsp': 'tablespoon',
'tsp': 'teaspoon',
'y': 'yard',
'y2': 'square-yard',
'y3': 'cubic-yard',
'y^2': 'square-yard',
'y^3': 'cubic-yard',
}
HELP_MESSAGE = ('Converter usage:\n'
'`@convert <number> <unit_from> <unit_to>`\n'
'Converts `number` in the unit <unit_from> to '
'the <unit_to> and prints the result\n'
'`number`: integer or floating point number, e.g. 12, 13.05, 0.002\n'
'<unit_from> and <unit_to> are two of the following units:\n'
'* square-centimeter (cm^2, cm2), square-decimeter (dm^2, dm2), '
'square-meter (m^2, m2), square-kilometer (km^2, km2),'
' square-inch (in^2, in2), square-foot (ft^2, ft2), square-yard (y^2, y2), '
' square-mile(mi^2, mi2), are (a), hectare (ha), acre (ac)\n'
'* bit, byte\n'
'* centimeter (cm), decimeter(dm), meter (m),'
' kilometer (km), inch (in), foot (ft), yard (y),'
' mile (mi), nautical-mile (nmi)\n'
'* Kelvin (K), Celsius(C), Fahrenheit (F)\n'
'* cubic-centimeter (cm^3, cm3), cubic-decimeter (dm^3, dm3), liter (l), '
'cubic-meter (m^3, m3), cubic-inch (in^3, in3), fluid-ounce (fl-oz), '
'cubic-foot (ft^3, ft3), cubic-yard (y^3, y3)\n'
'* gram (g), kilogram (kg), ton (t), ounce (oz), pound(lb)\n'
'* (metric only, U.S. and imperial units differ slightly:) teaspoon (tsp), tablespoon (tbsp), cup\n\n\n'
'Allowed prefixes are:\n'
'* atto, pico, femto, nano, micro, milli, centi, deci\n'
'* deca, hecto, kilo, mega, giga, tera, peta, exa\n\n\n'
'Usage examples:\n'
'* `@convert 12 celsius fahrenheit`\n'
'* `@convert 0.002 kilomile millimeter`\n'
'* `@convert 31.5 square-mile ha`\n'
'* `@convert 56 g lb`\n')
HELP_MESSAGE = (
'Converter usage:\n'
'`@convert <number> <unit_from> <unit_to>`\n'
'Converts `number` in the unit <unit_from> to '
'the <unit_to> and prints the result\n'
'`number`: integer or floating point number, e.g. 12, 13.05, 0.002\n'
'<unit_from> and <unit_to> are two of the following units:\n'
'* square-centimeter (cm^2, cm2), square-decimeter (dm^2, dm2), '
'square-meter (m^2, m2), square-kilometer (km^2, km2),'
' square-inch (in^2, in2), square-foot (ft^2, ft2), square-yard (y^2, y2), '
' square-mile(mi^2, mi2), are (a), hectare (ha), acre (ac)\n'
'* bit, byte\n'
'* centimeter (cm), decimeter(dm), meter (m),'
' kilometer (km), inch (in), foot (ft), yard (y),'
' mile (mi), nautical-mile (nmi)\n'
'* Kelvin (K), Celsius(C), Fahrenheit (F)\n'
'* cubic-centimeter (cm^3, cm3), cubic-decimeter (dm^3, dm3), liter (l), '
'cubic-meter (m^3, m3), cubic-inch (in^3, in3), fluid-ounce (fl-oz), '
'cubic-foot (ft^3, ft3), cubic-yard (y^3, y3)\n'
'* gram (g), kilogram (kg), ton (t), ounce (oz), pound(lb)\n'
'* (metric only, U.S. and imperial units differ slightly:) teaspoon (tsp), tablespoon (tbsp), cup\n\n\n'
'Allowed prefixes are:\n'
'* atto, pico, femto, nano, micro, milli, centi, deci\n'
'* deca, hecto, kilo, mega, giga, tera, peta, exa\n\n\n'
'Usage examples:\n'
'* `@convert 12 celsius fahrenheit`\n'
'* `@convert 0.002 kilomile millimeter`\n'
'* `@convert 31.5 square-mile ha`\n'
'* `@convert 56 g lb`\n'
)
QUICK_HELP = 'Enter `@convert help` for help on using the converter.'

View file

@ -66,7 +66,9 @@ class DefineHandler:
# Show definitions line by line.
for d in definitions:
example = d['example'] if d['example'] else '*No example available.*'
response += '\n' + '* (**{}**) {}\n&nbsp;&nbsp;{}'.format(d['type'], d['definition'], html2text.html2text(example))
response += '\n' + '* (**{}**) {}\n&nbsp;&nbsp;{}'.format(
d['type'], d['definition'], html2text.html2text(example)
)
except Exception:
response += self.REQUEST_ERROR_MESSAGE
@ -74,4 +76,5 @@ class DefineHandler:
return response
handler_class = DefineHandler

View file

@ -9,25 +9,29 @@ class TestDefineBot(BotTestCase, DefaultTests):
def test_bot(self) -> None:
# Only one type(noun) of word.
bot_response = ("**cat**:\n\n* (**noun**) a small domesticated carnivorous mammal "
"with soft fur, a short snout, and retractile claws. It is widely "
"kept as a pet or for catching mice, and many breeds have been "
"developed.\n&nbsp;&nbsp;their pet cat\n\n")
bot_response = (
"**cat**:\n\n* (**noun**) a small domesticated carnivorous mammal "
"with soft fur, a short snout, and retractile claws. It is widely "
"kept as a pet or for catching mice, and many breeds have been "
"developed.\n&nbsp;&nbsp;their pet cat\n\n"
)
with self.mock_http_conversation('test_single_type_word'):
self.verify_reply('cat', bot_response)
# Multi-type word.
bot_response = ("**help**:\n\n"
"* (**verb**) make it easier or possible for (someone) to do something by offering them one's services or resources.\n"
"&nbsp;&nbsp;they helped her with domestic chores\n\n\n"
"* (**verb**) serve someone with (food or drink).\n"
"&nbsp;&nbsp;may I help you to some more meat?\n\n\n"
"* (**verb**) cannot or could not avoid.\n"
"&nbsp;&nbsp;he couldn't help laughing\n\n\n"
"* (**noun**) the action of helping someone to do something.\n"
"&nbsp;&nbsp;I asked for help from my neighbours\n\n\n"
"* (**exclamation**) used as an appeal for urgent assistance.\n"
"&nbsp;&nbsp;Help! I'm drowning!\n\n")
bot_response = (
"**help**:\n\n"
"* (**verb**) make it easier or possible for (someone) to do something by offering them one's services or resources.\n"
"&nbsp;&nbsp;they helped her with domestic chores\n\n\n"
"* (**verb**) serve someone with (food or drink).\n"
"&nbsp;&nbsp;may I help you to some more meat?\n\n\n"
"* (**verb**) cannot or could not avoid.\n"
"&nbsp;&nbsp;he couldn't help laughing\n\n\n"
"* (**noun**) the action of helping someone to do something.\n"
"&nbsp;&nbsp;I asked for help from my neighbours\n\n\n"
"* (**exclamation**) used as an appeal for urgent assistance.\n"
"&nbsp;&nbsp;Help! I'm drowning!\n\n"
)
with self.mock_http_conversation('test_multi_type_word'):
self.verify_reply('help', bot_response)
@ -49,8 +53,5 @@ class TestDefineBot(BotTestCase, DefaultTests):
self.verify_reply('', bot_response)
def test_connection_error(self) -> None:
with patch('requests.get', side_effect=Exception), \
patch('logging.exception'):
self.verify_reply(
'aeroplane',
'**aeroplane**:\nCould not load definition.')
with patch('requests.get', side_effect=Exception), patch('logging.exception'):
self.verify_reply('aeroplane', '**aeroplane**:\nCould not load definition.')

View file

@ -12,6 +12,7 @@ This bot will interact with dialogflow bots.
Simply send this bot a message, and it will respond depending on the configured bot's behaviour.
'''
def get_bot_result(message_content: str, config: Dict[str, str], sender_id: str) -> str:
if message_content.strip() == '' or message_content.strip() == 'help':
return config['bot_info']
@ -24,7 +25,9 @@ def get_bot_result(message_content: str, config: Dict[str, str], sender_id: str)
res_str = response.read().decode('utf8', 'ignore')
res_json = json.loads(res_str)
if res_json['status']['errorType'] != 'success' and 'result' not in res_json.keys():
return 'Error {}: {}.'.format(res_json['status']['code'], res_json['status']['errorDetails'])
return 'Error {}: {}.'.format(
res_json['status']['code'], res_json['status']['errorDetails']
)
if res_json['result']['fulfillment']['speech'] == '':
if 'alternateResult' in res_json.keys():
if res_json['alternateResult']['fulfillment']['speech'] != '':
@ -35,6 +38,7 @@ def get_bot_result(message_content: str, config: Dict[str, str], sender_id: str)
logging.exception(str(e))
return 'Error. {}.'.format(str(e))
class DialogFlowHandler:
'''
This plugin allows users to easily add their own
@ -54,4 +58,5 @@ class DialogFlowHandler:
result = get_bot_result(message['content'], self.config_info, message['sender_id'])
bot_handler.send_reply(message, result)
handler_class = DialogFlowHandler

View file

@ -6,14 +6,15 @@ from unittest.mock import patch
from zulip_bots.test_lib import BotTestCase, DefaultTests, read_bot_fixture_data
class MockHttplibRequest():
class MockHttplibRequest:
def __init__(self, response: str) -> None:
self.response = response
def read(self) -> ByteString:
return json.dumps(self.response).encode()
class MockTextRequest():
class MockTextRequest:
def __init__(self) -> None:
self.session_id = ""
self.query = ""
@ -22,6 +23,7 @@ class MockTextRequest():
def getresponse(self) -> MockHttplibRequest:
return MockHttplibRequest(self.response)
@contextmanager
def mock_dialogflow(test_name: str, bot_name: str) -> Iterator[None]:
response_data = read_bot_fixture_data(bot_name, test_name)
@ -38,12 +40,14 @@ def mock_dialogflow(test_name: str, bot_name: str) -> Iterator[None]:
mock_text_request.return_value = request
yield
class TestDialogFlowBot(BotTestCase, DefaultTests):
bot_name = 'dialogflow'
def _test(self, test_name: str, message: str, response: str) -> None:
with self.mock_config_info({'key': 'abcdefg', 'bot_info': 'bot info foo bar'}), \
mock_dialogflow(test_name, 'dialogflow'):
with self.mock_config_info(
{'key': 'abcdefg', 'bot_info': 'bot info foo bar'}
), mock_dialogflow(test_name, 'dialogflow'):
self.verify_reply(message, response)
def test_normal(self) -> None:

View file

@ -7,6 +7,7 @@ from zulip_bots.lib import BotHandler
URL = "[{name}](https://www.dropbox.com/home{path})"
class DropboxHandler:
'''
This bot allows you to easily share, search and upload files
@ -28,6 +29,7 @@ class DropboxHandler:
msg = dbx_command(self.client, command)
bot_handler.send_reply(message, msg)
def get_help() -> str:
return '''
Example commands:
@ -44,6 +46,7 @@ def get_help() -> str:
```
'''
def get_usage_examples() -> str:
return '''
Usage:
@ -61,15 +64,17 @@ def get_usage_examples() -> str:
```
'''
REGEXES = dict(
command='(ls|mkdir|read|rm|write|search|usage|help)',
path=r'(\S+)',
optional_path=r'(\S*)',
some_text='(.+?)',
folder=r'?(?:--fd (\S+))?',
max_results=r'?(?:--mr (\d+))?'
max_results=r'?(?:--mr (\d+))?',
)
def get_commands() -> Dict[str, Tuple[Any, List[str]]]:
return {
'help': (dbx_help, ['command']),
@ -83,12 +88,13 @@ def get_commands() -> Dict[str, Tuple[Any, List[str]]]:
'usage': (dbx_usage, []),
}
def dbx_command(client: Any, cmd: str) -> str:
cmd = cmd.strip()
if cmd == 'help':
return get_help()
cmd_name = cmd.split()[0]
cmd_args = cmd[len(cmd_name):].strip()
cmd_args = cmd[len(cmd_name) :].strip()
commands = get_commands()
if cmd_name not in commands:
return 'ERROR: unrecognized command\n' + get_help()
@ -102,6 +108,7 @@ def dbx_command(client: Any, cmd: str) -> str:
else:
return 'ERROR: ' + syntax_help(cmd_name)
def syntax_help(cmd_name: str) -> str:
commands = get_commands()
f, arg_names = commands[cmd_name]
@ -112,23 +119,29 @@ def syntax_help(cmd_name: str) -> str:
cmd = cmd_name
return 'syntax: {}'.format(cmd)
def dbx_help(client: Any, cmd_name: str) -> str:
return syntax_help(cmd_name)
def dbx_usage(client: Any) -> str:
return get_usage_examples()
def dbx_mkdir(client: Any, fn: str) -> str:
fn = '/' + fn # foo/boo -> /foo/boo
try:
result = client.files_create_folder(fn)
msg = "CREATED FOLDER: " + URL.format(name=result.name, path=result.path_lower)
except Exception:
msg = "Please provide a correct folder path and name.\n"\
"Usage: `mkdir <foldername>` to create a folder."
msg = (
"Please provide a correct folder path and name.\n"
"Usage: `mkdir <foldername>` to create a folder."
)
return msg
def dbx_ls(client: Any, fn: str) -> str:
if fn != '':
fn = '/' + fn
@ -144,12 +157,15 @@ def dbx_ls(client: Any, fn: str) -> str:
msg = '`No files available`'
except Exception:
msg = "Please provide a correct folder path\n"\
"Usage: `ls <foldername>` to list folders in directory\n"\
"or simply `ls` for listing folders in the root directory"
msg = (
"Please provide a correct folder path\n"
"Usage: `ls <foldername>` to list folders in directory\n"
"or simply `ls` for listing folders in the root directory"
)
return msg
def dbx_rm(client: Any, fn: str) -> str:
fn = '/' + fn
@ -157,10 +173,13 @@ def dbx_rm(client: Any, fn: str) -> str:
result = client.files_delete(fn)
msg = "DELETED File/Folder : " + URL.format(name=result.name, path=result.path_lower)
except Exception:
msg = "Please provide a correct folder path and name.\n"\
"Usage: `rm <foldername>` to delete a folder in root directory."
msg = (
"Please provide a correct folder path and name.\n"
"Usage: `rm <foldername>` to delete a folder in root directory."
)
return msg
def dbx_write(client: Any, fn: str, content: str) -> str:
fn = '/' + fn
@ -172,6 +191,7 @@ def dbx_write(client: Any, fn: str, content: str) -> str:
return msg
def dbx_read(client: Any, fn: str) -> str:
fn = '/' + fn
@ -179,10 +199,13 @@ def dbx_read(client: Any, fn: str) -> str:
result = client.files_download(fn)
msg = "**{}** :\n{}".format(result[0].name, result[1].text)
except Exception:
msg = "Please provide a correct file path\nUsage: `read <filename>` to read content of a file"
msg = (
"Please provide a correct file path\nUsage: `read <filename>` to read content of a file"
)
return msg
def dbx_search(client: Any, query: str, folder: str, max_results: str) -> str:
if folder is None:
folder = ''
@ -201,17 +224,22 @@ def dbx_search(client: Any, query: str, folder: str, max_results: str) -> str:
msg = '\n'.join(msg_list)
except Exception:
msg = "Usage: `search <foldername> query --mr 10 --fd <folderName>`\n"\
"Note:`--mr <int>` is optional and is used to specify maximun results.\n"\
" `--fd <folderName>` to search in specific folder."
msg = (
"Usage: `search <foldername> query --mr 10 --fd <folderName>`\n"
"Note:`--mr <int>` is optional and is used to specify maximun results.\n"
" `--fd <folderName>` to search in specific folder."
)
if msg == '':
msg = "No files/folders found matching your query.\n"\
"For file name searching, the last token is used for prefix matching"\
" (i.e. “bat c” matches “bat cave” but not “batman car”)."
msg = (
"No files/folders found matching your query.\n"
"For file name searching, the last token is used for prefix matching"
" (i.e. “bat c” matches “bat cave” but not “batman car”)."
)
return msg
def dbx_share(client: Any, fn: str):
fn = '/' + fn
try:
@ -222,4 +250,5 @@ def dbx_share(client: Any, fn: str):
return msg
handler_class = DropboxHandler

View file

@ -13,50 +13,49 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests
def get_root_files_list(*args, **kwargs):
return MockListFolderResult(
entries = [
MockFileMetadata('foo', '/foo'),
MockFileMetadata('boo', '/boo')
],
has_more = False
entries=[MockFileMetadata('foo', '/foo'), MockFileMetadata('boo', '/boo')], has_more=False
)
def get_folder_files_list(*args, **kwargs):
return MockListFolderResult(
entries = [
entries=[
MockFileMetadata('moo', '/foo/moo'),
MockFileMetadata('noo', '/foo/noo'),
],
has_more = False
has_more=False,
)
def get_empty_files_list(*args, **kwargs):
return MockListFolderResult(
entries = [],
has_more = False
)
return MockListFolderResult(entries=[], has_more=False)
def create_file(*args, **kwargs):
return MockFileMetadata('foo', '/foo')
def download_file(*args, **kwargs):
return [MockFileMetadata('foo', '/foo'), MockHttpResponse('boo')]
def search_files(*args, **kwargs):
return MockSearchResult([
MockSearchMatch(
MockFileMetadata('foo', '/foo')
),
MockSearchMatch(
MockFileMetadata('fooboo', '/fooboo')
)
])
return MockSearchResult(
[
MockSearchMatch(MockFileMetadata('foo', '/foo')),
MockSearchMatch(MockFileMetadata('fooboo', '/fooboo')),
]
)
def get_empty_search_result(*args, **kwargs):
return MockSearchResult([])
def get_shared_link(*args, **kwargs):
return MockPathLinkMetadata('http://www.foo.com/boo')
def get_help() -> str:
return '''
Example commands:
@ -73,6 +72,7 @@ def get_help() -> str:
```
'''
class TestDropboxBot(BotTestCase, DefaultTests):
bot_name = "dropbox_share"
config_info = {"access_token": "1234567890"}
@ -83,116 +83,151 @@ class TestDropboxBot(BotTestCase, DefaultTests):
self.verify_reply('help', get_help())
def test_dbx_ls_root(self):
bot_response = " - [foo](https://www.dropbox.com/home/foo)\n"\
" - [boo](https://www.dropbox.com/home/boo)"
with patch('dropbox.Dropbox.files_list_folder', side_effect=get_root_files_list), \
self.mock_config_info(self.config_info):
bot_response = (
" - [foo](https://www.dropbox.com/home/foo)\n"
" - [boo](https://www.dropbox.com/home/boo)"
)
with patch(
'dropbox.Dropbox.files_list_folder', side_effect=get_root_files_list
), self.mock_config_info(self.config_info):
self.verify_reply("ls", bot_response)
def test_dbx_ls_folder(self):
bot_response = " - [moo](https://www.dropbox.com/home/foo/moo)\n"\
" - [noo](https://www.dropbox.com/home/foo/noo)"
with patch('dropbox.Dropbox.files_list_folder', side_effect=get_folder_files_list), \
self.mock_config_info(self.config_info):
bot_response = (
" - [moo](https://www.dropbox.com/home/foo/moo)\n"
" - [noo](https://www.dropbox.com/home/foo/noo)"
)
with patch(
'dropbox.Dropbox.files_list_folder', side_effect=get_folder_files_list
), self.mock_config_info(self.config_info):
self.verify_reply("ls foo", bot_response)
def test_dbx_ls_empty(self):
bot_response = '`No files available`'
with patch('dropbox.Dropbox.files_list_folder', side_effect=get_empty_files_list), \
self.mock_config_info(self.config_info):
with patch(
'dropbox.Dropbox.files_list_folder', side_effect=get_empty_files_list
), self.mock_config_info(self.config_info):
self.verify_reply("ls", bot_response)
def test_dbx_ls_error(self):
bot_response = "Please provide a correct folder path\n"\
"Usage: `ls <foldername>` to list folders in directory\n"\
"or simply `ls` for listing folders in the root directory"
with patch('dropbox.Dropbox.files_list_folder', side_effect=Exception()), \
self.mock_config_info(self.config_info):
bot_response = (
"Please provide a correct folder path\n"
"Usage: `ls <foldername>` to list folders in directory\n"
"or simply `ls` for listing folders in the root directory"
)
with patch(
'dropbox.Dropbox.files_list_folder', side_effect=Exception()
), self.mock_config_info(self.config_info):
self.verify_reply("ls", bot_response)
def test_dbx_mkdir(self):
bot_response = "CREATED FOLDER: [foo](https://www.dropbox.com/home/foo)"
with patch('dropbox.Dropbox.files_create_folder', side_effect=create_file), \
self.mock_config_info(self.config_info):
with patch(
'dropbox.Dropbox.files_create_folder', side_effect=create_file
), self.mock_config_info(self.config_info):
self.verify_reply('mkdir foo', bot_response)
def test_dbx_mkdir_error(self):
bot_response = "Please provide a correct folder path and name.\n"\
"Usage: `mkdir <foldername>` to create a folder."
with patch('dropbox.Dropbox.files_create_folder', side_effect=Exception()), \
self.mock_config_info(self.config_info):
bot_response = (
"Please provide a correct folder path and name.\n"
"Usage: `mkdir <foldername>` to create a folder."
)
with patch(
'dropbox.Dropbox.files_create_folder', side_effect=Exception()
), self.mock_config_info(self.config_info):
self.verify_reply('mkdir foo/bar', bot_response)
def test_dbx_rm(self):
bot_response = "DELETED File/Folder : [foo](https://www.dropbox.com/home/foo)"
with patch('dropbox.Dropbox.files_delete', side_effect=create_file), \
self.mock_config_info(self.config_info):
with patch('dropbox.Dropbox.files_delete', side_effect=create_file), self.mock_config_info(
self.config_info
):
self.verify_reply('rm foo', bot_response)
def test_dbx_rm_error(self):
bot_response = "Please provide a correct folder path and name.\n"\
"Usage: `rm <foldername>` to delete a folder in root directory."
with patch('dropbox.Dropbox.files_delete', side_effect=Exception()), \
self.mock_config_info(self.config_info):
bot_response = (
"Please provide a correct folder path and name.\n"
"Usage: `rm <foldername>` to delete a folder in root directory."
)
with patch('dropbox.Dropbox.files_delete', side_effect=Exception()), self.mock_config_info(
self.config_info
):
self.verify_reply('rm foo', bot_response)
def test_dbx_write(self):
bot_response = "Written to file: [foo](https://www.dropbox.com/home/foo)"
with patch('dropbox.Dropbox.files_upload', side_effect=create_file), \
self.mock_config_info(self.config_info):
with patch('dropbox.Dropbox.files_upload', side_effect=create_file), self.mock_config_info(
self.config_info
):
self.verify_reply('write foo boo', bot_response)
def test_dbx_write_error(self):
bot_response = "Incorrect file path or file already exists.\nUsage: `write <filename> CONTENT`"
with patch('dropbox.Dropbox.files_upload', side_effect=Exception()), \
self.mock_config_info(self.config_info):
bot_response = (
"Incorrect file path or file already exists.\nUsage: `write <filename> CONTENT`"
)
with patch('dropbox.Dropbox.files_upload', side_effect=Exception()), self.mock_config_info(
self.config_info
):
self.verify_reply('write foo boo', bot_response)
def test_dbx_read(self):
bot_response = "**foo** :\nboo"
with patch('dropbox.Dropbox.files_download', side_effect=download_file), \
self.mock_config_info(self.config_info):
with patch(
'dropbox.Dropbox.files_download', side_effect=download_file
), self.mock_config_info(self.config_info):
self.verify_reply('read foo', bot_response)
def test_dbx_read_error(self):
bot_response = "Please provide a correct file path\n"\
"Usage: `read <filename>` to read content of a file"
with patch('dropbox.Dropbox.files_download', side_effect=Exception()), \
self.mock_config_info(self.config_info):
bot_response = (
"Please provide a correct file path\n"
"Usage: `read <filename>` to read content of a file"
)
with patch(
'dropbox.Dropbox.files_download', side_effect=Exception()
), self.mock_config_info(self.config_info):
self.verify_reply('read foo', bot_response)
def test_dbx_search(self):
bot_response = " - [foo](https://www.dropbox.com/home/foo)\n - [fooboo](https://www.dropbox.com/home/fooboo)"
with patch('dropbox.Dropbox.files_search', side_effect=search_files), \
self.mock_config_info(self.config_info):
with patch('dropbox.Dropbox.files_search', side_effect=search_files), self.mock_config_info(
self.config_info
):
self.verify_reply('search foo', bot_response)
def test_dbx_search_empty(self):
bot_response = "No files/folders found matching your query.\n"\
"For file name searching, the last token is used for prefix matching"\
" (i.e. “bat c” matches “bat cave” but not “batman car”)."
with patch('dropbox.Dropbox.files_search', side_effect=get_empty_search_result), \
self.mock_config_info(self.config_info):
bot_response = (
"No files/folders found matching your query.\n"
"For file name searching, the last token is used for prefix matching"
" (i.e. “bat c” matches “bat cave” but not “batman car”)."
)
with patch(
'dropbox.Dropbox.files_search', side_effect=get_empty_search_result
), self.mock_config_info(self.config_info):
self.verify_reply('search boo --fd foo', bot_response)
def test_dbx_search_error(self):
bot_response = "Usage: `search <foldername> query --mr 10 --fd <folderName>`\n"\
"Note:`--mr <int>` is optional and is used to specify maximun results.\n"\
" `--fd <folderName>` to search in specific folder."
with patch('dropbox.Dropbox.files_search', side_effect=Exception()), \
self.mock_config_info(self.config_info):
bot_response = (
"Usage: `search <foldername> query --mr 10 --fd <folderName>`\n"
"Note:`--mr <int>` is optional and is used to specify maximun results.\n"
" `--fd <folderName>` to search in specific folder."
)
with patch('dropbox.Dropbox.files_search', side_effect=Exception()), self.mock_config_info(
self.config_info
):
self.verify_reply('search foo', bot_response)
def test_dbx_share(self):
bot_response = 'http://www.foo.com/boo'
with patch('dropbox.Dropbox.sharing_create_shared_link', side_effect=get_shared_link), \
self.mock_config_info(self.config_info):
with patch(
'dropbox.Dropbox.sharing_create_shared_link', side_effect=get_shared_link
), self.mock_config_info(self.config_info):
self.verify_reply('share boo', bot_response)
def test_dbx_share_error(self):
bot_response = "Please provide a correct file name.\nUsage: `share <filename>`"
with patch('dropbox.Dropbox.sharing_create_shared_link', side_effect=Exception()), \
self.mock_config_info(self.config_info):
with patch(
'dropbox.Dropbox.sharing_create_shared_link', side_effect=Exception()
), self.mock_config_info(self.config_info):
self.verify_reply('share boo', bot_response)
def test_dbx_help(self):

View file

@ -6,23 +6,28 @@ class MockFileMetadata:
self.name = name
self.path_lower = path_lower
class MockListFolderResult:
def __init__(self, entries: str, has_more: str):
self.entries = entries
self.has_more = has_more
class MockSearchMatch:
def __init__(self, metadata: List[MockFileMetadata]):
self.metadata = metadata
class MockSearchResult:
def __init__(self, matches: List[MockSearchMatch]):
self.matches = matches
class MockPathLinkMetadata:
def __init__(self, url: str):
self.url = url
class MockHttpResponse:
def __init__(self, text: str):
self.text = text

View file

@ -20,6 +20,7 @@ def encrypt(text: str) -> str:
return newtext
class EncryptHandler:
'''
This bot allows users to quickly encrypt messages using ROT13 encryption.
@ -43,4 +44,5 @@ class EncryptHandler:
send_content = "Encrypted/Decrypted text: " + temp_content
return send_content
handler_class = EncryptHandler

View file

@ -40,4 +40,5 @@ class FileUploaderHandler:
uploaded_file_reply = '[{}]({})'.format(path.name, upload['uri'])
bot_handler.send_reply(message, uploaded_file_reply)
handler_class = FileUploaderHandler

View file

@ -15,20 +15,28 @@ class TestFileUploaderBot(BotTestCase, DefaultTests):
@patch('pathlib.Path.is_file', return_value=True)
def test_file_upload_failed(self, is_file: Mock, resolve: Mock) -> None:
server_reply = dict(result='', msg='error')
with patch('zulip_bots.test_lib.StubBotHandler.upload_file_from_path',
return_value=server_reply):
self.verify_reply('file.txt', 'Failed to upload `{}` file: error'.format(Path('file.txt').resolve()))
with patch(
'zulip_bots.test_lib.StubBotHandler.upload_file_from_path', return_value=server_reply
):
self.verify_reply(
'file.txt', 'Failed to upload `{}` file: error'.format(Path('file.txt').resolve())
)
@patch('pathlib.Path.resolve', return_value=Path('/file.txt'))
@patch('pathlib.Path.is_file', return_value=True)
def test_file_upload_success(self, is_file: Mock, resolve: Mock) -> None:
server_reply = dict(result='success', uri='https://file/uri')
with patch('zulip_bots.test_lib.StubBotHandler.upload_file_from_path',
return_value=server_reply):
with patch(
'zulip_bots.test_lib.StubBotHandler.upload_file_from_path', return_value=server_reply
):
self.verify_reply('file.txt', '[file.txt](https://file/uri)')
def test_help(self):
self.verify_reply('help',
('Use this bot with any of the following commands:'
'\n* `@uploader <local_file_path>` : Upload a file, where `<local_file_path>` is the path to the file'
'\n* `@uploader help` : Display help message'))
self.verify_reply(
'help',
(
'Use this bot with any of the following commands:'
'\n* `@uploader <local_file_path>` : Upload a file, where `<local_file_path>` is the path to the file'
'\n* `@uploader help` : Display help message'
),
)

View file

@ -21,6 +21,7 @@ def find_recipient_id(users: List[Any], recipient_name: str) -> str:
if recipient_name == user['firstName']:
return user['id']
# Make request to given flock URL and return a two-element tuple
# whose left-hand value contains JSON body of response (or None if request failed)
# and whose right-hand value contains an error message (or None if request succeeded)
@ -34,14 +35,15 @@ def make_flock_request(url: str, params: Dict[str, str]) -> Tuple[Any, str]:
right now.\nPlease try again later"
return (None, error)
# Returns two-element tuple whose left-hand value contains recipient
# user's ID (or None if it was not found) and right-hand value contains
# an error message (or None if recipient user's ID was found)
def get_recipient_id(recipient_name: str, config: Dict[str, str]) -> Tuple[Optional[str], Optional[str]]:
def get_recipient_id(
recipient_name: str, config: Dict[str, str]
) -> Tuple[Optional[str], Optional[str]]:
token = config['token']
payload = {
'token': token
}
payload = {'token': token}
users, error = make_flock_request(USERS_LIST_URL, payload)
if users is None:
return (None, error)
@ -53,6 +55,7 @@ def get_recipient_id(recipient_name: str, config: Dict[str, str]) -> Tuple[Optio
else:
return (recipient_id, None)
# This handles the message sending work.
def get_flock_response(content: str, config: Dict[str, str]) -> str:
token = config['token']
@ -67,11 +70,7 @@ def get_flock_response(content: str, config: Dict[str, str]) -> str:
if len(str(recipient_id)) > 30:
return "Found user is invalid."
payload = {
'to': recipient_id,
'text': message,
'token': token
}
payload = {'to': recipient_id, 'text': message, 'token': token}
res, error = make_flock_request(SEND_MESSAGE_URL, payload)
if res is None:
return error
@ -81,6 +80,7 @@ def get_flock_response(content: str, config: Dict[str, str]) -> str:
else:
return "Message sending failed :slightly_frowning_face:. Please try again."
def get_flock_bot_response(content: str, config: Dict[str, str]) -> None:
content = content.strip()
if content == '' or content == 'help':
@ -89,6 +89,7 @@ def get_flock_bot_response(content: str, config: Dict[str, str]) -> None:
result = get_flock_response(content, config)
return result
class FlockHandler:
'''
This is flock bot. Now you can send messages to any of your
@ -106,4 +107,5 @@ right from Zulip.'''
response = get_flock_bot_response(message['content'], self.config_info)
bot_handler.send_reply(message, response)
handler_class = FlockHandler

View file

@ -9,11 +9,7 @@ class TestFlockBot(BotTestCase, DefaultTests):
bot_name = "flock"
normal_config = {"token": "12345"}
message_config = {
"token": "12345",
"text": "Ricky: test message",
"to": "u:somekey"
}
message_config = {"token": "12345", "text": "Ricky: test message", "to": "u:somekey"}
help_message = '''
You can send messages to any Flock user associated with your account from Zulip.
@ -27,52 +23,62 @@ You can send messages to any Flock user associated with your account from Zulip.
self.verify_reply('', self.help_message)
def test_fetch_id_connection_error(self) -> None:
with self.mock_config_info(self.normal_config), \
patch('requests.get', side_effect=ConnectionError()), \
patch('logging.exception'):
self.verify_reply('tyler: Hey tyler', "Uh-Oh, couldn\'t process the request \
right now.\nPlease try again later")
with self.mock_config_info(self.normal_config), patch(
'requests.get', side_effect=ConnectionError()
), patch('logging.exception'):
self.verify_reply(
'tyler: Hey tyler',
"Uh-Oh, couldn\'t process the request \
right now.\nPlease try again later",
)
def test_response_connection_error(self) -> None:
with self.mock_config_info(self.message_config), \
patch('requests.get', side_effect=ConnectionError()), \
patch('logging.exception'):
self.verify_reply('Ricky: test message', "Uh-Oh, couldn\'t process the request \
right now.\nPlease try again later")
with self.mock_config_info(self.message_config), patch(
'requests.get', side_effect=ConnectionError()
), patch('logging.exception'):
self.verify_reply(
'Ricky: test message',
"Uh-Oh, couldn\'t process the request \
right now.\nPlease try again later",
)
def test_no_recipient_found(self) -> None:
bot_response = "No user found. Make sure you typed it correctly."
with self.mock_config_info(self.normal_config), \
self.mock_http_conversation('test_no_recipient_found'):
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
'test_no_recipient_found'
):
self.verify_reply('david: hello', bot_response)
def test_found_invalid_recipient(self) -> None:
bot_response = "Found user is invalid."
with self.mock_config_info(self.normal_config), \
self.mock_http_conversation('test_found_invalid_recipient'):
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
'test_found_invalid_recipient'
):
self.verify_reply('david: hello', bot_response)
@patch('zulip_bots.bots.flock.flock.get_recipient_id')
def test_message_send_connection_error(self, get_recipient_id: str) -> None:
bot_response = "Uh-Oh, couldn't process the request right now.\nPlease try again later"
get_recipient_id.return_value = ["u:userid", None]
with self.mock_config_info(self.normal_config), \
patch('requests.get', side_effect=ConnectionError()), \
patch('logging.exception'):
with self.mock_config_info(self.normal_config), patch(
'requests.get', side_effect=ConnectionError()
), patch('logging.exception'):
self.verify_reply('Rishabh: hi there', bot_response)
@patch('zulip_bots.bots.flock.flock.get_recipient_id')
def test_message_send_success(self, get_recipient_id: str) -> None:
bot_response = "Message sent."
get_recipient_id.return_value = ["u:userid", None]
with self.mock_config_info(self.normal_config), \
self.mock_http_conversation('test_message_send_success'):
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
'test_message_send_success'
):
self.verify_reply('Rishabh: hi there', bot_response)
@patch('zulip_bots.bots.flock.flock.get_recipient_id')
def test_message_send_failed(self, get_recipient_id: str) -> None:
bot_response = "Message sending failed :slightly_frowning_face:. Please try again."
get_recipient_id.return_value = ["u:invalid", None]
with self.mock_config_info(self.normal_config), \
self.mock_http_conversation('test_message_send_failed'):
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
'test_message_send_failed'
):
self.verify_reply('Rishabh: hi there', bot_response)

View file

@ -32,18 +32,22 @@ class FollowupHandler:
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
if message['content'] == '':
bot_response = "Please specify the message you want to send to followup stream after @mention-bot"
bot_response = (
"Please specify the message you want to send to followup stream after @mention-bot"
)
bot_handler.send_reply(message, bot_response)
elif message['content'] == 'help':
bot_handler.send_reply(message, self.usage())
else:
bot_response = self.get_bot_followup_response(message)
bot_handler.send_message(dict(
type='stream',
to=self.stream,
subject=message['sender_email'],
content=bot_response,
))
bot_handler.send_message(
dict(
type='stream',
to=self.stream,
subject=message['sender_email'],
content=bot_response,
)
)
def get_bot_followup_response(self, message: Dict[str, str]) -> str:
original_content = message['content']
@ -53,4 +57,5 @@ class FollowupHandler:
return new_content
handler_class = FollowupHandler

View file

@ -31,7 +31,9 @@ class TestFollowUpBot(BotTestCase, DefaultTests):
self.assertEqual(response['to'], 'issue')
def test_bot_responds_to_empty_message(self) -> None:
bot_response = 'Please specify the message you want to send to followup stream after @mention-bot'
bot_response = (
'Please specify the message you want to send to followup stream after @mention-bot'
)
with self.mock_config_info({'stream': 'followup'}):
self.verify_reply('', bot_response)

View file

@ -13,7 +13,7 @@ class FrontHandler:
('delete', "Delete a conversation."),
('spam', "Mark a conversation as spam."),
('open', "Restore a conversation."),
('comment <text>', "Leave a comment.")
('comment <text>', "Leave a comment."),
]
CNV_ID_REGEXP = 'cnv_(?P<id>[0-9a-z]+)'
COMMENT_PREFIX = "comment "
@ -40,9 +40,11 @@ class FrontHandler:
return response
def archive(self, bot_handler: BotHandler) -> str:
response = requests.patch(self.FRONT_API.format(self.conversation_id),
headers={"Authorization": self.auth},
json={"status": "archived"})
response = requests.patch(
self.FRONT_API.format(self.conversation_id),
headers={"Authorization": self.auth},
json={"status": "archived"},
)
if response.status_code not in (200, 204):
return "Something went wrong."
@ -50,9 +52,11 @@ class FrontHandler:
return "Conversation was archived."
def delete(self, bot_handler: BotHandler) -> str:
response = requests.patch(self.FRONT_API.format(self.conversation_id),
headers={"Authorization": self.auth},
json={"status": "deleted"})
response = requests.patch(
self.FRONT_API.format(self.conversation_id),
headers={"Authorization": self.auth},
json={"status": "deleted"},
)
if response.status_code not in (200, 204):
return "Something went wrong."
@ -60,9 +64,11 @@ class FrontHandler:
return "Conversation was deleted."
def spam(self, bot_handler: BotHandler) -> str:
response = requests.patch(self.FRONT_API.format(self.conversation_id),
headers={"Authorization": self.auth},
json={"status": "spam"})
response = requests.patch(
self.FRONT_API.format(self.conversation_id),
headers={"Authorization": self.auth},
json={"status": "spam"},
)
if response.status_code not in (200, 204):
return "Something went wrong."
@ -70,9 +76,11 @@ class FrontHandler:
return "Conversation was marked as spam."
def restore(self, bot_handler: BotHandler) -> str:
response = requests.patch(self.FRONT_API.format(self.conversation_id),
headers={"Authorization": self.auth},
json={"status": "open"})
response = requests.patch(
self.FRONT_API.format(self.conversation_id),
headers={"Authorization": self.auth},
json={"status": "open"},
)
if response.status_code not in (200, 204):
return "Something went wrong."
@ -80,8 +88,11 @@ class FrontHandler:
return "Conversation was restored."
def comment(self, bot_handler: BotHandler, **kwargs: Any) -> str:
response = requests.post(self.FRONT_API.format(self.conversation_id) + "/comments",
headers={"Authorization": self.auth}, json=kwargs)
response = requests.post(
self.FRONT_API.format(self.conversation_id) + "/comments",
headers={"Authorization": self.auth},
json=kwargs,
)
if response.status_code not in (200, 201):
return "Something went wrong."
@ -93,9 +104,12 @@ class FrontHandler:
result = re.search(self.CNV_ID_REGEXP, message['subject'])
if not result:
bot_handler.send_reply(message, "No coversation ID found. Please make "
"sure that the name of the topic "
"contains a valid coversation ID.")
bot_handler.send_reply(
message,
"No coversation ID found. Please make "
"sure that the name of the topic "
"contains a valid coversation ID.",
)
return None
self.conversation_id = result.group()
@ -118,10 +132,11 @@ class FrontHandler:
elif command.startswith(self.COMMENT_PREFIX):
kwargs = {
'author_id': "alt:email:" + message['sender_email'],
'body': command[len(self.COMMENT_PREFIX):]
'body': command[len(self.COMMENT_PREFIX) :],
}
bot_handler.send_reply(message, self.comment(bot_handler, **kwargs))
else:
bot_handler.send_reply(message, "Unknown command. Use `help` for instructions.")
handler_class = FrontHandler

View file

@ -24,11 +24,14 @@ class TestFrontBot(BotTestCase, DefaultTests):
def test_help(self) -> None:
with self.mock_config_info({'api_key': "TEST"}):
self.verify_reply('help', "`archive` Archive a conversation.\n"
"`delete` Delete a conversation.\n"
"`spam` Mark a conversation as spam.\n"
"`open` Restore a conversation.\n"
"`comment <text>` Leave a comment.\n")
self.verify_reply(
'help',
"`archive` Archive a conversation.\n"
"`delete` Delete a conversation.\n"
"`spam` Mark a conversation as spam.\n"
"`open` Restore a conversation.\n"
"`comment <text>` Leave a comment.\n",
)
def test_archive(self) -> None:
with self.mock_config_info({'api_key': "TEST"}):
@ -94,6 +97,9 @@ class TestFrontBotWrongTopic(BotTestCase, DefaultTests):
def test_no_conversation_id(self) -> None:
with self.mock_config_info({'api_key': "TEST"}):
self.verify_reply('archive', "No coversation ID found. Please make "
"sure that the name of the topic "
"contains a valid coversation ID.")
self.verify_reply(
'archive',
"No coversation ID found. Please make "
"sure that the name of the topic "
"contains a valid coversation ID.",
)

View file

@ -26,12 +26,7 @@ class MockModel:
def __init__(self) -> None:
self.current_board = 'mock board'
def make_move(
self,
move: str,
player: int,
is_computer: bool = False
) -> Any:
def make_move(self, move: str, player: int, is_computer: bool = False) -> Any:
if not is_computer:
if int(move.replace('move ', '')) < 9:
return 'mock board'
@ -67,7 +62,7 @@ class GameHandlerBotHandler(GameAdapter):
gameMessageHandler,
rules,
max_players=2,
supports_computer=True
supports_computer=True,
)

View file

@ -16,7 +16,7 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
user_name: str = 'foo',
type: str = 'private',
stream: str = '',
subject: str = ''
subject: str = '',
) -> Dict[str, str]:
message = dict(
sender_email=user,
@ -38,7 +38,7 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
user_name: str = 'foo',
stream: str = '',
subject: str = '',
max_messages: int = 20
max_messages: int = 20,
) -> None:
'''
This function serves a similar purpose
@ -52,15 +52,12 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
_b, bot_handler = self._get_handlers()
type = 'private' if stream == '' else 'stream'
message = self.make_request_message(
request, user_name + '@example.com', user_name, type, stream, subject)
request, user_name + '@example.com', user_name, type, stream, subject
)
bot_handler.reset_transcript()
bot.handle_message(message, bot_handler)
responses = [
message
for (method, message)
in bot_handler.transcript
]
responses = [message for (method, message) in bot_handler.transcript]
first_response = responses[response_number]
self.assertEqual(expected_response, first_response['content'])
self.assertLessEqual(len(responses), max_messages)
@ -70,12 +67,19 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
bot, bot_handler = self._get_handlers()
message = {
'sender_email': '{}@example.com'.format(name),
'sender_full_name': '{}'.format(name)
'sender_full_name': '{}'.format(name),
}
bot.add_user_to_cache(message)
return bot
def setup_game(self, id: str = '', bot: Any = None, players: List[str] = ['foo', 'baz'], subject: str = 'test game', stream: str = 'test') -> Any:
def setup_game(
self,
id: str = '',
bot: Any = None,
players: List[str] = ['foo', 'baz'],
subject: str = 'test game',
stream: str = 'test',
) -> Any:
if bot is None:
bot, bot_handler = self._get_handlers()
for p in players:
@ -84,8 +88,7 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
game_id = 'abc123'
if id != '':
game_id = id
instance = GameInstance(bot, False, subject,
game_id, players_emails, stream)
instance = GameInstance(bot, False, subject, game_id, players_emails, stream)
bot.instances.update({game_id: instance})
instance.turn = -1
instance.start()
@ -95,8 +98,9 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
bot = self.add_user_to_cache('foo')
bot.email = 'test-bot@example.com'
self.add_user_to_cache('test-bot', bot)
instance = GameInstance(bot, False, 'test game', 'abc123', [
'foo@example.com', 'test-bot@example.com'], 'test')
instance = GameInstance(
bot, False, 'test game', 'abc123', ['foo@example.com', 'test-bot@example.com'], 'test'
)
bot.instances.update({'abc123': instance})
instance.start()
return bot
@ -132,41 +136,55 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
self.verify_response('foo bar baz', self.help_message(), 0)
def test_exception_handling(self) -> None:
with patch('logging.exception'), \
patch('zulip_bots.game_handler.GameAdapter.command_quit',
side_effect=Exception):
with patch('logging.exception'), patch(
'zulip_bots.game_handler.GameAdapter.command_quit', side_effect=Exception
):
self.verify_response('quit', 'Error .', 0)
def test_not_in_game_messages(self) -> None:
self.verify_response(
'move 3', 'You are not in a game at the moment. Type `help` for help.', 0, max_messages=1)
'move 3',
'You are not in a game at the moment. Type `help` for help.',
0,
max_messages=1,
)
self.verify_response(
'quit', 'You are not in a game. Type `help` for all commands.', 0, max_messages=1)
'quit', 'You are not in a game. Type `help` for all commands.', 0, max_messages=1
)
def test_start_game_with_name(self) -> None:
bot = self.add_user_to_cache('baz')
self.verify_response('start game with @**baz**',
'You\'ve sent an invitation to play foo test game with @**baz**', 1, bot=bot)
self.verify_response(
'start game with @**baz**',
'You\'ve sent an invitation to play foo test game with @**baz**',
1,
bot=bot,
)
self.assertEqual(len(bot.invites), 1)
def test_start_game_with_email(self) -> None:
bot = self.add_user_to_cache('baz')
self.verify_response('start game with baz@example.com',
'You\'ve sent an invitation to play foo test game with @**baz**', 1, bot=bot)
self.verify_response(
'start game with baz@example.com',
'You\'ve sent an invitation to play foo test game with @**baz**',
1,
bot=bot,
)
self.assertEqual(len(bot.invites), 1)
def test_join_game_and_start_in_stream(self) -> None:
bot = self.add_user_to_cache('baz')
self.add_user_to_cache('foo', bot)
bot.invites = {
'abc': {
'stream': 'test',
'subject': 'test game',
'host': 'foo@example.com'
}
}
self.verify_response('join', '@**baz** has joined the game', 0, bot=bot,
stream='test', subject='test game', user_name='baz')
bot.invites = {'abc': {'stream': 'test', 'subject': 'test game', 'host': 'foo@example.com'}}
self.verify_response(
'join',
'@**baz** has joined the game',
0,
bot=bot,
stream='test',
subject='test game',
user_name='baz',
)
self.assertEqual(len(bot.instances.keys()), 1)
def test_start_game_in_stream(self) -> None:
@ -175,7 +193,7 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
'**foo** wants to play **foo test game**. Type @**test-bot** join to play them!',
0,
stream='test',
subject='test game'
subject='test game',
)
def test_start_invite_game_in_stream(self) -> None:
@ -186,12 +204,19 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
2,
bot=bot,
stream='test',
subject='game test'
subject='game test',
)
def test_join_no_game(self) -> None:
self.verify_response('join', 'There is not a game in this subject. Type `help` for all commands.',
0, stream='test', subject='test game', user_name='baz', max_messages=1)
self.verify_response(
'join',
'There is not a game in this subject. Type `help` for all commands.',
0,
stream='test',
subject='test game',
user_name='baz',
max_messages=1,
)
def test_accept_invitation(self) -> None:
bot = self.add_user_to_cache('baz')
@ -201,80 +226,96 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
'subject': '###private###',
'stream': 'games',
'host': 'foo@example.com',
'baz@example.com': 'p'
'baz@example.com': 'p',
}
}
self.verify_response(
'accept', 'Accepted invitation to play **foo test game** from @**foo**.', 0, bot, 'baz')
'accept', 'Accepted invitation to play **foo test game** from @**foo**.', 0, bot, 'baz'
)
def test_decline_invitation(self) -> None:
bot = self.add_user_to_cache('baz')
self.add_user_to_cache('foo', bot)
bot.invites = {
'abc': {
'subject': '###private###',
'host': 'foo@example.com',
'baz@example.com': 'p'
}
'abc': {'subject': '###private###', 'host': 'foo@example.com', 'baz@example.com': 'p'}
}
self.verify_response(
'decline', 'Declined invitation to play **foo test game** from @**foo**.', 0, bot, 'baz')
'decline', 'Declined invitation to play **foo test game** from @**foo**.', 0, bot, 'baz'
)
def test_quit_invite(self) -> None:
bot = self.add_user_to_cache('foo')
bot.invites = {
'abc': {
'subject': '###private###',
'host': 'foo@example.com'
}
}
self.verify_response(
'quit', 'Game cancelled.\n**foo** quit.', 0, bot, 'foo')
bot.invites = {'abc': {'subject': '###private###', 'host': 'foo@example.com'}}
self.verify_response('quit', 'Game cancelled.\n**foo** quit.', 0, bot, 'foo')
def test_user_already_in_game_errors(self) -> None:
bot = self.setup_game()
self.verify_response('start game with @**baz**',
'You are already in a game. Type `quit` to leave.', 0, bot=bot, max_messages=1)
self.verify_response(
'start game', 'You are already in a game. Type `quit` to leave.', 0, bot=bot, stream='test', max_messages=1)
'start game with @**baz**',
'You are already in a game. Type `quit` to leave.',
0,
bot=bot,
max_messages=1,
)
self.verify_response(
'accept', 'You are already in a game. Type `quit` to leave.', 0, bot=bot, max_messages=1)
'start game',
'You are already in a game. Type `quit` to leave.',
0,
bot=bot,
stream='test',
max_messages=1,
)
self.verify_response(
'decline', 'You are already in a game. Type `quit` to leave.', 0, bot=bot, max_messages=1)
'accept', 'You are already in a game. Type `quit` to leave.', 0, bot=bot, max_messages=1
)
self.verify_response(
'join', 'You are already in a game. Type `quit` to leave.', 0, bot=bot, max_messages=1)
'decline',
'You are already in a game. Type `quit` to leave.',
0,
bot=bot,
max_messages=1,
)
self.verify_response(
'join', 'You are already in a game. Type `quit` to leave.', 0, bot=bot, max_messages=1
)
def test_register_command(self) -> None:
bot = self.add_user_to_cache('foo')
self.verify_response(
'register', 'Hello @**foo**. Thanks for registering!', 0, bot, 'foo')
self.verify_response('register', 'Hello @**foo**. Thanks for registering!', 0, bot, 'foo')
self.assertIn('foo@example.com', bot.user_cache.keys())
def test_no_active_invite_errors(self) -> None:
self.verify_response(
'accept', 'No active invites. Type `help` for commands.', 0)
self.verify_response(
'decline', 'No active invites. Type `help` for commands.', 0)
self.verify_response('accept', 'No active invites. Type `help` for commands.', 0)
self.verify_response('decline', 'No active invites. Type `help` for commands.', 0)
def test_wrong_number_of_players_message(self) -> None:
bot = self.add_user_to_cache('baz')
bot.min_players = 5
self.verify_response('start game with @**baz**',
'You must have at least 5 players to play.\nGame cancelled.', 0, bot=bot)
self.verify_response(
'start game with @**baz**',
'You must have at least 5 players to play.\nGame cancelled.',
0,
bot=bot,
)
bot.min_players = 2
bot.max_players = 1
self.verify_response('start game with @**baz**',
'The maximum number of players for this game is 1.', 0, bot=bot)
self.verify_response(
'start game with @**baz**',
'The maximum number of players for this game is 1.',
0,
bot=bot,
)
bot.max_players = 1
bot.invites = {
'abc': {
'stream': 'test',
'subject': 'test game',
'host': 'foo@example.com'
}
}
self.verify_response('join', 'This game is full.', 0, bot=bot,
stream='test', subject='test game', user_name='baz')
bot.invites = {'abc': {'stream': 'test', 'subject': 'test game', 'host': 'foo@example.com'}}
self.verify_response(
'join',
'This game is full.',
0,
bot=bot,
stream='test',
subject='test game',
user_name='baz',
)
def test_public_accept(self) -> None:
bot = self.add_user_to_cache('baz')
@ -284,70 +325,125 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
'stream': 'test',
'subject': 'test game',
'host': 'baz@example.com',
'foo@example.com': 'p'
'foo@example.com': 'p',
}
}
self.verify_response('accept', '@**foo** has accepted the invitation.',
0, bot=bot, stream='test', subject='test game')
self.verify_response(
'accept',
'@**foo** has accepted the invitation.',
0,
bot=bot,
stream='test',
subject='test game',
)
def test_start_game_with_computer(self) -> None:
self.verify_response('start game with @**test-bot**',
'Wait... That\'s me!', 4, stream='test', subject='test game')
self.verify_response(
'start game with @**test-bot**',
'Wait... That\'s me!',
4,
stream='test',
subject='test game',
)
def test_sent_by_bot(self) -> None:
with self.assertRaises(IndexError):
self.verify_response(
'foo', '', 0, user_name='test-bot', stream='test', subject='test game')
'foo', '', 0, user_name='test-bot', stream='test', subject='test game'
)
def test_forfeit(self) -> None:
bot = self.setup_game()
self.verify_response('forfeit', '**foo** forfeited!',
0, bot=bot, stream='test', subject='test game')
self.verify_response(
'forfeit', '**foo** forfeited!', 0, bot=bot, stream='test', subject='test game'
)
def test_draw(self) -> None:
bot = self.setup_game()
self.verify_response('draw', '**foo** has voted for a draw!\nType `draw` to accept',
0, bot=bot, stream='test', subject='test game')
self.verify_response('draw', 'It was a draw!', 0, bot=bot, stream='test',
subject='test game', user_name='baz')
self.verify_response(
'draw',
'**foo** has voted for a draw!\nType `draw` to accept',
0,
bot=bot,
stream='test',
subject='test game',
)
self.verify_response(
'draw',
'It was a draw!',
0,
bot=bot,
stream='test',
subject='test game',
user_name='baz',
)
def test_normal_turns(self) -> None:
bot = self.setup_game()
self.verify_response('move 3', '**foo** moved in column 3\n\nfoo\n\nIt\'s **baz**\'s (:red_circle:) turn.',
0, bot=bot, stream='test', subject='test game')
self.verify_response('move 3', '**baz** moved in column 3\n\nfoo\n\nIt\'s **foo**\'s (:blue_circle:) turn.',
0, bot=bot, stream='test', subject='test game', user_name='baz')
self.verify_response(
'move 3',
'**foo** moved in column 3\n\nfoo\n\nIt\'s **baz**\'s (:red_circle:) turn.',
0,
bot=bot,
stream='test',
subject='test game',
)
self.verify_response(
'move 3',
'**baz** moved in column 3\n\nfoo\n\nIt\'s **foo**\'s (:blue_circle:) turn.',
0,
bot=bot,
stream='test',
subject='test game',
user_name='baz',
)
def test_wrong_turn(self) -> None:
bot = self.setup_game()
self.verify_response('move 5', 'It\'s **foo**\'s (:blue_circle:) turn.', 0,
bot=bot, stream='test', subject='test game', user_name='baz')
self.verify_response(
'move 5',
'It\'s **foo**\'s (:blue_circle:) turn.',
0,
bot=bot,
stream='test',
subject='test game',
user_name='baz',
)
def test_private_message_error(self) -> None:
self.verify_response(
'start game', 'If you are starting a game in private messages, you must invite players. Type `help` for commands.', 0, max_messages=1)
'start game',
'If you are starting a game in private messages, you must invite players. Type `help` for commands.',
0,
max_messages=1,
)
bot = self.add_user_to_cache('bar')
bot.invites = {
'abcdefg': {
'host': 'bar@example.com',
'stream': 'test',
'subject': 'test game'
}
'abcdefg': {'host': 'bar@example.com', 'stream': 'test', 'subject': 'test game'}
}
self.verify_response(
'join', 'You cannot join games in private messages. Type `help` for all commands.', 0, bot=bot, max_messages=1)
'join',
'You cannot join games in private messages. Type `help` for all commands.',
0,
bot=bot,
max_messages=1,
)
def test_game_already_in_subject(self) -> None:
bot = self.add_user_to_cache('foo')
bot.invites = {
'abcdefg': {
'host': 'foo@example.com',
'stream': 'test',
'subject': 'test game'
}
'abcdefg': {'host': 'foo@example.com', 'stream': 'test', 'subject': 'test game'}
}
self.verify_response('start game', 'There is already a game in this stream.', 0,
bot=bot, stream='test', subject='test game', user_name='baz', max_messages=1)
self.verify_response(
'start game',
'There is already a game in this stream.',
0,
bot=bot,
stream='test',
subject='test game',
user_name='baz',
max_messages=1,
)
# def test_not_authorized(self) -> None:
# bot = self.setup_game()
@ -355,32 +451,46 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
# user_name='bar', stream='test', subject='test game', max_messages=1)
def test_unknown_user(self) -> None:
self.verify_response('start game with @**bar**',
'I don\'t know @**bar**. Tell them to say @**test-bot** register', 0)
self.verify_response('start game with bar@example.com',
'I don\'t know bar@example.com. Tell them to use @**test-bot** register', 0)
self.verify_response(
'start game with @**bar**',
'I don\'t know @**bar**. Tell them to say @**test-bot** register',
0,
)
self.verify_response(
'start game with bar@example.com',
'I don\'t know bar@example.com. Tell them to use @**test-bot** register',
0,
)
def test_is_user_not_player(self) -> None:
bot = self.add_user_to_cache('foo')
self.add_user_to_cache('baz', bot)
bot.invites = {
'abcdefg': {
'host': 'foo@example.com',
'baz@example.com': 'a'
}
}
bot.invites = {'abcdefg': {'host': 'foo@example.com', 'baz@example.com': 'a'}}
self.assertFalse(bot.is_user_not_player('foo@example.com'))
self.assertFalse(bot.is_user_not_player('baz@example.com'))
def test_move_help_message(self) -> None:
bot = self.setup_game()
self.verify_response('move 123', '* To make your move during a game, type\n```move <column-number>```',
0, bot=bot, stream='test', subject='test game')
self.verify_response(
'move 123',
'* To make your move during a game, type\n```move <column-number>```',
0,
bot=bot,
stream='test',
subject='test game',
)
def test_invalid_move_message(self) -> None:
bot = self.setup_game()
self.verify_response('move 9', 'Invalid Move.', 0,
bot=bot, stream='test', subject='test game', max_messages=2)
self.verify_response(
'move 9',
'Invalid Move.',
0,
bot=bot,
stream='test',
subject='test game',
max_messages=2,
)
def test_get_game_id_by_email(self) -> None:
bot = self.setup_game()
@ -389,9 +499,13 @@ class TestGameHandlerBot(BotTestCase, DefaultTests):
def test_game_over_and_leaderboard(self) -> None:
bot = self.setup_game()
bot.put_user_cache()
with patch('zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over', return_value='foo@example.com'):
self.verify_response('move 3', '**foo** won! :tada:',
1, bot=bot, stream='test', subject='test game')
with patch(
'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over',
return_value='foo@example.com',
):
self.verify_response(
'move 3', '**foo** won! :tada:', 1, bot=bot, stream='test', subject='test game'
)
leaderboard = '**Most wins**\n\n\
Player | Games Won | Games Drawn | Games Lost | Total Games\n\
--- | --- | --- | --- | --- \n\
@ -402,27 +516,54 @@ Player | Games Won | Games Drawn | Games Lost | Total Games\n\
def test_current_turn_winner(self) -> None:
bot = self.setup_game()
with patch('zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over', return_value='current turn'):
self.verify_response('move 3', '**foo** won! :tada:',
1, bot=bot, stream='test', subject='test game')
with patch(
'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over',
return_value='current turn',
):
self.verify_response(
'move 3', '**foo** won! :tada:', 1, bot=bot, stream='test', subject='test game'
)
def test_computer_turn(self) -> None:
bot = self.setup_computer_game()
self.verify_response('move 3', '**foo** moved in column 3\n\nfoo\n\nIt\'s **test-bot**\'s (:red_circle:) turn.',
0, bot=bot, stream='test', subject='test game')
with patch('zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over', return_value='test-bot@example.com'):
self.verify_response('move 5', 'I won! Well Played!',
2, bot=bot, stream='test', subject='test game')
self.verify_response(
'move 3',
'**foo** moved in column 3\n\nfoo\n\nIt\'s **test-bot**\'s (:red_circle:) turn.',
0,
bot=bot,
stream='test',
subject='test game',
)
with patch(
'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over',
return_value='test-bot@example.com',
):
self.verify_response(
'move 5', 'I won! Well Played!', 2, bot=bot, stream='test', subject='test game'
)
def test_computer_endgame_responses(self) -> None:
bot = self.setup_computer_game()
with patch('zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over', return_value='foo@example.com'):
self.verify_response('move 5', 'You won! Nice!',
2, bot=bot, stream='test', subject='test game')
with patch(
'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over',
return_value='foo@example.com',
):
self.verify_response(
'move 5', 'You won! Nice!', 2, bot=bot, stream='test', subject='test game'
)
bot = self.setup_computer_game()
with patch('zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over', return_value='draw'):
self.verify_response('move 5', 'It was a draw! Well Played!',
2, bot=bot, stream='test', subject='test game')
with patch(
'zulip_bots.bots.game_handler_bot.game_handler_bot.MockModel.determine_game_over',
return_value='draw',
):
self.verify_response(
'move 5',
'It was a draw! Well Played!',
2,
bot=bot,
stream='test',
subject='test game',
)
def test_add_user_statistics(self) -> None:
bot = self.add_user_to_cache('foo')
@ -448,54 +589,82 @@ Player | Games Won | Games Drawn | Games Lost | Total Games\n\
'host': 'foo@example.com',
'baz@example.com': 'a',
'stream': 'test',
'subject': 'test game'
'subject': 'test game',
}
}
self.assertEqual(bot.get_game_info('abcdefg'), {
'game_id': 'abcdefg',
'type': 'invite',
'stream': 'test',
'subject': 'test game',
'players': ['foo@example.com', 'baz@example.com']
})
self.assertEqual(
bot.get_game_info('abcdefg'),
{
'game_id': 'abcdefg',
'type': 'invite',
'stream': 'test',
'subject': 'test game',
'players': ['foo@example.com', 'baz@example.com'],
},
)
def test_parse_message(self) -> None:
bot = self.setup_game()
self.verify_response('move 3', 'Join your game using the link below!\n\n> **Game `abc123`**\n\
self.verify_response(
'move 3',
'Join your game using the link below!\n\n> **Game `abc123`**\n\
> foo test game\n\
> 2/2 players\n\
> **[Join Game](/#narrow/stream/test/topic/test game)**', 0, bot=bot)
> **[Join Game](/#narrow/stream/test/topic/test game)**',
0,
bot=bot,
)
bot = self.setup_game()
self.verify_response('move 3', '''Your current game is not in this subject. \n\
self.verify_response(
'move 3',
'''Your current game is not in this subject. \n\
To move subjects, send your message again, otherwise join the game using the link below.
> **Game `abc123`**
> foo test game
> 2/2 players
> **[Join Game](/#narrow/stream/test/topic/test game)**''', 0, bot=bot, stream='test 2', subject='game 2')
self.verify_response('move 3', 'foo', 0, bot=bot,
stream='test 2', subject='game 2')
> **[Join Game](/#narrow/stream/test/topic/test game)**''',
0,
bot=bot,
stream='test 2',
subject='game 2',
)
self.verify_response('move 3', 'foo', 0, bot=bot, stream='test 2', subject='game 2')
def test_change_game_subject(self) -> None:
bot = self.setup_game('abc123')
self.setup_game('abcdefg', bot, ['bar', 'abc'], 'test game 2', 'test2')
self.verify_response('move 3', '''Your current game is not in this subject. \n\
self.verify_response(
'move 3',
'''Your current game is not in this subject. \n\
To move subjects, send your message again, otherwise join the game using the link below.
> **Game `abcdefg`**
> foo test game
> 2/2 players
> **[Join Game](/#narrow/stream/test2/topic/test game 2)**''', 0, bot=bot, user_name='bar', stream='test game', subject='test2')
self.verify_response('move 3', 'There is already a game in this subject.',
0, bot=bot, user_name='bar', stream='test game', subject='test')
> **[Join Game](/#narrow/stream/test2/topic/test game 2)**''',
0,
bot=bot,
user_name='bar',
stream='test game',
subject='test2',
)
self.verify_response(
'move 3',
'There is already a game in this subject.',
0,
bot=bot,
user_name='bar',
stream='test game',
subject='test',
)
bot.invites = {
'foo bar baz': {
'host': 'foo@example.com',
'baz@example.com': 'a',
'stream': 'test',
'subject': 'test game'
'subject': 'test game',
}
}
bot.change_game_subject('foo bar baz', 'test2',
'game2', self.make_request_message('foo'))
bot.change_game_subject('foo bar baz', 'test2', 'game2', self.make_request_message('foo'))
self.assertEqual(bot.invites['foo bar baz']['stream'], 'test2')

View file

@ -6,13 +6,9 @@ from zulip_bots.game_handler import BadMoveException, GameAdapter
class GameOfFifteenModel:
final_board = [[0, 1, 2],
[3, 4, 5],
[6, 7, 8]]
final_board = [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
initial_board = [[8, 7, 6],
[5, 4, 3],
[2, 1, 0]]
initial_board = [[8, 7, 6], [5, 4, 3], [2, 1, 0]]
def __init__(self, board: Any = None) -> None:
if board is not None:
@ -41,7 +37,7 @@ class GameOfFifteenModel:
def won(self, board: Any) -> bool:
for i in range(3):
for j in range(3):
if (board[i][j] != self.final_board[i][j]):
if board[i][j] != self.final_board[i][j]:
return False
return True
@ -67,23 +63,26 @@ class GameOfFifteenModel:
if tile not in coordinates:
raise BadMoveException('You can only move tiles which exist in the board.')
i, j = coordinates[tile]
if (j-1) > -1 and board[i][j-1] == 0:
board[i][j-1] = tile
if (j - 1) > -1 and board[i][j - 1] == 0:
board[i][j - 1] = tile
board[i][j] = 0
elif (i-1) > -1 and board[i-1][j] == 0:
board[i-1][j] = tile
elif (i - 1) > -1 and board[i - 1][j] == 0:
board[i - 1][j] = tile
board[i][j] = 0
elif (j+1) < 3 and board[i][j+1] == 0:
board[i][j+1] = tile
elif (j + 1) < 3 and board[i][j + 1] == 0:
board[i][j + 1] = tile
board[i][j] = 0
elif (i+1) < 3 and board[i+1][j] == 0:
board[i+1][j] = tile
elif (i + 1) < 3 and board[i + 1][j] == 0:
board[i + 1][j] = tile
board[i][j] = 0
else:
raise BadMoveException('You can only move tiles which are adjacent to :grey_question:.')
raise BadMoveException(
'You can only move tiles which are adjacent to :grey_question:.'
)
if m == moves - 1:
return board
class GameOfFifteenMessageHandler:
tiles = {
@ -113,8 +112,11 @@ class GameOfFifteenMessageHandler:
return original_player + ' moved ' + tile
def game_start_message(self) -> str:
return ("Welcome to Game of Fifteen!"
"To make a move, type @-mention `move <tile1> <tile2> ...`")
return (
"Welcome to Game of Fifteen!"
"To make a move, type @-mention `move <tile1> <tile2> ...`"
)
class GameOfFifteenBotHandler(GameAdapter):
'''
@ -125,7 +127,9 @@ class GameOfFifteenBotHandler(GameAdapter):
def __init__(self) -> None:
game_name = 'Game of Fifteen'
bot_name = 'Game of Fifteen'
move_help_message = '* To make your move during a game, type\n```move <tile1> <tile2> ...```'
move_help_message = (
'* To make your move during a game, type\n```move <tile1> <tile2> ...```'
)
move_regex = r'move [\d{1}\s]+$'
model = GameOfFifteenModel
gameMessageHandler = GameOfFifteenMessageHandler
@ -145,4 +149,5 @@ class GameOfFifteenBotHandler(GameAdapter):
max_players=1,
)
handler_class = GameOfFifteenBotHandler

View file

@ -9,20 +9,19 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests):
bot_name = 'game_of_fifteen'
def make_request_message(
self,
content: str,
user: str = 'foo@example.com',
user_name: str = 'foo'
self, content: str, user: str = 'foo@example.com', user_name: str = 'foo'
) -> Dict[str, str]:
message = dict(
sender_email=user,
content=content,
sender_full_name=user_name
)
message = dict(sender_email=user, content=content, sender_full_name=user_name)
return message
# Function that serves similar purpose to BotTestCase.verify_dialog, but allows for multiple responses to be handled
def verify_response(self, request: str, expected_response: str, response_number: int, user: str = 'foo@example.com') -> None:
def verify_response(
self,
request: str,
expected_response: str,
response_number: int,
user: str = 'foo@example.com',
) -> None:
'''
This function serves a similar purpose
to BotTestCase.verify_dialog, but allows
@ -36,11 +35,7 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests):
bot.handle_message(message, bot_handler)
responses = [
message
for (method, message)
in bot_handler.transcript
]
responses = [message for (method, message) in bot_handler.transcript]
first_response = responses[response_number]
self.assertEqual(expected_response, first_response['content'])
@ -63,23 +58,19 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests):
def test_game_message_handler_responses(self) -> None:
board = '\n\n:grey_question::one::two:\n\n:three::four::five:\n\n:six::seven::eight:'
bot, bot_handler = self._get_handlers()
self.assertEqual(bot.gameMessageHandler.parse_board(
self.winning_board), board)
self.assertEqual(bot.gameMessageHandler.alert_move_message(
'foo', 'move 1'), 'foo moved 1')
self.assertEqual(bot.gameMessageHandler.game_start_message(
), "Welcome to Game of Fifteen!"
"To make a move, type @-mention `move <tile1> <tile2> ...`")
self.assertEqual(bot.gameMessageHandler.parse_board(self.winning_board), board)
self.assertEqual(bot.gameMessageHandler.alert_move_message('foo', 'move 1'), 'foo moved 1')
self.assertEqual(
bot.gameMessageHandler.game_start_message(),
"Welcome to Game of Fifteen!"
"To make a move, type @-mention `move <tile1> <tile2> ...`",
)
winning_board = [[0, 1, 2],
[3, 4, 5],
[6, 7, 8]]
winning_board = [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
def test_game_of_fifteen_logic(self) -> None:
def confirmAvailableMoves(
good_moves: List[int],
bad_moves: List[int],
board: List[List[int]]
good_moves: List[int], bad_moves: List[int], board: List[List[int]]
) -> None:
gameOfFifteenModel.update_board(board)
for move in good_moves:
@ -92,18 +83,16 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests):
tile: str,
token_number: int,
initial_board: List[List[int]],
final_board: List[List[int]]
final_board: List[List[int]],
) -> None:
gameOfFifteenModel.update_board(initial_board)
test_board = gameOfFifteenModel.make_move(
'move ' + tile, token_number)
test_board = gameOfFifteenModel.make_move('move ' + tile, token_number)
self.assertEqual(test_board, final_board)
def confirmGameOver(board: List[List[int]], result: str) -> None:
gameOfFifteenModel.update_board(board)
game_over = gameOfFifteenModel.determine_game_over(
['first_player'])
game_over = gameOfFifteenModel.determine_game_over(['first_player'])
self.assertEqual(game_over, result)
@ -115,62 +104,43 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests):
gameOfFifteenModel = GameOfFifteenModel()
# Basic Board setups
initial_board = [[8, 7, 6],
[5, 4, 3],
[2, 1, 0]]
initial_board = [[8, 7, 6], [5, 4, 3], [2, 1, 0]]
sample_board = [[7, 6, 8],
[3, 0, 1],
[2, 4, 5]]
sample_board = [[7, 6, 8], [3, 0, 1], [2, 4, 5]]
winning_board = [[0, 1, 2],
[3, 4, 5],
[6, 7, 8]]
winning_board = [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
# Test Move Validation Logic
confirmAvailableMoves([1, 2, 3, 4, 5, 6, 7, 8], [0, 9, -1], initial_board)
# Test Move Logic
confirmMove('1', 0, initial_board,
[[8, 7, 6],
[5, 4, 3],
[2, 0, 1]])
confirmMove('1', 0, initial_board, [[8, 7, 6], [5, 4, 3], [2, 0, 1]])
confirmMove('1 2', 0, initial_board,
[[8, 7, 6],
[5, 4, 3],
[0, 2, 1]])
confirmMove('1 2', 0, initial_board, [[8, 7, 6], [5, 4, 3], [0, 2, 1]])
confirmMove('1 2 5', 0, initial_board,
[[8, 7, 6],
[0, 4, 3],
[5, 2, 1]])
confirmMove('1 2 5', 0, initial_board, [[8, 7, 6], [0, 4, 3], [5, 2, 1]])
confirmMove('1 2 5 4', 0, initial_board,
[[8, 7, 6],
[4, 0, 3],
[5, 2, 1]])
confirmMove('1 2 5 4', 0, initial_board, [[8, 7, 6], [4, 0, 3], [5, 2, 1]])
confirmMove('3', 0, sample_board,
[[7, 6, 8],
[0, 3, 1],
[2, 4, 5]])
confirmMove('3', 0, sample_board, [[7, 6, 8], [0, 3, 1], [2, 4, 5]])
confirmMove('3 7', 0, sample_board,
[[0, 6, 8],
[7, 3, 1],
[2, 4, 5]])
confirmMove('3 7', 0, sample_board, [[0, 6, 8], [7, 3, 1], [2, 4, 5]])
# Test coordinates logic:
confirm_coordinates(initial_board, {8: (0, 0),
7: (0, 1),
6: (0, 2),
5: (1, 0),
4: (1, 1),
3: (1, 2),
2: (2, 0),
1: (2, 1),
0: (2, 2)})
confirm_coordinates(
initial_board,
{
8: (0, 0),
7: (0, 1),
6: (0, 2),
5: (1, 0),
4: (1, 1),
3: (1, 2),
2: (2, 0),
1: (2, 1),
0: (2, 2),
},
)
# Test Game Over Logic:
confirmGameOver(winning_board, 'current turn')
@ -183,9 +153,7 @@ class TestGameOfFifteenBot(BotTestCase, DefaultTests):
move3 = 'move 23'
move4 = 'move 0'
move5 = 'move 1 2'
initial_board = [[8, 7, 6],
[5, 4, 3],
[2, 1, 0]]
initial_board = [[8, 7, 6], [5, 4, 3], [2, 1, 0]]
model.update_board(initial_board)
with self.assertRaises(BadMoveException):

View file

@ -19,6 +19,7 @@ class GiphyHandler:
and responds with a message with the GIF based on provided keywords.
It also responds to private messages.
"""
def usage(self) -> str:
return '''
This plugin allows users to post GIFs provided by Giphy.
@ -28,8 +29,7 @@ class GiphyHandler:
@staticmethod
def validate_config(config_info: Dict[str, str]) -> None:
query = {'s': 'Hello',
'api_key': config_info['key']}
query = {'s': 'Hello', 'api_key': config_info['key']}
try:
data = requests.get(GIPHY_TRANSLATE_API, params=query)
data.raise_for_status()
@ -38,19 +38,17 @@ class GiphyHandler:
except HTTPError as e:
error_message = str(e)
if data.status_code == 403:
error_message += ('This is likely due to an invalid key.\n'
'Follow the instructions in doc.md for setting an API key.')
error_message += (
'This is likely due to an invalid key.\n'
'Follow the instructions in doc.md for setting an API key.'
)
raise ConfigValidationError(error_message)
def initialize(self, bot_handler: BotHandler) -> None:
self.config_info = bot_handler.get_config_info('giphy')
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
bot_response = get_bot_giphy_response(
message,
bot_handler,
self.config_info
)
bot_response = get_bot_giphy_response(message, bot_handler, self.config_info)
bot_handler.send_reply(message, bot_response)
@ -83,21 +81,26 @@ def get_url_gif_giphy(keyword: str, api_key: str) -> Union[int, str]:
return gif_url
def get_bot_giphy_response(message: Dict[str, str], bot_handler: BotHandler, config_info: Dict[str, str]) -> str:
def get_bot_giphy_response(
message: Dict[str, str], bot_handler: BotHandler, config_info: Dict[str, str]
) -> str:
# Each exception has a specific reply should "gif_url" return a number.
# The bot will post the appropriate message for the error.
keyword = message['content']
try:
gif_url = get_url_gif_giphy(keyword, config_info['key'])
except requests.exceptions.ConnectionError:
return ('Uh oh, sorry :slightly_frowning_face:, I '
'cannot process your request right now. But, '
'let\'s try again later! :grin:')
return (
'Uh oh, sorry :slightly_frowning_face:, I '
'cannot process your request right now. But, '
'let\'s try again later! :grin:'
)
except GiphyNoResultException:
return ('Sorry, I don\'t have a GIF for "%s"! '
':astonished:' % (keyword,))
return ('[Click to enlarge](%s)'
'[](/static/images/interactive-bot/giphy/powered-by-giphy.png)'
% (gif_url,))
return 'Sorry, I don\'t have a GIF for "%s"! ' ':astonished:' % (keyword,)
return (
'[Click to enlarge](%s)'
'[](/static/images/interactive-bot/giphy/powered-by-giphy.png)' % (gif_url,)
)
handler_class = GiphyHandler

View file

@ -10,25 +10,28 @@ class TestGiphyBot(BotTestCase, DefaultTests):
# Test for bot response to empty message
def test_bot_responds_to_empty_message(self) -> None:
bot_response = '[Click to enlarge]' \
'(https://media0.giphy.com/media/ISumMYQyX4sSI/giphy.gif)' \
'[](/static/images/interactive-bot/giphy/powered-by-giphy.png)'
with self.mock_config_info({'key': '12345678'}), \
self.mock_http_conversation('test_random'):
bot_response = (
'[Click to enlarge]'
'(https://media0.giphy.com/media/ISumMYQyX4sSI/giphy.gif)'
'[](/static/images/interactive-bot/giphy/powered-by-giphy.png)'
)
with self.mock_config_info({'key': '12345678'}), self.mock_http_conversation('test_random'):
self.verify_reply('', bot_response)
def test_normal(self) -> None:
bot_response = '[Click to enlarge]' \
'(https://media4.giphy.com/media/3o6ZtpxSZbQRRnwCKQ/giphy.gif)' \
'[](/static/images/interactive-bot/giphy/powered-by-giphy.png)'
bot_response = (
'[Click to enlarge]'
'(https://media4.giphy.com/media/3o6ZtpxSZbQRRnwCKQ/giphy.gif)'
'[](/static/images/interactive-bot/giphy/powered-by-giphy.png)'
)
with self.mock_config_info({'key': '12345678'}), \
self.mock_http_conversation('test_normal'):
with self.mock_config_info({'key': '12345678'}), self.mock_http_conversation('test_normal'):
self.verify_reply('Hello', bot_response)
def test_no_result(self) -> None:
with self.mock_config_info({'key': '12345678'}), \
self.mock_http_conversation('test_no_result'):
with self.mock_config_info({'key': '12345678'}), self.mock_http_conversation(
'test_no_result'
):
self.verify_reply(
'world without zulip',
'Sorry, I don\'t have a GIF for "world without zulip"! :astonished:',
@ -38,8 +41,9 @@ class TestGiphyBot(BotTestCase, DefaultTests):
get_bot_message_handler(self.bot_name)
StubBotHandler()
with self.mock_http_conversation('test_403'):
self.validate_invalid_config({'key': '12345678'},
"This is likely due to an invalid key.\n")
self.validate_invalid_config(
{'key': '12345678'}, "This is likely due to an invalid key.\n"
)
def test_connection_error_when_validate_config(self) -> None:
error = ConnectionError()
@ -53,11 +57,12 @@ class TestGiphyBot(BotTestCase, DefaultTests):
self.validate_valid_config({'key': '12345678'})
def test_connection_error_while_running(self) -> None:
with self.mock_config_info({'key': '12345678'}), \
patch('requests.get', side_effect=[ConnectionError()]), \
patch('logging.exception'):
with self.mock_config_info({'key': '12345678'}), patch(
'requests.get', side_effect=[ConnectionError()]
), patch('logging.exception'):
self.verify_reply(
'world without chocolate',
'Uh oh, sorry :slightly_frowning_face:, I '
'cannot process your request right now. But, '
'let\'s try again later! :grin:')
'let\'s try again later! :grin:',
)

View file

@ -22,11 +22,13 @@ class GithubHandler:
self.repo = self.config_info.get("repo", False)
def usage(self) -> str:
return ("This plugin displays details on github issues and pull requests. "
"To reference an issue or pull request usename mention the bot then "
"anytime in the message type its id, for example:\n"
"@**Github detail** #3212 zulip#3212 zulip/zulip#3212\n"
"The default owner is {} and the default repo is {}.".format(self.owner, self.repo))
return (
"This plugin displays details on github issues and pull requests. "
"To reference an issue or pull request usename mention the bot then "
"anytime in the message type its id, for example:\n"
"@**Github detail** #3212 zulip#3212 zulip/zulip#3212\n"
"The default owner is {} and the default repo is {}.".format(self.owner, self.repo)
)
def format_message(self, details: Dict[str, Any]) -> str:
number = details['number']
@ -39,17 +41,24 @@ class GithubHandler:
description = details['body']
status = details['state'].title()
message_string = ('**[{owner}/{repo}#{id}]'.format(owner=owner, repo=repo, id=number),
'({link}) - {title}**\n'.format(title=title, link=link),
'Created by **[{author}](https://github.com/{author})**\n'.format(author=author),
'Status - **{status}**\n```quote\n{description}\n```'.format(status=status, description=description))
message_string = (
'**[{owner}/{repo}#{id}]'.format(owner=owner, repo=repo, id=number),
'({link}) - {title}**\n'.format(title=title, link=link),
'Created by **[{author}](https://github.com/{author})**\n'.format(author=author),
'Status - **{status}**\n```quote\n{description}\n```'.format(
status=status, description=description
),
)
return ''.join(message_string)
def get_details_from_github(self, owner: str, repo: str, number: str) -> Union[None, Dict[str, Union[str, int, bool]]]:
def get_details_from_github(
self, owner: str, repo: str, number: str
) -> Union[None, Dict[str, Union[str, int, bool]]]:
# Gets the details of an issues or pull request
try:
r = requests.get(
self.GITHUB_ISSUE_URL_TEMPLATE.format(owner=owner, repo=repo, id=number))
self.GITHUB_ISSUE_URL_TEMPLATE.format(owner=owner, repo=repo, id=number)
)
except requests.exceptions.RequestException as e:
logging.exception(str(e))
return None
@ -73,8 +82,7 @@ class GithubHandler:
return
# Capture owner, repo, id
issue_prs = list(re.finditer(
self.HANDLE_MESSAGE_REGEX, message['content']))
issue_prs = list(re.finditer(self.HANDLE_MESSAGE_REGEX, message['content']))
bot_messages = []
if len(issue_prs) > 5:
# We limit to 5 requests to prevent denial-of-service
@ -91,8 +99,11 @@ class GithubHandler:
details['repo'] = repo
bot_messages.append(self.format_message(details))
else:
bot_messages.append("Failed to find issue/pr: {owner}/{repo}#{id}"
.format(owner=owner, repo=repo, id=issue_pr.group(3)))
bot_messages.append(
"Failed to find issue/pr: {owner}/{repo}#{id}".format(
owner=owner, repo=repo, id=issue_pr.group(3)
)
)
else:
bot_messages.append("Failed to detect owner and repository name.")
if len(bot_messages) == 0:
@ -100,4 +111,5 @@ class GithubHandler:
bot_message = '\n'.join(bot_messages)
bot_handler.send_reply(message, bot_message)
handler_class = GithubHandler

View file

@ -23,15 +23,17 @@ class TestGithubDetailBot(BotTestCase, DefaultTests):
def test_issue(self) -> None:
request = 'zulip/zulip#5365'
bot_response = '**[zulip/zulip#5365](https://github.com/zulip/zulip/issues/5365)'\
' - frontend: Enable hot-reloading of CSS in development**\n'\
'Created by **[timabbott](https://github.com/timabbott)**\n'\
'Status - **Open**\n'\
'```quote\n'\
'There\'s strong interest among folks working on the frontend in being '\
'able to use the hot-reloading feature of webpack for managing our CSS.\r\n\r\n'\
'In order to do this, step 1 is to move our CSS minification pipeline '\
'from django-pipeline to Webpack. \n```'
bot_response = (
'**[zulip/zulip#5365](https://github.com/zulip/zulip/issues/5365)'
' - frontend: Enable hot-reloading of CSS in development**\n'
'Created by **[timabbott](https://github.com/timabbott)**\n'
'Status - **Open**\n'
'```quote\n'
'There\'s strong interest among folks working on the frontend in being '
'able to use the hot-reloading feature of webpack for managing our CSS.\r\n\r\n'
'In order to do this, step 1 is to move our CSS minification pipeline '
'from django-pipeline to Webpack. \n```'
)
with self.mock_http_conversation('test_issue'):
with self.mock_config_info(self.mock_config):
@ -39,18 +41,20 @@ class TestGithubDetailBot(BotTestCase, DefaultTests):
def test_pull_request(self) -> None:
request = 'zulip/zulip#5345'
bot_response = '**[zulip/zulip#5345](https://github.com/zulip/zulip/pull/5345)'\
' - [WIP] modal: Replace bootstrap modal with custom modal class**\n'\
'Created by **[jackrzhang](https://github.com/jackrzhang)**\n'\
'Status - **Open**\n```quote\nAn interaction bug (#4811) '\
'between our settings UI and the bootstrap modals breaks hotkey '\
'support for `Esc` when multiple modals are open.\r\n\r\ntodo:\r\n[x]'\
' Create `Modal` class in `modal.js` (drafted by @brockwhittaker)\r\n[x]'\
' Reimplement change_email_modal utilizing `Modal` class\r\n[] Dump '\
'using bootstrap for the account settings modal and all other modals,'\
' replace with `Modal` class\r\n[] Add hotkey support for closing the'\
' top modal for `Esc`\r\n\r\nThis should also be a helpful step in'\
' removing dependencies from Bootstrap.\n```'
bot_response = (
'**[zulip/zulip#5345](https://github.com/zulip/zulip/pull/5345)'
' - [WIP] modal: Replace bootstrap modal with custom modal class**\n'
'Created by **[jackrzhang](https://github.com/jackrzhang)**\n'
'Status - **Open**\n```quote\nAn interaction bug (#4811) '
'between our settings UI and the bootstrap modals breaks hotkey '
'support for `Esc` when multiple modals are open.\r\n\r\ntodo:\r\n[x]'
' Create `Modal` class in `modal.js` (drafted by @brockwhittaker)\r\n[x]'
' Reimplement change_email_modal utilizing `Modal` class\r\n[] Dump '
'using bootstrap for the account settings modal and all other modals,'
' replace with `Modal` class\r\n[] Add hotkey support for closing the'
' top modal for `Esc`\r\n\r\nThis should also be a helpful step in'
' removing dependencies from Bootstrap.\n```'
)
with self.mock_http_conversation('test_pull'):
with self.mock_config_info(self.mock_config):
self.verify_reply(request, bot_response)
@ -77,18 +81,22 @@ class TestGithubDetailBot(BotTestCase, DefaultTests):
def test_help_text(self) -> None:
request = 'help'
bot_response = 'This plugin displays details on github issues and pull requests. '\
'To reference an issue or pull request usename mention the bot then '\
'anytime in the message type its id, for example:\n@**Github detail** '\
'#3212 zulip#3212 zulip/zulip#3212\nThe default owner is zulip and '\
'the default repo is zulip.'
bot_response = (
'This plugin displays details on github issues and pull requests. '
'To reference an issue or pull request usename mention the bot then '
'anytime in the message type its id, for example:\n@**Github detail** '
'#3212 zulip#3212 zulip/zulip#3212\nThe default owner is zulip and '
'the default repo is zulip.'
)
with self.mock_config_info(self.mock_config):
self.verify_reply(request, bot_response)
def test_too_many_request(self) -> None:
request = 'zulip/zulip#1 zulip/zulip#1 zulip/zulip#1 zulip/zulip#1 '\
'zulip/zulip#1 zulip/zulip#1 zulip/zulip#1 zulip/zulip#1'
request = (
'zulip/zulip#1 zulip/zulip#1 zulip/zulip#1 zulip/zulip#1 '
'zulip/zulip#1 zulip/zulip#1 zulip/zulip#1 zulip/zulip#1'
)
bot_response = 'Please ask for <=5 links in any one request'
with self.mock_config_info(self.mock_config):
@ -102,36 +110,40 @@ class TestGithubDetailBot(BotTestCase, DefaultTests):
def test_owner_and_repo_specified_in_config_file(self) -> None:
request = '/#5345'
bot_response = '**[zulip/zulip#5345](https://github.com/zulip/zulip/pull/5345)'\
' - [WIP] modal: Replace bootstrap modal with custom modal class**\n'\
'Created by **[jackrzhang](https://github.com/jackrzhang)**\n'\
'Status - **Open**\n```quote\nAn interaction bug (#4811) '\
'between our settings UI and the bootstrap modals breaks hotkey '\
'support for `Esc` when multiple modals are open.\r\n\r\ntodo:\r\n[x]'\
' Create `Modal` class in `modal.js` (drafted by @brockwhittaker)\r\n[x]'\
' Reimplement change_email_modal utilizing `Modal` class\r\n[] Dump '\
'using bootstrap for the account settings modal and all other modals,'\
' replace with `Modal` class\r\n[] Add hotkey support for closing the'\
' top modal for `Esc`\r\n\r\nThis should also be a helpful step in'\
' removing dependencies from Bootstrap.\n```'
bot_response = (
'**[zulip/zulip#5345](https://github.com/zulip/zulip/pull/5345)'
' - [WIP] modal: Replace bootstrap modal with custom modal class**\n'
'Created by **[jackrzhang](https://github.com/jackrzhang)**\n'
'Status - **Open**\n```quote\nAn interaction bug (#4811) '
'between our settings UI and the bootstrap modals breaks hotkey '
'support for `Esc` when multiple modals are open.\r\n\r\ntodo:\r\n[x]'
' Create `Modal` class in `modal.js` (drafted by @brockwhittaker)\r\n[x]'
' Reimplement change_email_modal utilizing `Modal` class\r\n[] Dump '
'using bootstrap for the account settings modal and all other modals,'
' replace with `Modal` class\r\n[] Add hotkey support for closing the'
' top modal for `Esc`\r\n\r\nThis should also be a helpful step in'
' removing dependencies from Bootstrap.\n```'
)
with self.mock_http_conversation('test_pull'):
with self.mock_config_info(self.mock_config):
self.verify_reply(request, bot_response)
def test_owner_and_repo_specified_in_message(self) -> None:
request = 'zulip/zulip#5345'
bot_response = '**[zulip/zulip#5345](https://github.com/zulip/zulip/pull/5345)'\
' - [WIP] modal: Replace bootstrap modal with custom modal class**\n'\
'Created by **[jackrzhang](https://github.com/jackrzhang)**\n'\
'Status - **Open**\n```quote\nAn interaction bug (#4811) '\
'between our settings UI and the bootstrap modals breaks hotkey '\
'support for `Esc` when multiple modals are open.\r\n\r\ntodo:\r\n[x]'\
' Create `Modal` class in `modal.js` (drafted by @brockwhittaker)\r\n[x]'\
' Reimplement change_email_modal utilizing `Modal` class\r\n[] Dump '\
'using bootstrap for the account settings modal and all other modals,'\
' replace with `Modal` class\r\n[] Add hotkey support for closing the'\
' top modal for `Esc`\r\n\r\nThis should also be a helpful step in'\
' removing dependencies from Bootstrap.\n```'
bot_response = (
'**[zulip/zulip#5345](https://github.com/zulip/zulip/pull/5345)'
' - [WIP] modal: Replace bootstrap modal with custom modal class**\n'
'Created by **[jackrzhang](https://github.com/jackrzhang)**\n'
'Status - **Open**\n```quote\nAn interaction bug (#4811) '
'between our settings UI and the bootstrap modals breaks hotkey '
'support for `Esc` when multiple modals are open.\r\n\r\ntodo:\r\n[x]'
' Create `Modal` class in `modal.js` (drafted by @brockwhittaker)\r\n[x]'
' Reimplement change_email_modal utilizing `Modal` class\r\n[] Dump '
'using bootstrap for the account settings modal and all other modals,'
' replace with `Modal` class\r\n[] Add hotkey support for closing the'
' top modal for `Esc`\r\n\r\nThis should also be a helpful step in'
' removing dependencies from Bootstrap.\n```'
)
with self.mock_http_conversation('test_pull'):
with self.mock_config_info(self.empty_config):
self.verify_reply(request, bot_response)

View file

@ -32,11 +32,11 @@ def google_search(keywords: str) -> List[Dict[str, str]]:
if a.text.strip() == 'Cached' and 'webcache.googleusercontent.com' in a['href']:
continue
# a.text: The name of the page
result = {'url': "https://www.google.com{}".format(link),
'name': a.text}
result = {'url': "https://www.google.com{}".format(link), 'name': a.text}
results.append(result)
return results
def get_google_result(search_keywords: str) -> str:
help_message = "To use this bot, start messages with @mentioned-bot, \
followed by what you want to search for. If \
@ -56,13 +56,14 @@ def get_google_result(search_keywords: str) -> str:
else:
try:
results = google_search(search_keywords)
if (len(results) == 0):
if len(results) == 0:
return "Found no results."
return "Found Result: [{}]({})".format(results[0]['name'], results[0]['url'])
except Exception as e:
logging.exception(str(e))
return 'Error: Search failed. {}.'.format(e)
class GoogleSearchHandler:
'''
This plugin allows users to enter a search
@ -87,4 +88,5 @@ class GoogleSearchHandler:
result = get_google_result(original_content)
bot_handler.send_reply(message, result)
handler_class = GoogleSearchHandler

View file

@ -11,7 +11,7 @@ class TestGoogleSearchBot(BotTestCase, DefaultTests):
with self.mock_http_conversation('test_normal'):
self.verify_reply(
'zulip',
'Found Result: [Zulip](https://www.google.com/url?url=https%3A%2F%2Fzulip.com%2F)'
'Found Result: [Zulip](https://www.google.com/url?url=https%3A%2F%2Fzulip.com%2F)',
)
def test_bot_help(self) -> None:
@ -31,9 +31,10 @@ class TestGoogleSearchBot(BotTestCase, DefaultTests):
self.verify_reply('no res', 'Found no results.')
def test_attribute_error(self) -> None:
with self.mock_http_conversation('test_attribute_error'), \
patch('logging.exception'):
self.verify_reply('test', 'Error: Search failed. \'NoneType\' object has no attribute \'findAll\'.')
with self.mock_http_conversation('test_attribute_error'), patch('logging.exception'):
self.verify_reply(
'test', 'Error: Search failed. \'NoneType\' object has no attribute \'findAll\'.'
)
# Makes sure cached results, irrelevant links, or empty results are not displayed
def test_ignore_links(self) -> None:
@ -43,5 +44,5 @@ class TestGoogleSearchBot(BotTestCase, DefaultTests):
# See test_ignore_links.json
self.verify_reply(
'zulip',
'Found Result: [Zulip](https://www.google.com/url?url=https%3A%2F%2Fzulip.com%2F)'
'Found Result: [Zulip](https://www.google.com/url?url=https%3A%2F%2Fzulip.com%2F)',
)

View file

@ -10,6 +10,7 @@ class GoogleTranslateHandler:
Before using it, make sure you set up google api keys, and enable google
cloud translate from the google cloud console.
'''
def usage(self):
return '''
This plugin allows users translate messages
@ -27,12 +28,15 @@ class GoogleTranslateHandler:
bot_handler.quit(str(e))
def handle_message(self, message, bot_handler):
bot_response = get_translate_bot_response(message['content'],
self.config_info,
message['sender_full_name'],
self.supported_languages)
bot_response = get_translate_bot_response(
message['content'],
self.config_info,
message['sender_full_name'],
self.supported_languages,
)
bot_handler.send_reply(message, bot_response)
api_url = 'https://translation.googleapis.com/language/translate/v2'
help_text = '''
@ -44,17 +48,20 @@ Visit [here](https://cloud.google.com/translate/docs/languages) for all language
language_not_found_text = '{} language not found. Visit [here](https://cloud.google.com/translate/docs/languages) for all languages'
def get_supported_languages(key):
parameters = {'key': key, 'target': 'en'}
response = requests.get(api_url + '/languages', params = parameters)
response = requests.get(api_url + '/languages', params=parameters)
if response.status_code == requests.codes.ok:
languages = response.json()['data']['languages']
return {lang['name'].lower(): lang['language'].lower() for lang in languages}
raise TranslateError(response.json()['error']['message'])
class TranslateError(Exception):
pass
def translate(text_to_translate, key, dest, src):
parameters = {'q': text_to_translate, 'target': dest, 'key': key}
if src != '':
@ -64,6 +71,7 @@ def translate(text_to_translate, key, dest, src):
return response.json()['data']['translations'][0]['translatedText']
raise TranslateError(response.json()['error']['message'])
def get_code_for_language(language, all_languages):
if language.lower() not in all_languages.values():
if language.lower() not in all_languages.keys():
@ -71,6 +79,7 @@ def get_code_for_language(language, all_languages):
language = all_languages[language.lower()]
return language
def get_translate_bot_response(message_content, config_file, author, all_languages):
message_content = message_content.strip()
if message_content == 'help' or message_content is None or not message_content.startswith('"'):
@ -94,7 +103,9 @@ def get_translate_bot_response(message_content, config_file, author, all_languag
if source_language == '':
return language_not_found_text.format("Source")
try:
translated_text = translate(text_to_translate, config_file['key'], target_language, source_language)
translated_text = translate(
text_to_translate, config_file['key'], target_language, source_language
)
except requests.exceptions.ConnectionError as conn_err:
return "Could not connect to Google Translate. {}.".format(conn_err)
except TranslateError as tr_err:
@ -103,4 +114,5 @@ def get_translate_bot_response(message_content, config_file, author, all_languag
return "Error. {}.".format(err)
return "{} (from {})".format(translated_text, author)
handler_class = GoogleTranslateHandler

View file

@ -11,12 +11,14 @@ Please format your message like:
Visit [here](https://cloud.google.com/translate/docs/languages) for all languages
'''
class TestGoogleTranslateBot(BotTestCase, DefaultTests):
bot_name = "google_translate"
def _test(self, message, response, http_config_fixture, http_fixture=None):
with self.mock_config_info({'key': 'abcdefg'}), \
self.mock_http_conversation(http_config_fixture):
with self.mock_config_info({'key': 'abcdefg'}), self.mock_http_conversation(
http_config_fixture
):
if http_fixture:
with self.mock_http_conversation(http_fixture):
self.verify_reply(message, response)
@ -27,21 +29,32 @@ class TestGoogleTranslateBot(BotTestCase, DefaultTests):
self._test('"hello" de', 'Hallo (from Foo Test User)', 'test_languages', 'test_normal')
def test_source_language_not_found(self):
self._test('"hello" german foo',
('Source language not found. Visit [here]'
'(https://cloud.google.com/translate/docs/languages) for all languages'),
'test_languages')
self._test(
'"hello" german foo',
(
'Source language not found. Visit [here]'
'(https://cloud.google.com/translate/docs/languages) for all languages'
),
'test_languages',
)
def test_target_language_not_found(self):
self._test('"hello" bar english',
('Target language not found. Visit [here]'
'(https://cloud.google.com/translate/docs/languages) for all languages'),
'test_languages')
self._test(
'"hello" bar english',
(
'Target language not found. Visit [here]'
'(https://cloud.google.com/translate/docs/languages) for all languages'
),
'test_languages',
)
def test_403(self):
self._test('"hello" german english',
'Translate Error. Invalid API Key..',
'test_languages', 'test_403')
self._test(
'"hello" german english',
'Translate Error. Invalid API Key..',
'test_languages',
'test_403',
)
# Override default function in BotTestCase
def test_bot_responds_to_empty_message(self):
@ -57,13 +70,17 @@ class TestGoogleTranslateBot(BotTestCase, DefaultTests):
self._test('"hello"', help_text, 'test_languages')
def test_quotation_in_text(self):
self._test('"this has "quotation" marks in" english',
'this has "quotation" marks in (from Foo Test User)',
'test_languages', 'test_quotation')
self._test(
'"this has "quotation" marks in" english',
'this has "quotation" marks in (from Foo Test User)',
'test_languages',
'test_quotation',
)
def test_exception(self):
with patch('zulip_bots.bots.google_translate.google_translate.translate',
side_effect=Exception):
with patch(
'zulip_bots.bots.google_translate.google_translate.translate', side_effect=Exception
):
self._test('"hello" de', 'Error. .', 'test_languages')
def test_invalid_api_key(self):
@ -75,8 +92,5 @@ class TestGoogleTranslateBot(BotTestCase, DefaultTests):
self._test(None, None, 'test_api_access_not_configured')
def test_connection_error(self):
with patch('requests.post', side_effect=ConnectionError()), \
patch('logging.warning'):
self._test('"test" en',
'Could not connect to Google Translate. .',
'test_languages')
with patch('requests.post', side_effect=ConnectionError()), patch('logging.warning'):
self._test('"test" en', 'Could not connect to Google Translate. .', 'test_languages')

View file

@ -19,8 +19,9 @@ class HelloWorldHandler:
content = 'beep boop' # type: str
bot_handler.send_reply(message, content)
emoji_name = 'wave' # type: str
emoji_name = 'wave' # type: str
bot_handler.react(message, emoji_name)
return
handler_class = HelloWorldHandler

View file

@ -19,4 +19,5 @@ class HelpHandler:
help_content = "Info on Zulip can be found here:\nhttps://github.com/zulip/zulip"
bot_handler.send_reply(message, help_content)
handler_class = HelpHandler

View file

@ -8,9 +8,6 @@ class TestHelpBot(BotTestCase, DefaultTests):
help_text = "Info on Zulip can be found here:\nhttps://github.com/zulip/zulip"
requests = ["", "help", "Hi, my name is abc"]
dialog = [
(request, help_text)
for request in requests
]
dialog = [(request, help_text) for request in requests]
self.verify_dialog(dialog)

View file

@ -11,24 +11,31 @@ API_BASE_URL = "https://beta.idonethis.com/api/v2"
api_key = ""
default_team = ""
class AuthenticationException(Exception):
pass
class TeamNotFoundException(Exception):
def __init__(self, team: str) -> None:
self.team = team
class UnknownCommandSyntax(Exception):
def __init__(self, detail: str) -> None:
self.detail = detail
class UnspecifiedProblemException(Exception):
pass
def make_API_request(endpoint: str,
method: str = "GET",
body: Optional[Dict[str, str]] = None,
params: Optional[Dict[str, str]] = None) -> Any:
def make_API_request(
endpoint: str,
method: str = "GET",
body: Optional[Dict[str, str]] = None,
params: Optional[Dict[str, str]] = None,
) -> Any:
headers = {'Authorization': 'Token ' + api_key}
if method == "GET":
r = requests.get(API_BASE_URL + endpoint, headers=headers, params=params)
@ -36,50 +43,64 @@ def make_API_request(endpoint: str,
r = requests.post(API_BASE_URL + endpoint, headers=headers, params=params, json=body)
if r.status_code == 200:
return r.json()
elif r.status_code == 401 and 'error' in r.json() and r.json()['error'] == "Invalid API Authentication":
elif (
r.status_code == 401
and 'error' in r.json()
and r.json()['error'] == "Invalid API Authentication"
):
logging.error('Error authenticating, please check key ' + str(r.url))
raise AuthenticationException()
else:
logging.error('Error make API request, code ' + str(r.status_code) + '. json: ' + r.json())
raise UnspecifiedProblemException()
def api_noop() -> None:
make_API_request("/noop")
def api_list_team() -> List[Dict[str, str]]:
return make_API_request("/teams")
def api_show_team(hash_id: str) -> Dict[str, str]:
return make_API_request("/teams/{}".format(hash_id))
# NOTE: This function is not currently used
def api_show_users(hash_id: str) -> Any:
return make_API_request("/teams/{}/members".format(hash_id))
def api_list_entries(team_id: Optional[str] = None) -> List[Dict[str, Any]]:
if team_id:
return make_API_request("/entries", params=dict(team_id=team_id))
else:
return make_API_request("/entries")
def api_create_entry(body: str, team_id: str) -> Dict[str, Any]:
return make_API_request("/entries", "POST", {"body": body, "team_id": team_id})
def list_teams() -> str:
response = ["Teams:"] + [" * " + team['name'] for team in api_list_team()]
return "\n".join(response)
def get_team_hash(team_name: str) -> str:
for team in api_list_team():
if team['name'].lower() == team_name.lower() or team['hash_id'] == team_name:
return team['hash_id']
raise TeamNotFoundException(team_name)
def team_info(team_name: str) -> str:
data = api_show_team(get_team_hash(team_name))
return "\n".join(["Team Name: {name}",
"ID: `{hash_id}`",
"Created at: {created_at}"]).format(**data)
return "\n".join(["Team Name: {name}", "ID: `{hash_id}`", "Created at: {created_at}"]).format(
**data
)
def entries_list(team_name: str) -> str:
if team_name:
@ -90,18 +111,19 @@ def entries_list(team_name: str) -> str:
response = "Entries for all teams:"
for entry in data:
response += "\n".join(
["",
" * {body_formatted}",
" * Created at: {created_at}",
" * Status: {status}",
" * User: {username}",
" * Team: {teamname}",
" * ID: {hash_id}"
]).format(username=entry['user']['full_name'],
teamname=entry['team']['name'],
**entry)
[
"",
" * {body_formatted}",
" * Created at: {created_at}",
" * Status: {status}",
" * User: {username}",
" * Team: {teamname}",
" * ID: {hash_id}",
]
).format(username=entry['user']['full_name'], teamname=entry['team']['name'], **entry)
return response
def create_entry(message: str) -> str:
SINGLE_WORD_REGEX = re.compile("--team=([a-zA-Z0-9_]*)")
MULTIWORD_REGEX = re.compile('"--team=([^"]*)"')
@ -121,14 +143,17 @@ def create_entry(message: str) -> str:
team = default_team
new_message = message
else:
raise UnknownCommandSyntax("""I don't know which team you meant for me to create an entry under.
raise UnknownCommandSyntax(
"""I don't know which team you meant for me to create an entry under.
Either set a default team or pass the `--team` flag.
More information in my help""")
More information in my help"""
)
team_id = get_team_hash(team)
data = api_create_entry(new_message, team_id)
return "Great work :thumbs_up:. New entry `{}` created!".format(data['body_formatted'])
class IDoneThisHandler:
def initialize(self, bot_handler: BotHandler) -> None:
global api_key, default_team
@ -137,18 +162,24 @@ class IDoneThisHandler:
api_key = self.config_info['api_key']
else:
logging.error("An API key must be specified for this bot to run.")
logging.error("Have a look at the Setup section of my documenation for more information.")
logging.error(
"Have a look at the Setup section of my documenation for more information."
)
bot_handler.quit()
if 'default_team' in self.config_info:
default_team = self.config_info['default_team']
else:
logging.error("Cannot find default team. Users will need to manually specify a team each time an entry is created.")
logging.error(
"Cannot find default team. Users will need to manually specify a team each time an entry is created."
)
try:
api_noop()
except AuthenticationException:
logging.error("Authentication exception with idonethis. Can you check that your API keys are correct? ")
logging.error(
"Authentication exception with idonethis. Can you check that your API keys are correct? "
)
bot_handler.quit()
except UnspecifiedProblemException:
logging.error("Problem connecting to idonethis. Please check connection")
@ -160,7 +191,8 @@ class IDoneThisHandler:
default_team_message = "The default team is currently set as `" + default_team + "`."
else:
default_team_message = "There is currently no default team set up :frowning:."
return '''
return (
'''
This bot allows for interaction with idonethis, a collaboration tool to increase a team's productivity.
Below are some of the commands you can use, and what they do.
@ -179,7 +211,9 @@ Below are some of the commands you can use, and what they do.
Create a new entry. Optionally supply `--team=<team>` for teams with no spaces or `"--team=<team>"`
for teams with spaces. For example `@mention i did "--team=product team" something` will create a
new entry `something` for the product team.
''' + default_team_message
'''
+ default_team_message
)
def handle_message(self, message: Dict[str, Any], bot_handler: BotHandler) -> None:
bot_handler.send_reply(message, self.get_response(message))
@ -195,7 +229,9 @@ Below are some of the commands you can use, and what they do.
if len(message_content) > 2:
reply = team_info(" ".join(message_content[2:]))
else:
raise UnknownCommandSyntax("You must specify the team in which you request information from.")
raise UnknownCommandSyntax(
"You must specify the team in which you request information from."
)
elif command in ["entries list", "list entries"]:
reply = entries_list(" ".join(message_content[2:]))
elif command in ["entries create", "create entry", "new entry", "i did"]:
@ -205,15 +241,21 @@ Below are some of the commands you can use, and what they do.
else:
raise UnknownCommandSyntax("I can't understand the command you sent me :confused: ")
except TeamNotFoundException as e:
reply = "Sorry, it doesn't seem as if I can find a team named `" + e.team + "` :frowning:."
reply = (
"Sorry, it doesn't seem as if I can find a team named `" + e.team + "` :frowning:."
)
except AuthenticationException:
reply = "I can't currently authenticate with idonethis. "
reply += "Can you check that your API key is correct? For more information see my documentation."
except UnknownCommandSyntax as e:
reply = "Sorry, I don't understand what your trying to say. Use `@mention help` to see my help. " + e.detail
reply = (
"Sorry, I don't understand what your trying to say. Use `@mention help` to see my help. "
+ e.detail
)
except Exception as e: # catches UnspecifiedProblemException, and other problems
reply = "Oh dear, I'm having problems processing your request right now. Perhaps you could try again later :grinning:"
logging.error("Exception caught: " + str(e))
return reply
handler_class = IDoneThisHandler

View file

@ -7,86 +7,117 @@ class TestIDoneThisBot(BotTestCase, DefaultTests):
bot_name = "idonethis" # type: str
def test_create_entry_default_team(self) -> None:
with self.mock_config_info({'api_key': '12345678', 'default_team': 'testing team 1'}), \
self.mock_http_conversation('test_create_entry'), \
self.mock_http_conversation('team_list'):
self.verify_reply('i did something and something else',
'Great work :thumbs_up:. New entry `something and something else` created!')
with self.mock_config_info(
{'api_key': '12345678', 'default_team': 'testing team 1'}
), self.mock_http_conversation('test_create_entry'), self.mock_http_conversation(
'team_list'
):
self.verify_reply(
'i did something and something else',
'Great work :thumbs_up:. New entry `something and something else` created!',
)
def test_create_entry_quoted_team(self) -> None:
with self.mock_config_info({'api_key': '12345678', 'default_team': 'test_team_2'}), \
self.mock_http_conversation('test_create_entry'), \
self.mock_http_conversation('team_list'):
self.verify_reply('i did something and something else "--team=testing team 1"',
'Great work :thumbs_up:. New entry `something and something else` created!')
with self.mock_config_info(
{'api_key': '12345678', 'default_team': 'test_team_2'}
), self.mock_http_conversation('test_create_entry'), self.mock_http_conversation(
'team_list'
):
self.verify_reply(
'i did something and something else "--team=testing team 1"',
'Great work :thumbs_up:. New entry `something and something else` created!',
)
def test_create_entry_single_word_team(self) -> None:
with self.mock_config_info({'api_key': '12345678', 'default_team': 'testing team 1'}), \
self.mock_http_conversation('test_create_entry_team_2'), \
self.mock_http_conversation('team_list'):
self.verify_reply('i did something and something else --team=test_team_2',
'Great work :thumbs_up:. New entry `something and something else` created!')
with self.mock_config_info(
{'api_key': '12345678', 'default_team': 'testing team 1'}
), self.mock_http_conversation('test_create_entry_team_2'), self.mock_http_conversation(
'team_list'
):
self.verify_reply(
'i did something and something else --team=test_team_2',
'Great work :thumbs_up:. New entry `something and something else` created!',
)
def test_bad_key(self) -> None:
with self.mock_config_info({'api_key': '87654321', 'default_team': 'testing team 1'}), \
self.mock_http_conversation('test_401'), \
patch('zulip_bots.bots.idonethis.idonethis.api_noop'), \
patch('logging.error'):
self.verify_reply('list teams',
'I can\'t currently authenticate with idonethis. Can you check that your API key is correct? '
'For more information see my documentation.')
with self.mock_config_info(
{'api_key': '87654321', 'default_team': 'testing team 1'}
), self.mock_http_conversation('test_401'), patch(
'zulip_bots.bots.idonethis.idonethis.api_noop'
), patch(
'logging.error'
):
self.verify_reply(
'list teams',
'I can\'t currently authenticate with idonethis. Can you check that your API key is correct? '
'For more information see my documentation.',
)
def test_list_team(self) -> None:
with self.mock_config_info({'api_key': '12345678', 'default_team': 'testing team 1'}), \
self.mock_http_conversation('team_list'):
self.verify_reply('list teams',
'Teams:\n * testing team 1\n * test_team_2')
with self.mock_config_info(
{'api_key': '12345678', 'default_team': 'testing team 1'}
), self.mock_http_conversation('team_list'):
self.verify_reply('list teams', 'Teams:\n * testing team 1\n * test_team_2')
def test_show_team_no_team(self) -> None:
with self.mock_config_info({'api_key': '12345678', 'default_team': 'testing team 1'}), \
self.mock_http_conversation('api_noop'):
self.verify_reply('team info',
'Sorry, I don\'t understand what your trying to say. Use `@mention help` to see my help. '
'You must specify the team in which you request information from.')
with self.mock_config_info(
{'api_key': '12345678', 'default_team': 'testing team 1'}
), self.mock_http_conversation('api_noop'):
self.verify_reply(
'team info',
'Sorry, I don\'t understand what your trying to say. Use `@mention help` to see my help. '
'You must specify the team in which you request information from.',
)
def test_show_team(self) -> None:
with self.mock_config_info({'api_key': '12345678', 'default_team': 'testing team 1'}), \
self.mock_http_conversation('test_show_team'), \
patch('zulip_bots.bots.idonethis.idonethis.get_team_hash', return_value='31415926535') as get_team_hashFunction:
self.verify_reply('team info testing team 1',
'Team Name: testing team 1\n'
'ID: `31415926535`\n'
'Created at: 2017-12-28T19:12:55.121+11:00')
with self.mock_config_info(
{'api_key': '12345678', 'default_team': 'testing team 1'}
), self.mock_http_conversation('test_show_team'), patch(
'zulip_bots.bots.idonethis.idonethis.get_team_hash', return_value='31415926535'
) as get_team_hashFunction:
self.verify_reply(
'team info testing team 1',
'Team Name: testing team 1\n'
'ID: `31415926535`\n'
'Created at: 2017-12-28T19:12:55.121+11:00',
)
get_team_hashFunction.assert_called_with('testing team 1')
def test_entries_list(self) -> None:
with self.mock_config_info({'api_key': '12345678', 'default_team': 'testing team 1'}), \
self.mock_http_conversation('test_entries_list'), \
patch('zulip_bots.bots.idonethis.idonethis.get_team_hash', return_value='31415926535'):
self.verify_reply('entries list testing team 1',
'Entries for testing team 1:\n'
' * TESTING\n'
' * Created at: 2018-01-04T21:10:13.084+11:00\n'
' * Status: done\n'
' * User: John Doe\n'
' * Team: testing team 1\n'
' * ID: 65e1b21fd8f63adede1daae0bdf28c0e47b84923\n'
' * Grabbing some more data...\n'
' * Created at: 2018-01-04T20:07:58.078+11:00\n'
' * Status: done\n'
' * User: John Doe\n'
' * Team: testing team 1\n'
' * ID: fa974ad8c1acb9e81361a051a697f9dae22908d6\n'
' * GRABBING HTTP DATA\n'
' * Created at: 2018-01-04T19:07:17.214+11:00\n'
' * Status: done\n'
' * User: John Doe\n'
' * Team: testing team 1\n'
' * ID: 72c8241d2218464433268c5abd6625ac104e3d8f')
with self.mock_config_info(
{'api_key': '12345678', 'default_team': 'testing team 1'}
), self.mock_http_conversation('test_entries_list'), patch(
'zulip_bots.bots.idonethis.idonethis.get_team_hash', return_value='31415926535'
):
self.verify_reply(
'entries list testing team 1',
'Entries for testing team 1:\n'
' * TESTING\n'
' * Created at: 2018-01-04T21:10:13.084+11:00\n'
' * Status: done\n'
' * User: John Doe\n'
' * Team: testing team 1\n'
' * ID: 65e1b21fd8f63adede1daae0bdf28c0e47b84923\n'
' * Grabbing some more data...\n'
' * Created at: 2018-01-04T20:07:58.078+11:00\n'
' * Status: done\n'
' * User: John Doe\n'
' * Team: testing team 1\n'
' * ID: fa974ad8c1acb9e81361a051a697f9dae22908d6\n'
' * GRABBING HTTP DATA\n'
' * Created at: 2018-01-04T19:07:17.214+11:00\n'
' * Status: done\n'
' * User: John Doe\n'
' * Team: testing team 1\n'
' * ID: 72c8241d2218464433268c5abd6625ac104e3d8f',
)
def test_bot_responds_to_empty_message(self) -> None:
with self.mock_config_info({'api_key': '12345678', 'bot_info': 'team'}), \
self.mock_http_conversation('api_noop'):
self.verify_reply('',
'Sorry, I don\'t understand what your trying to say. Use `@mention help` to see my help. '
'I can\'t understand the command you sent me :confused: ')
with self.mock_config_info(
{'api_key': '12345678', 'bot_info': 'team'}
), self.mock_http_conversation('api_noop'):
self.verify_reply(
'',
'Sorry, I don\'t understand what your trying to say. Use `@mention help` to see my help. '
'I can\'t understand the command you sent me :confused: ',
)

View file

@ -13,9 +13,11 @@ ANSWERS = {
'4': 'escalate',
}
class InvalidAnswerException(Exception):
pass
class IncidentHandler:
def usage(self) -> str:
return '''
@ -43,12 +45,13 @@ class IncidentHandler:
bot_response = 'type "new <description>" for a new incident'
bot_handler.send_reply(message, bot_response)
def start_new_incident(query: str, message: Dict[str, Any], bot_handler: BotHandler) -> None:
# Here is where we would enter the incident in some sort of backend
# system. We just simulate everything by having an incident id that
# we generate here.
incident = query[len('new '):]
incident = query[len('new ') :]
ticket_id = generate_ticket_id(bot_handler.storage)
bot_response = format_incident_for_markdown(ticket_id, incident)
@ -56,6 +59,7 @@ def start_new_incident(query: str, message: Dict[str, Any], bot_handler: BotHand
bot_handler.send_reply(message, bot_response, widget_content)
def parse_answer(query: str) -> Tuple[str, str]:
m = re.match(r'answer\s+(TICKET....)\s+(.)', query)
if not m:
@ -74,6 +78,7 @@ def parse_answer(query: str) -> Tuple[str, str]:
return (ticket_id, ANSWERS[answer])
def generate_ticket_id(storage: Any) -> str:
try:
incident_num = storage.get('ticket_id')
@ -85,6 +90,7 @@ def generate_ticket_id(storage: Any) -> str:
ticket_id = 'TICKET%04d' % (incident_num,)
return ticket_id
def format_incident_for_widget(ticket_id: str, incident: Dict[str, Any]) -> str:
widget_type = 'zform'
@ -116,14 +122,17 @@ def format_incident_for_widget(ticket_id: str, incident: Dict[str, Any]) -> str:
payload = json.dumps(widget_content)
return payload
def format_incident_for_markdown(ticket_id: str, incident: Dict[str, Any]) -> str:
answer_list = '\n'.join([
'* **{code}** {answer}'.format(
code=code,
answer=ANSWERS[code],
)
for code in '1234'
])
answer_list = '\n'.join(
[
'* **{code}** {answer}'.format(
code=code,
answer=ANSWERS[code],
)
for code in '1234'
]
)
how_to_respond = '''**reply**: answer {ticket_id} <code>'''.format(ticket_id=ticket_id)
content = '''
@ -139,4 +148,5 @@ Q: {question}
)
return content
handler_class = IncidentHandler

View file

@ -35,10 +35,9 @@ class IncrementorHandler:
storage.put('number', num)
if storage.get('message_id') is not None:
result = bot_handler.update_message(dict(
message_id=storage.get('message_id'),
content=str(num)
))
result = bot_handler.update_message(
dict(message_id=storage.get('message_id'), content=str(num))
)
# When there isn't an error while updating the message, we won't
# attempt to send the it again.

View file

@ -20,10 +20,7 @@ class TestIncrementorBot(BotTestCase, DefaultTests):
bot.handle_message(message, bot_handler)
bot.handle_message(message, bot_handler)
content_updates = [
item[0][0]['content']
for item in m.call_args_list
]
content_updates = [item[0][0]['content'] for item in m.call_args_list]
self.assertEqual(content_updates, ['2', '3', '4'])
def test_bot_edit_timeout(self) -> None:
@ -44,8 +41,5 @@ class TestIncrementorBot(BotTestCase, DefaultTests):
# When there is an error, the bot should resend the message with the new value.
self.assertEqual(m.call_count, 2)
content_updates = [
item[0][0]['content']
for item in m.call_args_list
]
content_updates = [item[0][0]['content'] for item in m.call_args_list]
self.assertEqual(content_updates, ['2', '3'])

View file

@ -145,6 +145,7 @@ Jira Bot:
> Issue *BOTS-16* was edited! https://example.atlassian.net/browse/BOTS-16
'''
class JiraHandler:
def usage(self) -> str:
return '''
@ -181,7 +182,8 @@ class JiraHandler:
def jql_search(self, jql_query: str) -> str:
UNKNOWN_VAL = '*unknown*'
jira_response = requests.get(
self.domain_with_protocol + '/rest/api/2/search?jql={}&fields=key,summary,status'.format(jql_query),
self.domain_with_protocol
+ '/rest/api/2/search?jql={}&fields=key,summary,status'.format(jql_query),
headers={'Authorization': self.auth},
).json()
@ -197,7 +199,9 @@ class JiraHandler:
fields = issue.get('fields', {})
summary = fields.get('summary', UNKNOWN_VAL)
status_name = fields.get('status', {}).get('name', UNKNOWN_VAL)
response += "\n - {}: [{}]({}) **[{}]**".format(issue['key'], summary, url + issue['key'], status_name)
response += "\n - {}: [{}]({}) **[{}]**".format(
issue['key'], summary, url + issue['key'], status_name
)
return response
@ -246,20 +250,31 @@ class JiraHandler:
' - Project: *{}*\n'
' - Priority: *{}*\n'
' - Status: *{}*\n'
).format(key, url, summary, type_name, description, creator_name, project_name,
priority_name, status_name)
).format(
key,
url,
summary,
type_name,
description,
creator_name,
project_name,
priority_name,
status_name,
)
elif create_match:
jira_response = requests.post(
self.domain_with_protocol + '/rest/api/2/issue',
headers={'Authorization': self.auth},
json=make_create_json(create_match.group('summary'),
create_match.group('project_key'),
create_match.group('type_name'),
create_match.group('description'),
create_match.group('assignee'),
create_match.group('priority_name'),
create_match.group('labels'),
create_match.group('due_date'))
json=make_create_json(
create_match.group('summary'),
create_match.group('project_key'),
create_match.group('type_name'),
create_match.group('description'),
create_match.group('assignee'),
create_match.group('priority_name'),
create_match.group('labels'),
create_match.group('due_date'),
),
)
jira_response_json = jira_response.json() if jira_response.text else {}
@ -277,14 +292,16 @@ class JiraHandler:
jira_response = requests.put(
self.domain_with_protocol + '/rest/api/2/issue/' + key,
headers={'Authorization': self.auth},
json=make_edit_json(edit_match.group('summary'),
edit_match.group('project_key'),
edit_match.group('type_name'),
edit_match.group('description'),
edit_match.group('assignee'),
edit_match.group('priority_name'),
edit_match.group('labels'),
edit_match.group('due_date'))
json=make_edit_json(
edit_match.group('summary'),
edit_match.group('project_key'),
edit_match.group('type_name'),
edit_match.group('description'),
edit_match.group('assignee'),
edit_match.group('priority_name'),
edit_match.group('labels'),
edit_match.group('due_date'),
),
)
jira_response_json = jira_response.json() if jira_response.text else {}
@ -310,6 +327,7 @@ class JiraHandler:
bot_handler.send_reply(message, response)
def make_jira_auth(username: str, password: str) -> str:
'''Makes an auth header for Jira in the form 'Basic: <encoded credentials>'.
@ -321,10 +339,17 @@ def make_jira_auth(username: str, password: str) -> str:
encoded = base64.b64encode(combo.encode('utf-8')).decode('utf-8')
return 'Basic ' + encoded
def make_create_json(summary: str, project_key: str, type_name: str,
description: Optional[str], assignee: Optional[str],
priority_name: Optional[str], labels: Optional[str],
due_date: Optional[str]) -> Any:
def make_create_json(
summary: str,
project_key: str,
type_name: str,
description: Optional[str],
assignee: Optional[str],
priority_name: Optional[str],
labels: Optional[str],
due_date: Optional[str],
) -> Any:
'''Makes a JSON string for the Jira REST API editing endpoint based on
fields that could be edited.
@ -341,12 +366,8 @@ def make_create_json(summary: str, project_key: str, type_name: str,
'''
json_fields = {
'summary': summary,
'project': {
'key': project_key
},
'issuetype': {
'name': type_name
}
'project': {'key': project_key},
'issuetype': {'name': type_name},
}
if description:
json_fields['description'] = description
@ -363,10 +384,17 @@ def make_create_json(summary: str, project_key: str, type_name: str,
return json
def make_edit_json(summary: Optional[str], project_key: Optional[str],
type_name: Optional[str], description: Optional[str],
assignee: Optional[str], priority_name: Optional[str],
labels: Optional[str], due_date: Optional[str]) -> Any:
def make_edit_json(
summary: Optional[str],
project_key: Optional[str],
type_name: Optional[str],
description: Optional[str],
assignee: Optional[str],
priority_name: Optional[str],
labels: Optional[str],
due_date: Optional[str],
) -> Any:
'''Makes a JSON string for the Jira REST API editing endpoint based on
fields that could be edited.
@ -404,6 +432,7 @@ def make_edit_json(summary: Optional[str], project_key: Optional[str],
return json
def check_is_editing_something(match: Any) -> bool:
'''Checks if an editing match is actually going to do editing. It is
possible for an edit regex to match without doing any editing because each
@ -424,4 +453,5 @@ def check_is_editing_something(match: Any) -> bool:
or match.group('due_date')
)
handler_class = JiraHandler

View file

@ -7,20 +7,20 @@ class TestJiraBot(BotTestCase, DefaultTests):
MOCK_CONFIG_INFO = {
'username': 'example@example.com',
'password': 'qwerty!123',
'domain': 'example.atlassian.net'
'domain': 'example.atlassian.net',
}
MOCK_SCHEME_CONFIG_INFO = {
'username': 'example@example.com',
'password': 'qwerty!123',
'domain': 'http://example.atlassian.net'
'domain': 'http://example.atlassian.net',
}
MOCK_DISPLAY_CONFIG_INFO = {
'username': 'example@example.com',
'password': 'qwerty!123',
'domain': 'example.atlassian.net',
'display_url': 'http://test.com'
'display_url': 'http://test.com',
}
MOCK_GET_RESPONSE = '''\
@ -158,8 +158,7 @@ Jira Bot:
MOCK_JQL_RESPONSE = '**Search results for "summary ~ TEST"**\n\n*Found 2 results*\n\n\n - TEST-1: [summary test 1](https://example.atlassian.net/browse/TEST-1) **[To Do]**\n - TEST-2: [summary test 2](https://example.atlassian.net/browse/TEST-2) **[To Do]**'
def _test_invalid_config(self, invalid_config, error_message) -> None:
with self.mock_config_info(invalid_config), \
self.assertRaisesRegex(KeyError, error_message):
with self.mock_config_info(invalid_config), self.assertRaisesRegex(KeyError, error_message):
bot, bot_handler = self._get_handlers()
def test_config_without_username(self) -> None:
@ -167,83 +166,92 @@ Jira Bot:
'password': 'qwerty!123',
'domain': 'example.atlassian.net',
}
self._test_invalid_config(config_without_username,
'No `username` was specified')
self._test_invalid_config(config_without_username, 'No `username` was specified')
def test_config_without_password(self) -> None:
config_without_password = {
'username': 'example@example.com',
'domain': 'example.atlassian.net',
}
self._test_invalid_config(config_without_password,
'No `password` was specified')
self._test_invalid_config(config_without_password, 'No `password` was specified')
def test_config_without_domain(self) -> None:
config_without_domain = {
'username': 'example@example.com',
'password': 'qwerty!123',
}
self._test_invalid_config(config_without_domain,
'No `domain` was specified')
self._test_invalid_config(config_without_domain, 'No `domain` was specified')
def test_get(self) -> None:
with self.mock_config_info(self.MOCK_CONFIG_INFO), \
self.mock_http_conversation('test_get'):
with self.mock_config_info(self.MOCK_CONFIG_INFO), self.mock_http_conversation('test_get'):
self.verify_reply('get "TEST-13"', self.MOCK_GET_RESPONSE)
def test_get_error(self) -> None:
with self.mock_config_info(self.MOCK_CONFIG_INFO), \
self.mock_http_conversation('test_get_error'):
self.verify_reply('get "TEST-13"',
'Oh no! Jira raised an error:\n > error1')
with self.mock_config_info(self.MOCK_CONFIG_INFO), self.mock_http_conversation(
'test_get_error'
):
self.verify_reply('get "TEST-13"', 'Oh no! Jira raised an error:\n > error1')
def test_create(self) -> None:
with self.mock_config_info(self.MOCK_CONFIG_INFO), \
self.mock_http_conversation('test_create'):
self.verify_reply('create issue "Testing" in project "TEST" with type "Task"',
self.MOCK_CREATE_RESPONSE)
with self.mock_config_info(self.MOCK_CONFIG_INFO), self.mock_http_conversation(
'test_create'
):
self.verify_reply(
'create issue "Testing" in project "TEST" with type "Task"',
self.MOCK_CREATE_RESPONSE,
)
def test_create_error(self) -> None:
with self.mock_config_info(self.MOCK_CONFIG_INFO), \
self.mock_http_conversation('test_create_error'):
self.verify_reply('create issue "Testing" in project "TEST" with type "Task" '
'with description "This is a test description" assigned to "testuser" '
'with priority "Medium" labeled "issues, testing" due "2018-06-11"',
'Oh no! Jira raised an error:\n > error1')
with self.mock_config_info(self.MOCK_CONFIG_INFO), self.mock_http_conversation(
'test_create_error'
):
self.verify_reply(
'create issue "Testing" in project "TEST" with type "Task" '
'with description "This is a test description" assigned to "testuser" '
'with priority "Medium" labeled "issues, testing" due "2018-06-11"',
'Oh no! Jira raised an error:\n > error1',
)
def test_edit(self) -> None:
with self.mock_config_info(self.MOCK_CONFIG_INFO), \
self.mock_http_conversation('test_edit'):
self.verify_reply('edit issue "TEST-16" to use description "description"',
self.MOCK_EDIT_RESPONSE)
with self.mock_config_info(self.MOCK_CONFIG_INFO), self.mock_http_conversation('test_edit'):
self.verify_reply(
'edit issue "TEST-16" to use description "description"', self.MOCK_EDIT_RESPONSE
)
def test_edit_error(self) -> None:
with self.mock_config_info(self.MOCK_CONFIG_INFO), \
self.mock_http_conversation('test_edit_error'):
self.verify_reply('edit issue "TEST-13" to use summary "Change the summary" '
'to use project "TEST" to use type "Bug" to use description "This is a test description" '
'by assigning to "testuser" to use priority "Low" by labeling "issues, testing" '
'by making due "2018-06-11"',
'Oh no! Jira raised an error:\n > error1')
with self.mock_config_info(self.MOCK_CONFIG_INFO), self.mock_http_conversation(
'test_edit_error'
):
self.verify_reply(
'edit issue "TEST-13" to use summary "Change the summary" '
'to use project "TEST" to use type "Bug" to use description "This is a test description" '
'by assigning to "testuser" to use priority "Low" by labeling "issues, testing" '
'by making due "2018-06-11"',
'Oh no! Jira raised an error:\n > error1',
)
def test_search(self) -> None:
with self.mock_config_info(self.MOCK_CONFIG_INFO), \
self.mock_http_conversation('test_search'):
with self.mock_config_info(self.MOCK_CONFIG_INFO), self.mock_http_conversation(
'test_search'
):
self.verify_reply('search "TEST"', self.MOCK_SEARCH_RESPONSE)
def test_jql(self) -> None:
with self.mock_config_info(self.MOCK_CONFIG_INFO), \
self.mock_http_conversation('test_search'):
with self.mock_config_info(self.MOCK_CONFIG_INFO), self.mock_http_conversation(
'test_search'
):
self.verify_reply('jql "summary ~ TEST"', self.MOCK_JQL_RESPONSE)
def test_search_url(self) -> None:
with self.mock_config_info(self.MOCK_DISPLAY_CONFIG_INFO), \
self.mock_http_conversation('test_search'):
with self.mock_config_info(self.MOCK_DISPLAY_CONFIG_INFO), self.mock_http_conversation(
'test_search'
):
self.verify_reply('search "TEST"', self.MOCK_SEARCH_RESPONSE_URL)
def test_search_scheme(self) -> None:
with self.mock_config_info(self.MOCK_SCHEME_CONFIG_INFO), \
self.mock_http_conversation('test_search_scheme'):
with self.mock_config_info(self.MOCK_SCHEME_CONFIG_INFO), self.mock_http_conversation(
'test_search_scheme'
):
self.verify_reply('search "TEST"', self.MOCK_SEARCH_RESPONSE_SCHEME)
def test_help(self) -> None:

View file

@ -15,7 +15,8 @@ class LinkShortenerHandler:
return (
'Mention the link shortener bot in a conversation and then enter '
'any URLs you want to shorten in the body of the message. \n\n'
'`key` must be set in `link_shortener.conf`.')
'`key` must be set in `link_shortener.conf`.'
)
def initialize(self, bot_handler: BotHandler) -> None:
self.config_info = bot_handler.get_config_info('link_shortener')
@ -25,20 +26,25 @@ class LinkShortenerHandler:
test_request_data = self.call_link_shorten_service('www.youtube.com/watch') # type: Any
try:
if self.is_invalid_token_error(test_request_data):
bot_handler.quit('Invalid key. Follow the instructions in doc.md for setting API key.')
bot_handler.quit(
'Invalid key. Follow the instructions in doc.md for setting API key.'
)
except KeyError:
pass
def is_invalid_token_error(self, response_json: Any) -> bool:
return response_json['status_code'] == 500 and response_json['status_txt'] == 'INVALID_ARG_ACCESS_TOKEN'
return (
response_json['status_code'] == 500
and response_json['status_txt'] == 'INVALID_ARG_ACCESS_TOKEN'
)
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
REGEX_STR = (
r'('
r'(?:http|https):\/\/' # This allows for the HTTP or HTTPS
# protocol.
# protocol.
r'[^"<>\{\}|\^~[\]` ]+' # This allows for any character except
# for certain non-URL-safe ones.
# for certain non-URL-safe ones.
r')'
)
@ -51,10 +57,7 @@ class LinkShortenerHandler:
content = message['content']
if content.strip() == 'help':
bot_handler.send_reply(
message,
HELP_STR
)
bot_handler.send_reply(message, HELP_STR)
return
link_matches = re.findall(REGEX_STR, content)
@ -62,17 +65,13 @@ class LinkShortenerHandler:
shortened_links = [self.shorten_link(link) for link in link_matches]
link_pairs = [
(link_match + ': ' + shortened_link)
for link_match, shortened_link
in zip(link_matches, shortened_links)
for link_match, shortened_link in zip(link_matches, shortened_links)
if shortened_link != ''
]
final_response = '\n'.join(link_pairs)
if final_response == '':
bot_handler.send_reply(
message,
'No links found. ' + HELP_STR
)
bot_handler.send_reply(message, 'No links found. ' + HELP_STR)
return
bot_handler.send_reply(message, final_response)
@ -95,7 +94,7 @@ class LinkShortenerHandler:
def call_link_shorten_service(self, long_url: str) -> Any:
response = requests.get(
'https://api-ssl.bitly.com/v3/shorten',
params={'access_token': self.config_info['key'], 'longUrl': long_url}
params={'access_token': self.config_info['key'], 'longUrl': long_url},
)
return response.json()
@ -105,4 +104,5 @@ class LinkShortenerHandler:
def get_shorten_url(self, response_json: Any) -> str:
return response_json['data']['url']
handler_class = LinkShortenerHandler

View file

@ -13,36 +13,50 @@ class TestLinkShortenerBot(BotTestCase, DefaultTests):
def test_bot_responds_to_empty_message(self) -> None:
with patch('requests.get'):
self._test('',
('No links found. '
'Mention the link shortener bot in a conversation and '
'then enter any URLs you want to shorten in the body of '
'the message.'))
self._test(
'',
(
'No links found. '
'Mention the link shortener bot in a conversation and '
'then enter any URLs you want to shorten in the body of '
'the message.'
),
)
def test_normal(self) -> None:
with self.mock_http_conversation('test_normal'):
self._test('Shorten https://www.github.com/zulip/zulip please.',
'https://www.github.com/zulip/zulip: http://bit.ly/2Ht2hOI')
self._test(
'Shorten https://www.github.com/zulip/zulip please.',
'https://www.github.com/zulip/zulip: http://bit.ly/2Ht2hOI',
)
def test_no_links(self) -> None:
# No `mock_http_conversation` is necessary because the bot will
# recognize that no links are in the message and won't make any HTTP
# requests.
with patch('requests.get'):
self._test('Shorten nothing please.',
('No links found. '
'Mention the link shortener bot in a conversation and '
'then enter any URLs you want to shorten in the body of '
'the message.'))
self._test(
'Shorten nothing please.',
(
'No links found. '
'Mention the link shortener bot in a conversation and '
'then enter any URLs you want to shorten in the body of '
'the message.'
),
)
def test_help(self) -> None:
# No `mock_http_conversation` is necessary because the bot will
# recognize that the message is 'help' and won't make any HTTP
# requests.
with patch('requests.get'):
self._test('help',
('Mention the link shortener bot in a conversation and then '
'enter any URLs you want to shorten in the body of the message.'))
self._test(
'help',
(
'Mention the link shortener bot in a conversation and then '
'enter any URLs you want to shorten in the body of the message.'
),
)
def test_exception_when_api_key_is_invalid(self) -> None:
bot_test_instance = LinkShortenerHandler()

View file

@ -20,13 +20,19 @@ class MentionHandler:
'Authorization': 'Bearer ' + self.access_token,
'Accept-Version': '1.15',
}
test_query_response = requests.get('https://api.mention.net/api/accounts/me', headers=test_query_header)
test_query_response = requests.get(
'https://api.mention.net/api/accounts/me', headers=test_query_header
)
try:
test_query_data = test_query_response.json()
if test_query_data['error'] == 'invalid_grant' and \
test_query_data['error_description'] == 'The access token provided is invalid.':
bot_handler.quit('Access Token Invalid. Please see doc.md to find out how to get it.')
if (
test_query_data['error'] == 'invalid_grant'
and test_query_data['error_description'] == 'The access token provided is invalid.'
):
bot_handler.quit(
'Access Token Invalid. Please see doc.md to find out how to get it.'
)
except KeyError:
pass
@ -71,18 +77,15 @@ class MentionHandler:
create_alert_data = {
'name': keyword,
'query': {
'type': 'basic',
'included_keywords': [keyword]
},
'query': {'type': 'basic', 'included_keywords': [keyword]},
'languages': ['en'],
'sources': ['web']
'sources': ['web'],
} # type: Any
response = requests.post(
'https://api.mention.net/api/accounts/' + self.account_id
+ '/alerts',
data=create_alert_data, headers=create_alert_header,
'https://api.mention.net/api/accounts/' + self.account_id + '/alerts',
data=create_alert_data,
headers=create_alert_header,
)
data_json = response.json()
alert_id = data_json['alert']['id']
@ -94,8 +97,11 @@ class MentionHandler:
'Accept-Version': '1.15',
}
response = requests.get(
'https://api.mention.net/api/accounts/' + self.account_id
+ '/alerts/' + alert_id + '/mentions',
'https://api.mention.net/api/accounts/'
+ self.account_id
+ '/alerts/'
+ alert_id
+ '/mentions',
headers=get_mentions_header,
)
data_json = response.json()
@ -123,7 +129,9 @@ class MentionHandler:
reply += "[{title}]({id})\n".format(title=mention['title'], id=mention['original_url'])
return reply
handler_class = MentionHandler
class MentionNoResponseException(Exception):
pass

View file

@ -8,18 +8,19 @@ class TestMentionBot(BotTestCase, DefaultTests):
bot_name = "mention"
def test_bot_responds_to_empty_message(self) -> None:
with self.mock_config_info({'access_token': '12345'}), \
patch('requests.get'):
with self.mock_config_info({'access_token': '12345'}), patch('requests.get'):
self.verify_reply('', 'Empty Mention Query')
def test_help_query(self) -> None:
with self.mock_config_info({'access_token': '12345'}), \
patch('requests.get'):
self.verify_reply('help', '''
with self.mock_config_info({'access_token': '12345'}), patch('requests.get'):
self.verify_reply(
'help',
'''
This is a Mention API Bot which will find mentions
of the given keyword throughout the web.
Version 1.00
''')
''',
)
def test_get_account_id(self) -> None:
bot_test_instance = MentionHandler()

View file

@ -3,45 +3,105 @@
# Do NOT scramble these. This is written such that it starts from top left
# to bottom right.
ALLOWED_MOVES = ([0, 0], [0, 3], [0, 6],
[1, 1], [1, 3], [1, 5],
[2, 2], [2, 3], [2, 4],
[3, 0], [3, 1], [3, 2], [3, 4], [3, 5], [3, 6],
[4, 2], [4, 3], [4, 4],
[5, 1], [5, 3], [5, 5],
[6, 0], [6, 3], [6, 6])
ALLOWED_MOVES = (
[0, 0],
[0, 3],
[0, 6],
[1, 1],
[1, 3],
[1, 5],
[2, 2],
[2, 3],
[2, 4],
[3, 0],
[3, 1],
[3, 2],
[3, 4],
[3, 5],
[3, 6],
[4, 2],
[4, 3],
[4, 4],
[5, 1],
[5, 3],
[5, 5],
[6, 0],
[6, 3],
[6, 6],
)
AM = ALLOWED_MOVES
# Do NOT scramble these, This is written such that it starts from horizontal
# to vertical, top to bottom, left to right.
HILLS = ([AM[0], AM[1], AM[2]],
[AM[3], AM[4], AM[5]],
[AM[6], AM[7], AM[8]],
[AM[9], AM[10], AM[11]],
[AM[12], AM[13], AM[14]],
[AM[15], AM[16], AM[17]],
[AM[18], AM[19], AM[20]],
[AM[21], AM[22], AM[23]],
[AM[0], AM[9], AM[21]],
[AM[3], AM[10], AM[18]],
[AM[6], AM[11], AM[15]],
[AM[1], AM[4], AM[7]],
[AM[16], AM[19], AM[22]],
[AM[8], AM[12], AM[17]],
[AM[5], AM[13], AM[20]],
[AM[2], AM[14], AM[23]],
)
HILLS = (
[AM[0], AM[1], AM[2]],
[AM[3], AM[4], AM[5]],
[AM[6], AM[7], AM[8]],
[AM[9], AM[10], AM[11]],
[AM[12], AM[13], AM[14]],
[AM[15], AM[16], AM[17]],
[AM[18], AM[19], AM[20]],
[AM[21], AM[22], AM[23]],
[AM[0], AM[9], AM[21]],
[AM[3], AM[10], AM[18]],
[AM[6], AM[11], AM[15]],
[AM[1], AM[4], AM[7]],
[AM[16], AM[19], AM[22]],
[AM[8], AM[12], AM[17]],
[AM[5], AM[13], AM[20]],
[AM[2], AM[14], AM[23]],
)
OUTER_SQUARE = ([0, 0], [0, 1], [0, 2], [0, 3], [0, 4], [0, 5], [0, 6],
[1, 0], [2, 0], [3, 0], [4, 0], [5, 0], [6, 0],
[6, 0], [6, 1], [6, 2], [6, 3], [6, 4], [6, 5], [6, 6],
[0, 6], [1, 6], [2, 6], [3, 6], [4, 6], [5, 6])
OUTER_SQUARE = (
[0, 0],
[0, 1],
[0, 2],
[0, 3],
[0, 4],
[0, 5],
[0, 6],
[1, 0],
[2, 0],
[3, 0],
[4, 0],
[5, 0],
[6, 0],
[6, 0],
[6, 1],
[6, 2],
[6, 3],
[6, 4],
[6, 5],
[6, 6],
[0, 6],
[1, 6],
[2, 6],
[3, 6],
[4, 6],
[5, 6],
)
MIDDLE_SQUARE = ([1, 1], [1, 2], [1, 3], [1, 4], [1, 5],
[2, 1], [3, 1], [4, 1], [5, 1],
[5, 1], [5, 2], [5, 3], [5, 4], [5, 5],
[1, 5], [2, 5], [3, 5], [4, 5])
MIDDLE_SQUARE = (
[1, 1],
[1, 2],
[1, 3],
[1, 4],
[1, 5],
[2, 1],
[3, 1],
[4, 1],
[5, 1],
[5, 1],
[5, 2],
[5, 3],
[5, 4],
[5, 5],
[1, 5],
[2, 5],
[3, 5],
[4, 5],
)
INNER_SQUARE = ([2, 2], [2, 3], [2, 4], [3, 2], [3, 4], [4, 2], [4, 3], [4, 4])

View file

@ -11,7 +11,7 @@ finished yet so any matches that are finished will be removed.
import json
class MerelsStorage():
class MerelsStorage:
def __init__(self, topic_name, storage):
"""Instantiate storage field.
@ -28,9 +28,8 @@ class MerelsStorage():
"""
self.storage = storage
def update_game(self, topic_name, turn, x_taken, o_taken, board, hill_uid,
take_mode):
""" Updates the current status of the game to the database.
def update_game(self, topic_name, turn, x_taken, o_taken, board, hill_uid, take_mode):
"""Updates the current status of the game to the database.
:param topic_name: The name of the topic
:param turn: "X" or "O"
@ -45,13 +44,12 @@ class MerelsStorage():
:return: None
"""
parameters = (
turn, x_taken, o_taken, board, hill_uid, take_mode)
parameters = (turn, x_taken, o_taken, board, hill_uid, take_mode)
self.storage.put(topic_name, json.dumps(parameters))
def remove_game(self, topic_name):
""" Removes the game from the database by setting it to an empty
"""Removes the game from the database by setting it to an empty
string. An empty string marks an empty match.
:param topic_name: The name of the topic
@ -75,7 +73,6 @@ class MerelsStorage():
if select == "":
return None
else:
res = (topic_name, select[0], select[1], select[2], select[3],
select[4], select[5])
res = (topic_name, select[0], select[1], select[2], select[3], select[4], select[5])
return res

View file

@ -11,19 +11,19 @@ from zulip_bots.game_handler import BadMoveException
from . import database, mechanics
COMMAND_PATTERN = re.compile(
"^(\\w*).*(\\d,\\d).*(\\d,\\d)|^(\\w+).*(\\d,\\d)")
COMMAND_PATTERN = re.compile("^(\\w*).*(\\d,\\d).*(\\d,\\d)|^(\\w+).*(\\d,\\d)")
def getInfo():
""" Gets the info on starting the game
"""Gets the info on starting the game
:return: Info on how to start the game
"""
return "To start a game, mention me and add `create`. A game will start " \
"in that topic. "
return "To start a game, mention me and add `create`. A game will start " "in that topic. "
def getHelp():
""" Gets the help message
"""Gets the help message
:return: Help message
"""
@ -36,6 +36,7 @@ take (v,h): Take an opponent's man from the grid in phase 2/3
v: vertical position of grid
h: horizontal position of grid"""
def unknown_command():
"""Returns an unknown command info
@ -44,8 +45,9 @@ def unknown_command():
message = "Unknown command. Available commands: put (v,h), take (v,h), move (v,h) -> (v,h)"
raise BadMoveException(message)
def beat(message, topic_name, merels_storage):
""" This gets triggered every time a user send a message in any topic
"""This gets triggered every time a user send a message in any topic
:param message: User's message
:param topic_name: User's current topic
:param merels_storage: Merels' storage
@ -59,8 +61,7 @@ def beat(message, topic_name, merels_storage):
if match is None:
return unknown_command()
if match.group(1) is not None and match.group(
2) is not None and match.group(3) is not None:
if match.group(1) is not None and match.group(2) is not None and match.group(3) is not None:
responses = ""
command = match.group(1)
@ -72,11 +73,9 @@ def beat(message, topic_name, merels_storage):
if mechanics.get_take_status(topic_name, merels_storage) == 1:
raise BadMoveException("Take is required to proceed."
" Please try again.\n")
raise BadMoveException("Take is required to proceed." " Please try again.\n")
responses += mechanics.move_man(topic_name, p1, p2,
merels_storage) + "\n"
responses += mechanics.move_man(topic_name, p1, p2, merels_storage) + "\n"
no_moves = after_event_checkup(responses, topic_name, merels_storage)
mechanics.update_hill_uid(topic_name, merels_storage)
@ -102,10 +101,8 @@ def beat(message, topic_name, merels_storage):
responses = ""
if mechanics.get_take_status(topic_name, merels_storage) == 1:
raise BadMoveException("Take is required to proceed."
" Please try again.\n")
responses += mechanics.put_man(topic_name, p1[0], p1[1],
merels_storage) + "\n"
raise BadMoveException("Take is required to proceed." " Please try again.\n")
responses += mechanics.put_man(topic_name, p1[0], p1[1], merels_storage) + "\n"
no_moves = after_event_checkup(responses, topic_name, merels_storage)
mechanics.update_hill_uid(topic_name, merels_storage)
@ -121,8 +118,7 @@ def beat(message, topic_name, merels_storage):
elif command == "take":
responses = ""
if mechanics.get_take_status(topic_name, merels_storage) == 1:
responses += mechanics.take_man(topic_name, p1[0], p1[1],
merels_storage) + "\n"
responses += mechanics.take_man(topic_name, p1[0], p1[1], merels_storage) + "\n"
if "Failed" in responses:
raise BadMoveException(responses)
mechanics.update_toggle_take_mode(topic_name, merels_storage)
@ -141,6 +137,7 @@ def beat(message, topic_name, merels_storage):
else:
return unknown_command()
def check_take_mode(response, topic_name, merels_storage):
"""This checks whether the previous action can result in a take mode for
current player. This assumes that the previous action is successful and not
@ -157,6 +154,7 @@ def check_take_mode(response, topic_name, merels_storage):
else:
mechanics.update_change_turn(topic_name, merels_storage)
def check_any_moves(topic_name, merels_storage):
"""Check whether the player can make any moves, if can't switch to another
player
@ -167,11 +165,11 @@ def check_any_moves(topic_name, merels_storage):
"""
if not mechanics.can_make_any_move(topic_name, merels_storage):
mechanics.update_change_turn(topic_name, merels_storage)
return "Cannot make any move on the grid. Switching to " \
"previous player.\n"
return "Cannot make any move on the grid. Switching to " "previous player.\n"
return ""
def after_event_checkup(response, topic_name, merels_storage):
"""After doing certain moves in the game, it will check for take mode
availability and check for any possible moves
@ -185,6 +183,7 @@ def after_event_checkup(response, topic_name, merels_storage):
check_take_mode(response, topic_name, merels_storage)
return check_any_moves(topic_name, merels_storage)
def check_win(topic_name, merels_storage):
"""Checks whether the current grid has a winner, if it does, finish the
game and remove it from the database

View file

@ -9,9 +9,8 @@ from . import mechanics
from .interface import construct_grid
class GameData():
def __init__(self, game_data=(
'merels', 'X', 0, 0, 'NNNNNNNNNNNNNNNNNNNNNNNN', '', 0)):
class GameData:
def __init__(self, game_data=('merels', 'X', 0, 0, 'NNNNNNNNNNNNNNNNNNNNNNNN', '', 0)):
self.topic_name = game_data[0]
self.turn = game_data[1]
self.x_taken = game_data[2]
@ -30,8 +29,14 @@ class GameData():
"""
res = (
self.topic_name, self.turn, self.x_taken, self.o_taken, self.board,
self.hill_uid, self.take_mode)
self.topic_name,
self.turn,
self.x_taken,
self.o_taken,
self.board,
self.hill_uid,
self.take_mode,
)
return res
def grid(self):
@ -63,9 +68,12 @@ class GameData():
:return: A phase number (1, 2, or 3)
"""
return mechanics.get_phase_number(self.grid(), self.turn,
self.get_x_piece_possessed_not_on_grid(),
self.get_o_piece_possessed_not_on_grid())
return mechanics.get_phase_number(
self.grid(),
self.turn,
self.get_x_piece_possessed_not_on_grid(),
self.get_o_piece_possessed_not_on_grid(),
)
def switch_turn(self):
"""Switches turn between X and O

View file

@ -56,13 +56,31 @@ def graph_grid(grid):
5 | [{}]---------[{}]---------[{}] |
| | |
6 [{}]---------------[{}]---------------[{}]`'''.format(
grid[0][0], grid[0][3], grid[0][6],
grid[1][1], grid[1][3], grid[1][5],
grid[2][2], grid[2][3], grid[2][4],
grid[3][0], grid[3][1], grid[3][2], grid[3][4], grid[3][5], grid[3][6],
grid[4][2], grid[4][3], grid[4][4],
grid[5][1], grid[5][3], grid[5][5],
grid[6][0], grid[6][3], grid[6][6])
grid[0][0],
grid[0][3],
grid[0][6],
grid[1][1],
grid[1][3],
grid[1][5],
grid[2][2],
grid[2][3],
grid[2][4],
grid[3][0],
grid[3][1],
grid[3][2],
grid[3][4],
grid[3][5],
grid[3][6],
grid[4][2],
grid[4][3],
grid[4][4],
grid[5][1],
grid[5][3],
grid[5][5],
grid[6][0],
grid[6][3],
grid[6][6],
)
def construct_grid(board):
@ -78,8 +96,7 @@ def construct_grid(board):
for k, cell in enumerate(board):
if cell == "O" or cell == "X":
grid[constants.ALLOWED_MOVES[k][0]][
constants.ALLOWED_MOVES[k][1]] = cell
grid[constants.ALLOWED_MOVES[k][0]][constants.ALLOWED_MOVES[k][1]] = cell
return grid

View file

@ -52,8 +52,7 @@ def is_jump(vpos_before, hpos_before, vpos_after, hpos_after):
False, if it is not jumping
"""
distance = sqrt(
(vpos_after - vpos_before) ** 2 + (hpos_after - hpos_before) ** 2)
distance = sqrt((vpos_after - vpos_before) ** 2 + (hpos_after - hpos_before) ** 2)
# If the man is in outer square, the distance must be 3 or 1
if [vpos_before, hpos_before] in constants.OUTER_SQUARE:
@ -83,9 +82,9 @@ def get_hills_numbers(grid):
v1, h1 = hill[0][0], hill[0][1]
v2, h2 = hill[1][0], hill[1][1]
v3, h3 = hill[2][0], hill[2][1]
if all(x == "O" for x in
(grid[v1][h1], grid[v2][h2], grid[v3][h3])) or all(
x == "X" for x in (grid[v1][h1], grid[v2][h2], grid[v3][h3])):
if all(x == "O" for x in (grid[v1][h1], grid[v2][h2], grid[v3][h3])) or all(
x == "X" for x in (grid[v1][h1], grid[v2][h2], grid[v3][h3])
):
relative_hills += str(k)
return relative_hills
@ -148,14 +147,14 @@ def is_legal_move(v1, h1, v2, h2, turn, phase, grid):
return False # Place all the pieces first before moving one
if phase == 3 and get_piece(turn, grid) == 3:
return is_in_grid(v2, h2) and is_empty(v2, h2, grid) and is_own_piece(
v1, h1, turn, grid)
return is_in_grid(v2, h2) and is_empty(v2, h2, grid) and is_own_piece(v1, h1, turn, grid)
return is_in_grid(v2, h2) and is_empty(v2, h2, grid) and (
not is_jump(v1, h1, v2, h2)) and is_own_piece(v1,
h1,
turn,
grid)
return (
is_in_grid(v2, h2)
and is_empty(v2, h2, grid)
and (not is_jump(v1, h1, v2, h2))
and is_own_piece(v1, h1, turn, grid)
)
def is_own_piece(v, h, turn, grid):
@ -195,8 +194,12 @@ def is_legal_take(v, h, turn, grid, take_mode):
:return: True if it is legal, False if it is not legal
"""
return is_in_grid(v, h) and not is_empty(v, h, grid) and not is_own_piece(
v, h, turn, grid) and take_mode == 1
return (
is_in_grid(v, h)
and not is_empty(v, h, grid)
and not is_own_piece(v, h, turn, grid)
and take_mode == 1
)
def get_piece(turn, grid):
@ -239,8 +242,7 @@ def who_won(topic_name, merels_storage):
return "None"
def get_phase_number(grid, turn, x_pieces_possessed_not_on_grid,
o_pieces_possessed_not_on_grid):
def get_phase_number(grid, turn, x_pieces_possessed_not_on_grid, o_pieces_possessed_not_on_grid):
"""Updates current game phase
:param grid: A 2-dimensional 7x7 list
@ -253,8 +255,7 @@ def get_phase_number(grid, turn, x_pieces_possessed_not_on_grid,
is "flying"
"""
if x_pieces_possessed_not_on_grid != 0 or o_pieces_possessed_not_on_grid \
!= 0:
if x_pieces_possessed_not_on_grid != 0 or o_pieces_possessed_not_on_grid != 0:
# Placing pieces
return 1
else:
@ -277,14 +278,15 @@ def create_room(topic_name, merels_storage):
if merels.create_new_game(topic_name):
response = ""
response += "A room has been created in {0}. Starting game now.\n". \
format(topic_name)
response += "A room has been created in {0}. Starting game now.\n".format(topic_name)
response += display_game(topic_name, merels_storage)
return response
else:
return "Failed: Cannot create an already existing game in {}. " \
"Please finish the game first.".format(topic_name)
return (
"Failed: Cannot create an already existing game in {}. "
"Please finish the game first.".format(topic_name)
)
def display_game(topic_name, merels_storage):
@ -309,7 +311,9 @@ def display_game(topic_name, merels_storage):
response += interface.graph_grid(data.grid()) + "\n"
response += """Phase {}. Take mode: {}.
X taken: {}, O taken: {}.
""".format(data.get_phase(), take, data.x_taken, data.o_taken)
""".format(
data.get_phase(), take, data.x_taken, data.o_taken
)
return response
@ -324,8 +328,7 @@ def reset_game(topic_name, merels_storage):
merels = database.MerelsStorage(topic_name, merels_storage)
merels.remove_game(topic_name)
return "Game removed.\n" + create_room(topic_name,
merels_storage) + "Game reset.\n"
return "Game removed.\n" + create_room(topic_name, merels_storage) + "Game reset.\n"
def move_man(topic_name, p1, p2, merels_storage):
@ -344,8 +347,7 @@ def move_man(topic_name, p1, p2, merels_storage):
grid = data.grid()
# Check legal move
if is_legal_move(p1[0], p1[1], p2[0], p2[1], data.turn, data.get_phase(),
data.grid()):
if is_legal_move(p1[0], p1[1], p2[0], p2[1], data.turn, data.get_phase(), data.grid()):
# Move the man
move_man_legal(p1[0], p1[1], p2[0], p2[1], grid)
# Construct the board back from updated grid
@ -353,14 +355,22 @@ def move_man(topic_name, p1, p2, merels_storage):
# Insert/update the current board
data.board = board
# Update the game data
merels.update_game(data.topic_name, data.turn, data.x_taken,
data.o_taken, data.board, data.hill_uid,
data.take_mode)
merels.update_game(
data.topic_name,
data.turn,
data.x_taken,
data.o_taken,
data.board,
data.hill_uid,
data.take_mode,
)
return "Moved a man from ({}, {}) -> ({}, {}) for {}.".format(
p1[0], p1[1], p2[0], p2[1], data.turn)
p1[0], p1[1], p2[0], p2[1], data.turn
)
else:
raise BadMoveException("Failed: That's not a legal move. Please try again.")
def put_man(topic_name, v, h, merels_storage):
"""Puts a man into the specified cell in topic_name
@ -385,9 +395,15 @@ def put_man(topic_name, v, h, merels_storage):
# Insert/update form current board
data.board = board
# Update the game data
merels.update_game(data.topic_name, data.turn, data.x_taken,
data.o_taken, data.board, data.hill_uid,
data.take_mode)
merels.update_game(
data.topic_name,
data.turn,
data.x_taken,
data.o_taken,
data.board,
data.hill_uid,
data.take_mode,
)
return "Put a man to ({}, {}) for {}.".format(v, h, data.turn)
else:
raise BadMoveException("Failed: That's not a legal put. Please try again.")
@ -423,9 +439,15 @@ def take_man(topic_name, v, h, merels_storage):
# Insert/update form current board
data.board = board
# Update the game data
merels.update_game(data.topic_name, data.turn, data.x_taken,
data.o_taken, data.board, data.hill_uid,
data.take_mode)
merels.update_game(
data.topic_name,
data.turn,
data.x_taken,
data.o_taken,
data.board,
data.hill_uid,
data.take_mode,
)
return "Taken a man from ({}, {}) for {}.".format(v, h, data.turn)
else:
raise BadMoveException("Failed: That's not a legal take. Please try again.")
@ -444,9 +466,15 @@ def update_hill_uid(topic_name, merels_storage):
data.hill_uid = get_hills_numbers(data.grid())
merels.update_game(data.topic_name, data.turn, data.x_taken,
data.o_taken, data.board, data.hill_uid,
data.take_mode)
merels.update_game(
data.topic_name,
data.turn,
data.x_taken,
data.o_taken,
data.board,
data.hill_uid,
data.take_mode,
)
def update_change_turn(topic_name, merels_storage):
@ -462,9 +490,15 @@ def update_change_turn(topic_name, merels_storage):
data.switch_turn()
merels.update_game(data.topic_name, data.turn, data.x_taken,
data.o_taken, data.board, data.hill_uid,
data.take_mode)
merels.update_game(
data.topic_name,
data.turn,
data.x_taken,
data.o_taken,
data.board,
data.hill_uid,
data.take_mode,
)
def update_toggle_take_mode(topic_name, merels_storage):
@ -480,9 +514,15 @@ def update_toggle_take_mode(topic_name, merels_storage):
data.toggle_take_mode()
merels.update_game(data.topic_name, data.turn, data.x_taken,
data.o_taken, data.board, data.hill_uid,
data.take_mode)
merels.update_game(
data.topic_name,
data.turn,
data.x_taken,
data.o_taken,
data.board,
data.hill_uid,
data.take_mode,
)
def get_take_status(topic_name, merels_storage):
@ -534,8 +574,7 @@ def can_take_mode(topic_name, merels_storage):
updated_hill_uid = get_hills_numbers(updated_grid)
if current_hill_uid != updated_hill_uid and len(updated_hill_uid) >= len(
current_hill_uid):
if current_hill_uid != updated_hill_uid and len(updated_hill_uid) >= len(current_hill_uid):
return True
else:
return False

View file

@ -16,8 +16,8 @@ class Storage:
def get(self, topic_name):
return self.data[topic_name]
class MerelsModel:
class MerelsModel:
def __init__(self, board: Any = None) -> None:
self.topic = "merels"
self.storage = Storage(self.topic)
@ -34,8 +34,9 @@ class MerelsModel:
data = game_data.GameData(merels.get_game_data(self.topic))
if data.get_phase() > 1:
if (mechanics.get_piece("X", data.grid()) <= 2) or\
(mechanics.get_piece("O", data.grid()) <= 2):
if (mechanics.get_piece("X", data.grid()) <= 2) or (
mechanics.get_piece("O", data.grid()) <= 2
):
return True
return False
@ -43,14 +44,14 @@ class MerelsModel:
if self.storage.get(self.topic) == '["X", 0, 0, "NNNNNNNNNNNNNNNNNNNNNNNN", "", 0]':
self.storage.put(
self.topic,
'["{}", 0, 0, "NNNNNNNNNNNNNNNNNNNNNNNN", "", 0]'.format(
self.token[player_number]
))
'["{}", 0, 0, "NNNNNNNNNNNNNNNNNNNNNNNN", "", 0]'.format(self.token[player_number]),
)
self.current_board, same_player_move = game.beat(move, self.topic, self.storage)
if same_player_move != "":
raise SamePlayerMove(same_player_move)
return self.current_board
class MerelsMessageHandler:
tokens = [':o_button:', ':cross_mark_button:']
@ -66,11 +67,13 @@ class MerelsMessageHandler:
def game_start_message(self) -> str:
return game.getHelp()
class MerelsHandler(GameAdapter):
'''
You can play merels! Make sure your message starts with
"@mention-bot".
'''
META = {
'name': 'merels',
'description': 'Lets you play merels against any player.',
@ -95,9 +98,10 @@ class MerelsHandler(GameAdapter):
model,
gameMessageHandler,
rules,
max_players = 2,
min_players = 2,
supports_computer=False
max_players=2,
min_players=2,
supports_computer=False,
)
handler_class = MerelsHandler

View file

@ -4,47 +4,83 @@ from libraries import constants
class CheckIntegrity(unittest.TestCase):
def test_grid_layout_integrity(self):
grid_layout = ([0, 0], [0, 3], [0, 6],
[1, 1], [1, 3], [1, 5],
[2, 2], [2, 3], [2, 4],
[3, 0], [3, 1], [3, 2], [3, 4], [3, 5], [3, 6],
[4, 2], [4, 3], [4, 4],
[5, 1], [5, 3], [5, 5],
[6, 0], [6, 3], [6, 6])
grid_layout = (
[0, 0],
[0, 3],
[0, 6],
[1, 1],
[1, 3],
[1, 5],
[2, 2],
[2, 3],
[2, 4],
[3, 0],
[3, 1],
[3, 2],
[3, 4],
[3, 5],
[3, 6],
[4, 2],
[4, 3],
[4, 4],
[5, 1],
[5, 3],
[5, 5],
[6, 0],
[6, 3],
[6, 6],
)
self.assertEqual(constants.ALLOWED_MOVES, grid_layout,
"Incorrect grid layout.")
self.assertEqual(constants.ALLOWED_MOVES, grid_layout, "Incorrect grid layout.")
def test_relative_hills_integrity(self):
grid_layout = ([0, 0], [0, 3], [0, 6],
[1, 1], [1, 3], [1, 5],
[2, 2], [2, 3], [2, 4],
[3, 0], [3, 1], [3, 2], [3, 4], [3, 5], [3, 6],
[4, 2], [4, 3], [4, 4],
[5, 1], [5, 3], [5, 5],
[6, 0], [6, 3], [6, 6])
grid_layout = (
[0, 0],
[0, 3],
[0, 6],
[1, 1],
[1, 3],
[1, 5],
[2, 2],
[2, 3],
[2, 4],
[3, 0],
[3, 1],
[3, 2],
[3, 4],
[3, 5],
[3, 6],
[4, 2],
[4, 3],
[4, 4],
[5, 1],
[5, 3],
[5, 5],
[6, 0],
[6, 3],
[6, 6],
)
AM = grid_layout
relative_hills = ([AM[0], AM[1], AM[2]],
[AM[3], AM[4], AM[5]],
[AM[6], AM[7], AM[8]],
[AM[9], AM[10], AM[11]],
[AM[12], AM[13], AM[14]],
[AM[15], AM[16], AM[17]],
[AM[18], AM[19], AM[20]],
[AM[21], AM[22], AM[23]],
[AM[0], AM[9], AM[21]],
[AM[3], AM[10], AM[18]],
[AM[6], AM[11], AM[15]],
[AM[1], AM[4], AM[7]],
[AM[16], AM[19], AM[22]],
[AM[8], AM[12], AM[17]],
[AM[5], AM[13], AM[20]],
[AM[2], AM[14], AM[23]],
)
relative_hills = (
[AM[0], AM[1], AM[2]],
[AM[3], AM[4], AM[5]],
[AM[6], AM[7], AM[8]],
[AM[9], AM[10], AM[11]],
[AM[12], AM[13], AM[14]],
[AM[15], AM[16], AM[17]],
[AM[18], AM[19], AM[20]],
[AM[21], AM[22], AM[23]],
[AM[0], AM[9], AM[21]],
[AM[3], AM[10], AM[18]],
[AM[6], AM[11], AM[15]],
[AM[1], AM[4], AM[7]],
[AM[16], AM[19], AM[22]],
[AM[8], AM[12], AM[17]],
[AM[5], AM[13], AM[20]],
[AM[2], AM[14], AM[23]],
)
self.assertEqual(constants.HILLS, relative_hills,
"Incorrect relative hills arrangement")
self.assertEqual(constants.HILLS, relative_hills, "Incorrect relative hills arrangement")

View file

@ -1,4 +1,3 @@
from libraries import database, game_data
from zulip_bots.simple_lib import SimpleStorage
@ -15,8 +14,7 @@ class DatabaseTest(BotTestCase, DefaultTests):
def test_obtain_gamedata(self):
self.merels.update_game("topic1", "X", 0, 0, 'NNNNNNNNNNNNNNNNNNNNNNNN', "", 0)
res = self.merels.get_game_data("topic1")
self.assertTupleEqual(res, (
'topic1', 'X', 0, 0, 'NNNNNNNNNNNNNNNNNNNNNNNN', "", 0))
self.assertTupleEqual(res, ('topic1', 'X', 0, 0, 'NNNNNNNNNNNNNNNNNNNNNNNN', "", 0))
self.assertEqual(len(res), 7)
def test_obtain_nonexisting_gamedata(self):

View file

@ -4,10 +4,11 @@ from libraries import interface
class BoardLayoutTest(unittest.TestCase):
def test_empty_layout_arrangement(self):
grid = interface.construct_grid("NNNNNNNNNNNNNNNNNNNNNNNN")
self.assertEqual(interface.graph_grid(grid), '''` 0 1 2 3 4 5 6
self.assertEqual(
interface.graph_grid(grid),
'''` 0 1 2 3 4 5 6
0 [ ]---------------[ ]---------------[ ]
| | |
1 | [ ]---------[ ]---------[ ] |
@ -20,11 +21,14 @@ class BoardLayoutTest(unittest.TestCase):
| | | | |
5 | [ ]---------[ ]---------[ ] |
| | |
6 [ ]---------------[ ]---------------[ ]`''')
6 [ ]---------------[ ]---------------[ ]`''',
)
def test_full_layout_arragement(self):
grid = interface.construct_grid("NXONXONXONXONXONXONXONXO")
self.assertEqual(interface.graph_grid(grid), '''` 0 1 2 3 4 5 6
self.assertEqual(
interface.graph_grid(grid),
'''` 0 1 2 3 4 5 6
0 [ ]---------------[X]---------------[O]
| | |
1 | [ ]---------[X]---------[O] |
@ -37,11 +41,14 @@ class BoardLayoutTest(unittest.TestCase):
| | | | |
5 | [ ]---------[X]---------[O] |
| | |
6 [ ]---------------[X]---------------[O]`''')
6 [ ]---------------[X]---------------[O]`''',
)
def test_illegal_character_arrangement(self):
grid = interface.construct_grid("ABCDABCDABCDABCDABCDXXOO")
self.assertEqual(interface.graph_grid(grid), '''` 0 1 2 3 4 5 6
self.assertEqual(
interface.graph_grid(grid),
'''` 0 1 2 3 4 5 6
0 [ ]---------------[ ]---------------[ ]
| | |
1 | [ ]---------[ ]---------[ ] |
@ -54,25 +61,27 @@ class BoardLayoutTest(unittest.TestCase):
| | | | |
5 | [ ]---------[ ]---------[X] |
| | |
6 [X]---------------[O]---------------[O]`''')
6 [X]---------------[O]---------------[O]`''',
)
class ParsingTest(unittest.TestCase):
def test_consistent_parse(self):
boards = ["NNNNOOOOXXXXNNNNOOOOXXXX",
"NOXNXOXNOXNOXOXOXNOXONON",
"OOONXNOXNONXONOXNXNNONOX",
"NNNNNNNNNNNNNNNNNNNNNNNN",
"OOOOOOOOOOOOOOOOOOOOOOOO",
"XXXXXXXXXXXXXXXXXXXXXXXX"]
boards = [
"NNNNOOOOXXXXNNNNOOOOXXXX",
"NOXNXOXNOXNOXOXOXNOXONON",
"OOONXNOXNONXONOXNXNNONOX",
"NNNNNNNNNNNNNNNNNNNNNNNN",
"OOOOOOOOOOOOOOOOOOOOOOOO",
"XXXXXXXXXXXXXXXXXXXXXXXX",
]
for board in boards:
self.assertEqual(board, interface.construct_board(
interface.construct_grid(
interface.construct_board(
interface.construct_grid(board)
self.assertEqual(
board,
interface.construct_board(
interface.construct_grid(
interface.construct_board(interface.construct_grid(board))
)
)
)
),
)

View file

@ -6,118 +6,198 @@ from zulip_bots.simple_lib import SimpleStorage
class GridTest(unittest.TestCase):
def test_out_of_grid(self):
points = [[v, h] for h in range(7) for v in range(7)]
expected_outcomes = [True, False, False, True, False, False, True,
False, True, False, True, False, True, False,
False, False, True, True, True, False, False,
True, True, True, False, True, True, True,
False, False, True, True, True, False, False,
False, True, False, True, False, True, False,
True, False, False, True, False, False, True]
expected_outcomes = [
True,
False,
False,
True,
False,
False,
True,
False,
True,
False,
True,
False,
True,
False,
False,
False,
True,
True,
True,
False,
False,
True,
True,
True,
False,
True,
True,
True,
False,
False,
True,
True,
True,
False,
False,
False,
True,
False,
True,
False,
True,
False,
True,
False,
False,
True,
False,
False,
True,
]
test_outcomes = [mechanics.is_in_grid(point[0], point[1]) for point in
points]
test_outcomes = [mechanics.is_in_grid(point[0], point[1]) for point in points]
self.assertListEqual(test_outcomes, expected_outcomes)
def test_jump_and_grids(self):
points = [[0, 0, 1, 1], [1, 1, 2, 2], [2, 2, 3, 3], [0, 0, 0, 2],
[0, 0, 2, 2], [6, 6, 5, 4]]
points = [
[0, 0, 1, 1],
[1, 1, 2, 2],
[2, 2, 3, 3],
[0, 0, 0, 2],
[0, 0, 2, 2],
[6, 6, 5, 4],
]
expected_outcomes = [True, True, True, True, True, True]
test_outcomes = [
mechanics.is_jump(point[0], point[1], point[2], point[3]) for point
in points]
mechanics.is_jump(point[0], point[1], point[2], point[3]) for point in points
]
self.assertListEqual(test_outcomes, expected_outcomes)
def test_jump_special_cases(self):
points = [[0, 0, 0, 3], [0, 0, 3, 0], [6, 0, 6, 3], [4, 2, 6, 2],
[4, 3, 3, 4], [4, 3, 2, 2],
[0, 0, 0, 6], [0, 0, 1, 1], [0, 0, 2, 2], [3, 0, 3, 1],
[3, 0, 3, 2], [3, 1, 3, 0], [3, 1, 3, 2]]
points = [
[0, 0, 0, 3],
[0, 0, 3, 0],
[6, 0, 6, 3],
[4, 2, 6, 2],
[4, 3, 3, 4],
[4, 3, 2, 2],
[0, 0, 0, 6],
[0, 0, 1, 1],
[0, 0, 2, 2],
[3, 0, 3, 1],
[3, 0, 3, 2],
[3, 1, 3, 0],
[3, 1, 3, 2],
]
expected_outcomes = [False, False, False, True, True, True, True, True,
True, False, True, False, False]
expected_outcomes = [
False,
False,
False,
True,
True,
True,
True,
True,
True,
False,
True,
False,
False,
]
test_outcomes = [
mechanics.is_jump(point[0], point[1], point[2], point[3]) for point
in points]
mechanics.is_jump(point[0], point[1], point[2], point[3]) for point in points
]
self.assertListEqual(test_outcomes, expected_outcomes)
def test_not_populated_move(self):
grid = interface.construct_grid("XXXNNNOOOXXXNNNOOOXXXNNN")
moves = [[0, 0, 1, 1], [0, 3, 1, 3], [5, 1, 5, 3], [0, 0, 0, 3],
[0, 0, 3, 0]]
moves = [[0, 0, 1, 1], [0, 3, 1, 3], [5, 1, 5, 3], [0, 0, 0, 3], [0, 0, 3, 0]]
expected_outcomes = [True, True, False, False, False]
test_outcomes = [mechanics.is_empty(move[2], move[3], grid) for move in
moves]
test_outcomes = [mechanics.is_empty(move[2], move[3], grid) for move in moves]
self.assertListEqual(test_outcomes, expected_outcomes)
def test_legal_move(self):
grid = interface.construct_grid("XXXNNNOOONNNNNNOOONNNNNN")
presets = [[0, 0, 0, 3, "X", 1], [0, 0, 0, 6, "X", 2],
[0, 0, 3, 6, "X", 3], [0, 0, 2, 2, "X", 3]]
presets = [
[0, 0, 0, 3, "X", 1],
[0, 0, 0, 6, "X", 2],
[0, 0, 3, 6, "X", 3],
[0, 0, 2, 2, "X", 3],
]
expected_outcomes = [False, False, True, False]
test_outcomes = [
mechanics.is_legal_move(preset[0], preset[1], preset[2], preset[3],
preset[4], preset[5], grid)
for preset in presets]
mechanics.is_legal_move(
preset[0], preset[1], preset[2], preset[3], preset[4], preset[5], grid
)
for preset in presets
]
self.assertListEqual(test_outcomes, expected_outcomes)
def test_legal_put(self):
grid = interface.construct_grid("XXXNNNOOOXXXNNNOOOXXXNNN")
presets = [[0, 0, 1], [0, 3, 2], [0, 6, 3], [1, 1, 2], [1, 3, 1],
[1, 6, 1], [1, 5, 1]]
presets = [[0, 0, 1], [0, 3, 2], [0, 6, 3], [1, 1, 2], [1, 3, 1], [1, 6, 1], [1, 5, 1]]
expected_outcomes = [False, False, False, False, True, False, True]
test_outcomes = [
mechanics.is_legal_put(preset[0], preset[1], grid, preset[2]) for
preset in presets]
mechanics.is_legal_put(preset[0], preset[1], grid, preset[2]) for preset in presets
]
self.assertListEqual(test_outcomes, expected_outcomes)
def test_legal_take(self):
grid = interface.construct_grid("XXXNNNOOOXXXNNNOOOXXXNNN")
presets = [[0, 0, "X", 1], [0, 1, "X", 1], [0, 0, "O", 1],
[0, 0, "O", 0], [0, 1, "O", 1], [2, 2, "X", 1],
[2, 3, "X", 1], [2, 4, "O", 1]]
presets = [
[0, 0, "X", 1],
[0, 1, "X", 1],
[0, 0, "O", 1],
[0, 0, "O", 0],
[0, 1, "O", 1],
[2, 2, "X", 1],
[2, 3, "X", 1],
[2, 4, "O", 1],
]
expected_outcomes = [False, False, True, False, False, True, True,
False]
expected_outcomes = [False, False, True, False, False, True, True, False]
test_outcomes = [
mechanics.is_legal_take(preset[0], preset[1], preset[2], grid,
preset[3]) for preset in
presets]
mechanics.is_legal_take(preset[0], preset[1], preset[2], grid, preset[3])
for preset in presets
]
self.assertListEqual(test_outcomes, expected_outcomes)
def test_own_piece(self):
grid = interface.construct_grid("XXXNNNOOOXXXNNNOOOXXXNNN")
presets = [[0, 0, "X"], [0, 0, "O"], [0, 6, "X"], [0, 6, "O"],
[1, 1, "X"], [1, 1, "O"]]
presets = [[0, 0, "X"], [0, 0, "O"], [0, 6, "X"], [0, 6, "O"], [1, 1, "X"], [1, 1, "O"]]
expected_outcomes = [True, False, True, False, False, False]
test_outcomes = [
mechanics.is_own_piece(preset[0], preset[1], preset[2], grid) for
preset in presets]
mechanics.is_own_piece(preset[0], preset[1], preset[2], grid) for preset in presets
]
self.assertListEqual(test_outcomes, expected_outcomes)
@ -172,14 +252,12 @@ class PhaseTest(unittest.TestCase):
res = game_data.GameData(merels.get_game_data("test"))
self.assertEqual(res.get_phase(), 1)
merels.update_game(res.topic_name, "O", 5, 4,
"XXXXNNNOOOOONNNNNNNNNNNN", "03", 0)
merels.update_game(res.topic_name, "O", 5, 4, "XXXXNNNOOOOONNNNNNNNNNNN", "03", 0)
res = game_data.GameData(merels.get_game_data("test"))
self.assertEqual(res.board, "XXXXNNNOOOOONNNNNNNNNNNN")
self.assertEqual(res.get_phase(), 2)
merels.update_game(res.topic_name, "X", 6, 4,
"XXXNNNNOOOOONNNNNNNNNNNN", "03", 0)
merels.update_game(res.topic_name, "X", 6, 4, "XXXNNNNOOOOONNNNNNNNNNNN", "03", 0)
res = game_data.GameData(merels.get_game_data("test"))
self.assertEqual(res.board, "XXXNNNNOOOOONNNNNNNNNNNN")
self.assertEqual(res.get_phase(), 3)

View file

@ -10,9 +10,13 @@ class TestMerelsBot(BotTestCase, DefaultTests):
bot_name = 'merels'
def test_no_command(self):
message = dict(content='magic', type='stream', sender_email="boo@email.com", sender_full_name="boo")
message = dict(
content='magic', type='stream', sender_email="boo@email.com", sender_full_name="boo"
)
res = self.get_response(message)
self.assertEqual(res['content'], 'You are not in a game at the moment.'' Type `help` for help.')
self.assertEqual(
res['content'], 'You are not in a game at the moment.' ' Type `help` for help.'
)
# FIXME: Add tests for computer moves
# FIXME: Add test lib for game_handler
@ -23,7 +27,9 @@ class TestMerelsBot(BotTestCase, DefaultTests):
model, message_handler = self._get_game_handlers()
self.assertNotEqual(message_handler.get_player_color(0), None)
self.assertNotEqual(message_handler.game_start_message(), None)
self.assertEqual(message_handler.alert_move_message('foo', 'moved right'), 'foo :moved right')
self.assertEqual(
message_handler.alert_move_message('foo', 'moved right'), 'foo :moved right'
)
# Test to see if the attributes exist
def test_has_attributes(self) -> None:
@ -55,15 +61,17 @@ class TestMerelsBot(BotTestCase, DefaultTests):
bot, bot_handler = self._get_handlers()
message = {
'sender_email': '{}@example.com'.format(name),
'sender_full_name': '{}'.format(name)}
'sender_full_name': '{}'.format(name),
}
bot.add_user_to_cache(message)
return bot
def setup_game(self) -> None:
bot = self.add_user_to_cache('foo')
self.add_user_to_cache('baz', bot)
instance = GameInstance(bot, False, 'test game', 'abc123', [
'foo@example.com', 'baz@example.com'], 'test')
instance = GameInstance(
bot, False, 'test game', 'abc123', ['foo@example.com', 'baz@example.com'], 'test'
)
bot.instances.update({'abc123': instance})
instance.start()
return bot
@ -77,7 +85,9 @@ class TestMerelsBot(BotTestCase, DefaultTests):
response = message_handler.parse_board(board)
self.assertEqual(response, expected_response)
def _test_determine_game_over(self, board: List[List[int]], players: List[str], expected_response: str) -> None:
def _test_determine_game_over(
self, board: List[List[int]], players: List[str], expected_response: str
) -> None:
model, message_handler = self._get_game_handlers()
response = model.determine_game_over(players)
self.assertEqual(response, expected_response)

View file

@ -20,9 +20,11 @@ def fetch(options: dict):
res = requests.get("https://monkeytest.it/test", params=options)
if "server timed out" in res.text:
return {"error": "The server timed out before sending a response to "
"the request. Report is available at "
"[Test Report History]"
"(https://monkeytest.it/dashboard)."}
return {
"error": "The server timed out before sending a response to "
"the request. Report is available at "
"[Test Report History]"
"(https://monkeytest.it/dashboard)."
}
return json.loads(res.text)

View file

@ -23,12 +23,18 @@ def execute(message: Text, apikey: Text) -> Text:
len_params = len(params)
if len_params < 2:
return failed("You **must** provide at least an URL to perform a "
"check.")
return failed("You **must** provide at least an URL to perform a " "check.")
options = {"secret": apikey, "url": params[1], "on_load": "true",
"on_click": "true", "page_weight": "true", "seo": "true",
"broken_links": "true", "asset_count": "true"}
options = {
"secret": apikey,
"url": params[1],
"on_load": "true",
"on_click": "true",
"page_weight": "true",
"seo": "true",
"broken_links": "true",
"asset_count": "true",
}
# Set the options only if supplied
@ -48,9 +54,11 @@ def execute(message: Text, apikey: Text) -> Text:
try:
fetch_result = extract.fetch(options)
except JSONDecodeError:
return failed("Cannot decode a JSON response. "
"Perhaps faulty link. Link must start "
"with `http://` or `https://`.")
return failed(
"Cannot decode a JSON response. "
"Perhaps faulty link. Link must start "
"with `http://` or `https://`."
)
return report.compose(fetch_result)
@ -58,8 +66,7 @@ def execute(message: Text, apikey: Text) -> Text:
# the user needs to modify the asset_count. There are probably ways
# to counteract this, but I think this is more fast to run.
else:
return "Unknown command. Available commands: `check <website> " \
"[params]`"
return "Unknown command. Available commands: `check <website> " "[params]`"
def failed(message: Text) -> Text:

View file

@ -74,13 +74,15 @@ def print_failures_checkers(results: Dict) -> Text:
:return: A response string containing number of failures in each enabled
checkers
"""
failures_checkers = [(checker, len(results['failures'][checker]))
for checker in get_enabled_checkers(results)
if checker in results['failures']] # [('seo', 3), ..]
failures_checkers = [
(checker, len(results['failures'][checker]))
for checker in get_enabled_checkers(results)
if checker in results['failures']
] # [('seo', 3), ..]
failures_checkers_messages = ["{} ({})".format(fail_checker[0],
fail_checker[1]) for fail_checker in
failures_checkers]
failures_checkers_messages = [
"{} ({})".format(fail_checker[0], fail_checker[1]) for fail_checker in failures_checkers
]
failures_checkers_message = ", ".join(failures_checkers_messages)
return "Failures from checkers: {}".format(failures_checkers_message)
@ -113,8 +115,7 @@ def print_enabled_checkers(results: Dict) -> Text:
:param results: A dictionary containing the results of a check
:return: A response string containing enabled checkers
"""
return "Enabled checkers: {}".format(", "
.join(get_enabled_checkers(results)))
return "Enabled checkers: {}".format(", ".join(get_enabled_checkers(results)))
def print_status(results: Dict) -> Text:

View file

@ -11,37 +11,43 @@ class MonkeyTestitBot:
self.config = None
def usage(self):
return "Remember to set your api_key first in the config. After " \
"that, to perform a check, mention me and add the website.\n\n" \
"Check doc.md for more options and setup instructions."
return (
"Remember to set your api_key first in the config. After "
"that, to perform a check, mention me and add the website.\n\n"
"Check doc.md for more options and setup instructions."
)
def initialize(self, bot_handler: BotHandler) -> None:
try:
self.config = bot_handler.get_config_info('monkeytestit')
except NoBotConfigException:
bot_handler.quit("Quitting because there's no config file "
"supplied. See doc.md for a guide on setting up "
"one. If you already know the drill, just create "
"a .conf file with \"monkeytestit\" as the "
"section header and api_key = <your key> for "
"the api key.")
bot_handler.quit(
"Quitting because there's no config file "
"supplied. See doc.md for a guide on setting up "
"one. If you already know the drill, just create "
"a .conf file with \"monkeytestit\" as the "
"section header and api_key = <your key> for "
"the api key."
)
self.api_key = self.config.get('api_key')
if not self.api_key:
bot_handler.quit("Config file exists, but can't find api_key key "
"or value. Perhaps it is misconfigured. Check "
"doc.md for details on how to setup the config.")
bot_handler.quit(
"Config file exists, but can't find api_key key "
"or value. Perhaps it is misconfigured. Check "
"doc.md for details on how to setup the config."
)
logging.info("Checking validity of API key. This will take a while.")
if "wrong secret" in parse.execute("check https://website",
self.api_key).lower():
bot_handler.quit("API key exists, but it is not valid. Reconfigure"
" your api_key value and try again.")
if "wrong secret" in parse.execute("check https://website", self.api_key).lower():
bot_handler.quit(
"API key exists, but it is not valid. Reconfigure"
" your api_key value and try again."
)
def handle_message(self, message: Dict[str, str],
bot_handler: BotHandler) -> None:
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
content = message['content']
response = parse.execute(content, self.api_key)

View file

@ -9,7 +9,8 @@ class TestMonkeyTestitBot(BotTestCase, DefaultTests):
def setUp(self):
self.monkeytestit_class = import_module(
"zulip_bots.bots.monkeytestit.monkeytestit").MonkeyTestitBot
"zulip_bots.bots.monkeytestit.monkeytestit"
).MonkeyTestitBot
def test_bot_responds_to_empty_message(self):
message = dict(

View file

@ -32,8 +32,9 @@ def get_help_text() -> str:
command_text = ''
for command in commands:
if 'template' in command.keys() and 'description' in command.keys():
command_text += '**{}**: {}\n'.format('{} [arguments]'.format(
command['template']), command['description'])
command_text += '**{}**: {}\n'.format(
'{} [arguments]'.format(command['template']), command['description']
)
object_type_text = ''
for object_type in object_types.values():
object_type_text += '{}\n'.format(object_type['table'])
@ -41,11 +42,11 @@ def get_help_text() -> str:
def format_result(
result: Dict[str, Any],
exclude_keys: List[str] = [],
force_keys: List[str] = [],
rank_output: bool = False,
show_all_keys: bool = False
result: Dict[str, Any],
exclude_keys: List[str] = [],
force_keys: List[str] = [],
rank_output: bool = False,
show_all_keys: bool = False,
) -> str:
exclude_keys += ['Name', 'attributes', 'Id']
output = ''
@ -53,8 +54,7 @@ def format_result(
return 'No records found.'
if result['totalSize'] == 1:
record = result['records'][0]
output += '**[{}]({}{})**\n'.format(record['Name'],
login_url, record['Id'])
output += '**[{}]({}{})**\n'.format(record['Name'], login_url, record['Id'])
for key, value in record.items():
if key not in exclude_keys:
output += '>**{}**: {}\n'.format(key, value)
@ -62,8 +62,7 @@ def format_result(
for i, record in enumerate(result['records']):
if rank_output:
output += '{}) '.format(i + 1)
output += '**[{}]({}{})**\n'.format(record['Name'],
login_url, record['Id'])
output += '**[{}]({}{})**\n'.format(record['Name'], login_url, record['Id'])
added_keys = False
for key, value in record.items():
if key in force_keys or (show_all_keys and key not in exclude_keys):
@ -74,7 +73,9 @@ def format_result(
return output
def query_salesforce(arg: str, salesforce: simple_salesforce.Salesforce, command: Dict[str, Any]) -> str:
def query_salesforce(
arg: str, salesforce: simple_salesforce.Salesforce, command: Dict[str, Any]
) -> str:
arg = arg.strip()
qarg = arg.split(' -', 1)[0]
split_args = [] # type: List[str]
@ -92,8 +93,9 @@ def query_salesforce(arg: str, salesforce: simple_salesforce.Salesforce, command
if 'query' in command.keys():
query = command['query']
object_type = object_types[command['object']]
res = salesforce.query(query.format(
object_type['fields'], object_type['table'], qarg, limit_num))
res = salesforce.query(
query.format(object_type['fields'], object_type['table'], qarg, limit_num)
)
exclude_keys = [] # type: List[str]
if 'exclude_keys' in command.keys():
exclude_keys = command['exclude_keys']
@ -106,7 +108,13 @@ def query_salesforce(arg: str, salesforce: simple_salesforce.Salesforce, command
show_all_keys = 'show' in split_args
if 'show_all_keys' in command.keys():
show_all_keys = command['show_all_keys'] or 'show' in split_args
return format_result(res, exclude_keys=exclude_keys, force_keys=force_keys, rank_output=rank_output, show_all_keys=show_all_keys)
return format_result(
res,
exclude_keys=exclude_keys,
force_keys=force_keys,
rank_output=rank_output,
show_all_keys=show_all_keys,
)
def get_salesforce_link_details(link: str, sf: Any) -> str:
@ -116,8 +124,7 @@ def get_salesforce_link_details(link: str, sf: Any) -> str:
return 'Invalid salesforce link'
id = re_id_res.group().strip('/')
for object_type in object_types.values():
res = sf.query(link_query.format(
object_type['fields'], object_type['table'], id))
res = sf.query(link_query.format(object_type['fields'], object_type['table'], id))
if res['totalSize'] == 1:
return format_result(res)
return 'No object found. Make sure it is of the supported types. Type `help` for more info.'
@ -160,7 +167,7 @@ class SalesforceHandler:
self.sf = simple_salesforce.Salesforce(
username=self.config_info['username'],
password=self.config_info['password'],
security_token=self.config_info['security_token']
security_token=self.config_info['security_token'],
)
except simple_salesforce.exceptions.SalesforceAuthenticationFailed as err:
bot_handler.quit('Failed to log in to Salesforce. {} {}'.format(err.code, err.message))

View file

@ -26,7 +26,7 @@ def mock_salesforce_auth(is_success: bool) -> Iterator[None]:
else:
with patch(
'simple_salesforce.api.Salesforce.__init__',
side_effect=SalesforceAuthenticationFailed(403, 'auth failed')
side_effect=SalesforceAuthenticationFailed(403, 'auth failed'),
) as mock_sf_init:
mock_sf_init.return_value = None
yield
@ -34,16 +34,13 @@ def mock_salesforce_auth(is_success: bool) -> Iterator[None]:
@contextmanager
def mock_salesforce_commands_types() -> Iterator[None]:
with patch('zulip_bots.bots.salesforce.utils.commands', mock_commands), \
patch('zulip_bots.bots.salesforce.utils.object_types', mock_object_types):
with patch('zulip_bots.bots.salesforce.utils.commands', mock_commands), patch(
'zulip_bots.bots.salesforce.utils.object_types', mock_object_types
):
yield
mock_config = {
'username': 'name@example.com',
'password': 'foo',
'security_token': 'abcdefg'
}
mock_config = {'username': 'name@example.com', 'password': 'foo', 'security_token': 'abcdefg'}
help_text = '''Salesforce bot
This bot can do simple salesforce query requests
@ -86,24 +83,15 @@ mock_commands = [
'rank_output': True,
'force_keys': ['Amount'],
'exclude_keys': ['Status'],
'show_all_keys': True
'show_all_keys': True,
},
{
'commands': ['echo'],
'callback': echo
}
{'commands': ['echo'], 'callback': echo},
]
mock_object_types = {
'contact': {
'fields': 'Id, Name, Phone',
'table': 'Table'
},
'opportunity': {
'fields': 'Id, Name, Amount, Status',
'table': 'Table'
}
'contact': {'fields': 'Id, Name, Phone', 'table': 'Table'},
'opportunity': {'fields': 'Id, Name, Amount, Status', 'table': 'Table'},
}
@ -111,16 +99,15 @@ class TestSalesforceBot(BotTestCase, DefaultTests):
bot_name = "salesforce" # type: str
def _test(self, test_name: str, message: str, response: str, auth_success: bool = True) -> None:
with self.mock_config_info(mock_config), \
mock_salesforce_auth(auth_success), \
mock_salesforce_query(test_name, 'salesforce'), \
mock_salesforce_commands_types():
with self.mock_config_info(mock_config), mock_salesforce_auth(
auth_success
), mock_salesforce_query(test_name, 'salesforce'), mock_salesforce_commands_types():
self.verify_reply(message, response)
def _test_initialize(self, auth_success: bool = True) -> None:
with self.mock_config_info(mock_config), \
mock_salesforce_auth(auth_success), \
mock_salesforce_commands_types():
with self.mock_config_info(mock_config), mock_salesforce_auth(
auth_success
), mock_salesforce_commands_types():
bot, bot_handler = self._get_handlers()
def test_bot_responds_to_empty_message(self) -> None:
@ -170,8 +157,7 @@ class TestSalesforceBot(BotTestCase, DefaultTests):
def test_help(self) -> None:
self._test('test_one_result', 'help', help_text)
self._test('test_one_result', 'foo bar baz', help_text)
self._test('test_one_result', 'find contact',
'Usage: find contact <name> [arguments]')
self._test('test_one_result', 'find contact', 'Usage: find contact <name> [arguments]')
def test_bad_auth(self) -> None:
with self.assertRaises(StubBotHandler.BotQuitException):
@ -184,15 +170,15 @@ class TestSalesforceBot(BotTestCase, DefaultTests):
res = '''**[foo](https://login.salesforce.com/foo_id)**
>**Phone**: 020 1234 5678
'''
self._test('test_one_result',
'https://login.salesforce.com/1c3e5g7i9k1m3o5q7s', res)
self._test('test_one_result', 'https://login.salesforce.com/1c3e5g7i9k1m3o5q7s', res)
def test_link_invalid(self) -> None:
self._test('test_one_result',
'https://login.salesforce.com/foo/bar/1c3e5g7$i9k1m3o5q7',
'Invalid salesforce link')
self._test(
'test_one_result',
'https://login.salesforce.com/foo/bar/1c3e5g7$i9k1m3o5q7',
'Invalid salesforce link',
)
def test_link_no_results(self) -> None:
res = 'No object found. Make sure it is of the supported types. Type `help` for more info.'
self._test('test_no_results',
'https://login.salesforce.com/1c3e5g7i9k1m3o5q7s', res)
self._test('test_no_results', 'https://login.salesforce.com/1c3e5g7i9k1m3o5q7s', res)

View file

@ -8,42 +8,49 @@ commands = [
'commands': ['search account', 'find account', 'search accounts', 'find accounts'],
'object': 'account',
'description': 'Returns a list of accounts of the name specified',
'template': 'search account <name>'
'template': 'search account <name>',
},
{
'commands': ['search contact', 'find contact', 'search contacts', 'find contacts'],
'object': 'contact',
'description': 'Returns a list of contacts of the name specified',
'template': 'search contact <name>'
'template': 'search contact <name>',
},
{
'commands': ['search opportunity', 'find opportunity', 'search opportunities', 'find opportunities'],
'commands': [
'search opportunity',
'find opportunity',
'search opportunities',
'find opportunities',
],
'object': 'opportunity',
'description': 'Returns a list of opportunities of the name specified',
'template': 'search opportunity <name>'
'template': 'search opportunity <name>',
},
{
'commands': ['search top opportunity', 'find top opportunity', 'search top opportunities', 'find top opportunities'],
'commands': [
'search top opportunity',
'find top opportunity',
'search top opportunities',
'find top opportunities',
],
'object': 'opportunity',
'query': 'SELECT {} FROM {} WHERE isClosed=false ORDER BY amount DESC LIMIT {}',
'description': 'Returns a list of opportunities organised by amount',
'template': 'search top opportunities <amount>',
'rank_output': True,
'force_keys': ['Amount']
}
'force_keys': ['Amount'],
},
] # type: List[Dict[str, Any]]
object_types = {
'account': {
'fields': 'Id, Name, Phone, BillingStreet, BillingCity, BillingState',
'table': 'Account'
},
'contact': {
'fields': 'Id, Name, Phone, MobilePhone, Email',
'table': 'Contact'
'table': 'Account',
},
'contact': {'fields': 'Id, Name, Phone, MobilePhone, Email', 'table': 'Contact'},
'opportunity': {
'fields': 'Id, Name, Amount, Probability, StageName, CloseDate',
'table': 'Opportunity'
}
'table': 'Opportunity',
},
} # type: Dict[str, Dict[str, str]]

View file

@ -7,6 +7,7 @@ from zulip_bots.lib import BotHandler
# See readme.md for instructions on running this code.
class StackOverflowHandler:
'''
This plugin facilitates searching Stack Overflow for a
@ -34,7 +35,9 @@ class StackOverflowHandler:
bot_response = self.get_bot_stackoverflow_response(message, bot_handler)
bot_handler.send_reply(message, bot_response)
def get_bot_stackoverflow_response(self, message: Dict[str, str], bot_handler: BotHandler) -> Optional[str]:
def get_bot_stackoverflow_response(
self, message: Dict[str, str], bot_handler: BotHandler
) -> Optional[str]:
'''This function returns the URLs of the requested topic.'''
help_text = 'Please enter your query after @mention-bot to search StackOverflow'
@ -45,36 +48,38 @@ class StackOverflowHandler:
return help_text
query_stack_url = 'http://api.stackexchange.com/2.2/search/advanced'
query_stack_params = dict(
order='desc',
sort='relevance',
site='stackoverflow',
title=query
)
query_stack_params = dict(order='desc', sort='relevance', site='stackoverflow', title=query)
try:
data = requests.get(query_stack_url, params=query_stack_params)
except requests.exceptions.RequestException:
logging.error('broken link')
return 'Uh-Oh ! Sorry ,couldn\'t process the request right now.:slightly_frowning_face:\n' \
'Please try again later.'
return (
'Uh-Oh ! Sorry ,couldn\'t process the request right now.:slightly_frowning_face:\n'
'Please try again later.'
)
# Checking if the bot accessed the link.
if data.status_code != 200:
logging.error('Page not found.')
return 'Uh-Oh ! Sorry ,couldn\'t process the request right now.:slightly_frowning_face:\n' \
'Please try again later.'
return (
'Uh-Oh ! Sorry ,couldn\'t process the request right now.:slightly_frowning_face:\n'
'Please try again later.'
)
new_content = 'For search term:' + query + '\n'
# Checking if there is content for the searched term
if len(data.json()['items']) == 0:
new_content = 'I am sorry. The search term you provided is not found :slightly_frowning_face:'
new_content = (
'I am sorry. The search term you provided is not found :slightly_frowning_face:'
)
else:
for i in range(min(3, len(data.json()['items']))):
search_string = data.json()['items'][i]['title']
link = data.json()['items'][i]['link']
new_content += str(i+1) + ' : ' + '[' + search_string + ']' + '(' + link + ')\n'
new_content += str(i + 1) + ' : ' + '[' + search_string + ']' + '(' + link + ')\n'
return new_content
handler_class = StackOverflowHandler

View file

@ -9,42 +9,46 @@ class TestStackoverflowBot(BotTestCase, DefaultTests):
# Single-word query
bot_request = 'restful'
bot_response = ('''For search term:restful
bot_response = '''For search term:restful
1 : [What exactly is RESTful programming?](https://stackoverflow.com/questions/671118/what-exactly-is-restful-programming)
2 : [RESTful Authentication](https://stackoverflow.com/questions/319530/restful-authentication)
3 : [RESTful URL design for search](https://stackoverflow.com/questions/319530/restful-authentication)
''')
'''
with self.mock_http_conversation('test_single_word'):
self.verify_reply(bot_request, bot_response)
# Multi-word query
bot_request = 'what is flutter'
bot_response = ('''For search term:what is flutter
bot_response = '''For search term:what is flutter
1 : [What is flutter/dart and what are its benefits over other tools?](https://stackoverflow.com/questions/49023008/what-is-flutter-dart-and-what-are-its-benefits-over-other-tools)
''')
'''
with self.mock_http_conversation('test_multi_word'):
self.verify_reply(bot_request, bot_response)
# Number query
bot_request = '113'
bot_response = ('''For search term:113
bot_response = '''For search term:113
1 : [INSTALL_FAILED_NO_MATCHING_ABIS res-113](https://stackoverflow.com/questions/47117788/install-failed-no-matching-abis-res-113)
2 : [com.sun.tools.xjc.reader.Ring.get(Ring.java:113)](https://stackoverflow.com/questions/12848282/com-sun-tools-xjc-reader-ring-getring-java113)
3 : [no route to host error 113](https://stackoverflow.com/questions/10516222/no-route-to-host-error-113)
''')
'''
with self.mock_http_conversation('test_number_query'):
self.verify_reply(bot_request, bot_response)
# Incorrect word
bot_request = 'narendra'
bot_response = "I am sorry. The search term you provided is not found :slightly_frowning_face:"
bot_response = (
"I am sorry. The search term you provided is not found :slightly_frowning_face:"
)
with self.mock_http_conversation('test_incorrect_query'):
self.verify_reply(bot_request, bot_response)
# 404 status code
bot_request = 'Zulip'
bot_response = 'Uh-Oh ! Sorry ,couldn\'t process the request right now.:slightly_frowning_face:\n' \
'Please try again later.'
bot_response = (
'Uh-Oh ! Sorry ,couldn\'t process the request right now.:slightly_frowning_face:\n'
'Please try again later.'
)
with self.mock_http_conversation('test_status_code'):
self.verify_reply(bot_request, bot_response)

View file

@ -50,4 +50,5 @@ class SusiHandler:
answer = "I don't understand. Can you rephrase?"
bot_handler.send_reply(message, answer)
handler_class = SusiHandler

View file

@ -17,51 +17,49 @@ class TestTicTacToeBot(BotTestCase, DefaultTests):
# avoid these errors.
def test_get_value(self) -> None:
board = [[0, 1, 0],
[0, 0, 0],
[0, 0, 2]]
board = [[0, 1, 0], [0, 0, 0], [0, 0, 2]]
position = (0, 1)
response = 1
self._test_get_value(board, position, response)
def _test_get_value(self, board: List[List[int]], position: Tuple[int, int], expected_response: int) -> None:
def _test_get_value(
self, board: List[List[int]], position: Tuple[int, int], expected_response: int
) -> None:
model, message_handler = self._get_game_handlers()
tictactoeboard = model(board)
response = tictactoeboard.get_value(board, position)
self.assertEqual(response, expected_response)
def test_determine_game_over_with_win(self) -> None:
board = [[1, 1, 1],
[0, 2, 0],
[2, 0, 2]]
board = [[1, 1, 1], [0, 2, 0], [2, 0, 2]]
players = ['Human', 'Computer']
response = 'current turn'
self._test_determine_game_over_with_win(board, players, response)
def _test_determine_game_over_with_win(self, board: List[List[int]], players: List[str], expected_response: str) -> None:
def _test_determine_game_over_with_win(
self, board: List[List[int]], players: List[str], expected_response: str
) -> None:
model, message_handler = self._get_game_handlers()
tictactoegame = model(board)
response = tictactoegame.determine_game_over(players)
self.assertEqual(response, expected_response)
def test_determine_game_over_with_draw(self) -> None:
board = [[1, 2, 1],
[1, 2, 1],
[2, 1, 2]]
board = [[1, 2, 1], [1, 2, 1], [2, 1, 2]]
players = ['Human', 'Computer']
response = 'draw'
self._test_determine_game_over_with_draw(board, players, response)
def _test_determine_game_over_with_draw(self, board: List[List[int]], players: List[str], expected_response: str) -> None:
def _test_determine_game_over_with_draw(
self, board: List[List[int]], players: List[str], expected_response: str
) -> None:
model, message_handler = self._get_game_handlers()
tictactoeboard = model(board)
response = tictactoeboard.determine_game_over(players)
self.assertEqual(response, expected_response)
def test_board_is_full(self) -> None:
board = [[1, 0, 1],
[1, 2, 1],
[2, 1, 2]]
board = [[1, 0, 1], [1, 2, 1], [2, 1, 2]]
response = False
self._test_board_is_full(board, response)
@ -72,9 +70,7 @@ class TestTicTacToeBot(BotTestCase, DefaultTests):
self.assertEqual(response, expected_response)
def test_contains_winning_move(self) -> None:
board = [[1, 1, 1],
[0, 2, 0],
[2, 0, 2]]
board = [[1, 1, 1], [0, 2, 0], [2, 0, 2]]
response = True
self._test_contains_winning_move(board, response)
@ -85,22 +81,20 @@ class TestTicTacToeBot(BotTestCase, DefaultTests):
self.assertEqual(response, expected_response)
def test_get_locations_of_char(self) -> None:
board = [[0, 0, 0],
[0, 0, 0],
[0, 0, 1]]
board = [[0, 0, 0], [0, 0, 0], [0, 0, 1]]
response = [[2, 2]]
self._test_get_locations_of_char(board, response)
def _test_get_locations_of_char(self, board: List[List[int]], expected_response: List[List[int]]) -> None:
def _test_get_locations_of_char(
self, board: List[List[int]], expected_response: List[List[int]]
) -> None:
model, message_handler = self._get_game_handlers()
tictactoeboard = model(board)
response = tictactoeboard.get_locations_of_char(board, 1)
self.assertEqual(response, expected_response)
def test_is_valid_move(self) -> None:
board = [[0, 0, 0],
[0, 0, 0],
[1, 0, 2]]
board = [[0, 0, 0], [0, 0, 0], [1, 0, 2]]
move = "1,2"
response = True
self._test_is_valid_move(board, move, response)
@ -109,7 +103,9 @@ class TestTicTacToeBot(BotTestCase, DefaultTests):
response = False
self._test_is_valid_move(board, move, response)
def _test_is_valid_move(self, board: List[List[int]], move: str, expected_response: bool) -> None:
def _test_is_valid_move(
self, board: List[List[int]], move: str, expected_response: bool
) -> None:
model, message_handler = self._get_game_handlers()
tictactoeboard = model(board)
response = tictactoeboard.is_valid_move(move)
@ -130,24 +126,20 @@ class TestTicTacToeBot(BotTestCase, DefaultTests):
model, message_handler = self._get_game_handlers()
self.assertNotEqual(message_handler.get_player_color(0), None)
self.assertNotEqual(message_handler.game_start_message(), None)
self.assertEqual(message_handler.alert_move_message(
'foo', 'move 3'), 'foo put a token at 3')
self.assertEqual(
message_handler.alert_move_message('foo', 'move 3'), 'foo put a token at 3'
)
def test_has_attributes(self) -> None:
model, message_handler = self._get_game_handlers()
self.assertTrue(hasattr(message_handler, 'parse_board') is not None)
self.assertTrue(
hasattr(message_handler, 'alert_move_message') is not None)
self.assertTrue(hasattr(message_handler, 'alert_move_message') is not None)
self.assertTrue(hasattr(model, 'current_board') is not None)
self.assertTrue(hasattr(model, 'determine_game_over') is not None)
def test_parse_board(self) -> None:
board = [[0, 1, 0],
[0, 0, 0],
[0, 0, 2]]
response = ':one: :x: :three:\n\n' +\
':four: :five: :six:\n\n' +\
':seven: :eight: :o:\n\n'
board = [[0, 1, 0], [0, 0, 0], [0, 0, 2]]
response = ':one: :x: :three:\n\n' + ':four: :five: :six:\n\n' + ':seven: :eight: :o:\n\n'
self._test_parse_board(board, response)
def _test_parse_board(self, board: List[List[int]], expected_response: str) -> None:
@ -160,7 +152,7 @@ class TestTicTacToeBot(BotTestCase, DefaultTests):
bot, bot_handler = self._get_handlers()
message = {
'sender_email': '{}@example.com'.format(name),
'sender_full_name': '{}'.format(name)
'sender_full_name': '{}'.format(name),
}
bot.add_user_to_cache(message)
return bot
@ -168,8 +160,9 @@ class TestTicTacToeBot(BotTestCase, DefaultTests):
def setup_game(self) -> None:
bot = self.add_user_to_cache('foo')
self.add_user_to_cache('baz', bot)
instance = GameInstance(bot, False, 'test game', 'abc123', [
'foo@example.com', 'baz@example.com'], 'test')
instance = GameInstance(
bot, False, 'test game', 'abc123', ['foo@example.com', 'baz@example.com'], 'test'
)
bot.instances.update({'abc123': instance})
instance.start()
return bot

View file

@ -13,19 +13,18 @@ class TicTacToeModel:
smarter = True
# If smarter is True, the computer will do some extra thinking - it'll be harder for the user.
triplets = [[(0, 0), (0, 1), (0, 2)], # Row 1
[(1, 0), (1, 1), (1, 2)], # Row 2
[(2, 0), (2, 1), (2, 2)], # Row 3
[(0, 0), (1, 0), (2, 0)], # Column 1
[(0, 1), (1, 1), (2, 1)], # Column 2
[(0, 2), (1, 2), (2, 2)], # Column 3
[(0, 0), (1, 1), (2, 2)], # Diagonal 1
[(0, 2), (1, 1), (2, 0)] # Diagonal 2
]
triplets = [
[(0, 0), (0, 1), (0, 2)], # Row 1
[(1, 0), (1, 1), (1, 2)], # Row 2
[(2, 0), (2, 1), (2, 2)], # Row 3
[(0, 0), (1, 0), (2, 0)], # Column 1
[(0, 1), (1, 1), (2, 1)], # Column 2
[(0, 2), (1, 2), (2, 2)], # Column 3
[(0, 0), (1, 1), (2, 2)], # Diagonal 1
[(0, 2), (1, 1), (2, 0)], # Diagonal 2
]
initial_board = [[0, 0, 0],
[0, 0, 0],
[0, 0, 0]]
initial_board = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
def __init__(self, board: Any = None) -> None:
if board is not None:
@ -44,7 +43,7 @@ class TicTacToeModel:
return ''
def board_is_full(self, board: Any) -> bool:
''' Determines if the board is full or not. '''
'''Determines if the board is full or not.'''
for row in board:
for element in row:
if element == 0:
@ -53,8 +52,8 @@ class TicTacToeModel:
# Used for current board & trial computer board
def contains_winning_move(self, board: Any) -> bool:
''' Returns true if all coordinates in a triplet have the same value in them (x or o) and no coordinates
in the triplet are blank. '''
'''Returns true if all coordinates in a triplet have the same value in them (x or o) and no coordinates
in the triplet are blank.'''
for triplet in self.triplets:
if (
self.get_value(board, triplet[0])
@ -66,7 +65,7 @@ class TicTacToeModel:
return False
def get_locations_of_char(self, board: Any, char: int) -> List[List[int]]:
''' Gets the locations of the board that have char in them. '''
'''Gets the locations of the board that have char in them.'''
locations = []
for row in range(3):
for col in range(3):
@ -75,8 +74,8 @@ class TicTacToeModel:
return locations
def two_blanks(self, triplet: List[Tuple[int, int]], board: Any) -> List[Tuple[int, int]]:
''' Determines which rows/columns/diagonals have two blank spaces and an 2 already in them. It's more advantageous
for the computer to move there. This is used when the computer makes its move. '''
'''Determines which rows/columns/diagonals have two blank spaces and an 2 already in them. It's more advantageous
for the computer to move there. This is used when the computer makes its move.'''
o_found = False
for position in triplet:
@ -95,9 +94,8 @@ class TicTacToeModel:
return []
def computer_move(self, board: Any, player_number: Any) -> Any:
''' The computer's logic for making its move. '''
my_board = copy.deepcopy(
board) # First the board is copied; used later on
'''The computer's logic for making its move.'''
my_board = copy.deepcopy(board) # First the board is copied; used later on
blank_locations = self.get_locations_of_char(my_board, 0)
# Gets the locations that already have x's
x_locations = self.get_locations_of_char(board, 1)
@ -186,7 +184,7 @@ class TicTacToeModel:
return board
def is_valid_move(self, move: str) -> bool:
''' Checks the validity of the coordinate input passed in to make sure it's not out-of-bounds (ex. 5, 5) '''
'''Checks the validity of the coordinate input passed in to make sure it's not out-of-bounds (ex. 5, 5)'''
try:
split_move = move.split(",")
row = split_move[0].strip()
@ -220,9 +218,19 @@ class TicTacToeMessageHandler:
tokens = [':x:', ':o:']
def parse_row(self, row: Tuple[int, int], row_num: int) -> str:
''' Takes the row passed in as a list and returns it as a string. '''
'''Takes the row passed in as a list and returns it as a string.'''
row_chars = []
num_symbols = [':one:', ':two:', ':three:', ':four:', ':five:', ':six:', ':seven:', ':eight:', ':nine:']
num_symbols = [
':one:',
':two:',
':three:',
':four:',
':five:',
':six:',
':seven:',
':eight:',
':nine:',
]
for i, e in enumerate(row):
if e == 0:
row_chars.append(num_symbols[row_num * 3 + i])
@ -232,7 +240,7 @@ class TicTacToeMessageHandler:
return row_string + '\n\n'
def parse_board(self, board: Any) -> str:
''' Takes the board as a nested list and returns a nice version for the user. '''
'''Takes the board as a nested list and returns a nice version for the user.'''
return "".join([self.parse_row(r, r_num) for r_num, r in enumerate(board)])
def get_player_color(self, turn: int) -> str:
@ -243,8 +251,9 @@ class TicTacToeMessageHandler:
return '{} put a token at {}'.format(original_player, move_info)
def game_start_message(self) -> str:
return ("Welcome to tic-tac-toe!"
"To make a move, type @-mention `move <number>` or `<number>`")
return (
"Welcome to tic-tac-toe!" "To make a move, type @-mention `move <number>` or `<number>`"
)
class ticTacToeHandler(GameAdapter):
@ -252,6 +261,7 @@ class ticTacToeHandler(GameAdapter):
You can play tic-tac-toe! Make sure your message starts with
"@mention-bot".
'''
META = {
'name': 'TicTacToe',
'description': 'Lets you play Tic-tac-toe against a computer.',
@ -279,15 +289,15 @@ class ticTacToeHandler(GameAdapter):
model,
gameMessageHandler,
rules,
supports_computer=True
supports_computer=True,
)
def coords_from_command(cmd: str) -> str:
# This function translates the input command into a TicTacToeGame move.
# It should return two indices, each one of (1,2,3), separated by a comma, eg. "3,2"
''' As there are various ways to input a coordinate (with/without parentheses, with/without spaces, etc.) the
input is stripped to just the numbers before being used in the program. '''
'''As there are various ways to input a coordinate (with/without parentheses, with/without spaces, etc.) the
input is stripped to just the numbers before being used in the program.'''
cmd_num = int(cmd.replace('move ', '')) - 1
cmd = '{},{}'.format((cmd_num % 3) + 1, (cmd_num // 3) + 1)
return cmd

View file

@ -3,11 +3,8 @@ from unittest.mock import patch
from zulip_bots.bots.trello.trello import TrelloHandler
from zulip_bots.test_lib import BotTestCase, DefaultTests, StubBotHandler
mock_config = {
'api_key': 'TEST',
'access_token': 'TEST',
'user_name': 'TEST'
}
mock_config = {'api_key': 'TEST', 'access_token': 'TEST', 'user_name': 'TEST'}
class TestTrelloBot(BotTestCase, DefaultTests):
bot_name = "trello" # type: str
@ -18,11 +15,14 @@ class TestTrelloBot(BotTestCase, DefaultTests):
def test_bot_usage(self) -> None:
with self.mock_config_info(mock_config), patch('requests.get'):
self.verify_reply('help', '''
self.verify_reply(
'help',
'''
This interactive bot can be used to interact with Trello.
Use `list-commands` to get information about the supported commands.
''')
''',
)
def test_bot_quit_with_invalid_config(self) -> None:
with self.mock_config_info(mock_config), self.assertRaises(StubBotHandler.BotQuitException):
@ -34,13 +34,15 @@ class TestTrelloBot(BotTestCase, DefaultTests):
self.verify_reply('abcd', 'Command not supported')
def test_list_commands_command(self) -> None:
expected_reply = ('**Commands:** \n'
'1. **help**: Get the bot usage information.\n'
'2. **list-commands**: Get information about the commands supported by the bot.\n'
'3. **get-all-boards**: Get all the boards under the configured account.\n'
'4. **get-all-cards <board_id>**: Get all the cards in the given board.\n'
'5. **get-all-checklists <card_id>**: Get all the checklists in the given card.\n'
'6. **get-all-lists <board_id>**: Get all the lists in the given board.\n')
expected_reply = (
'**Commands:** \n'
'1. **help**: Get the bot usage information.\n'
'2. **list-commands**: Get information about the commands supported by the bot.\n'
'3. **get-all-boards**: Get all the boards under the configured account.\n'
'4. **get-all-cards <board_id>**: Get all the cards in the given board.\n'
'5. **get-all-checklists <card_id>**: Get all the checklists in the given card.\n'
'6. **get-all-lists <board_id>**: Get all the lists in the given board.\n'
)
with self.mock_config_info(mock_config), patch('requests.get'):
self.verify_reply('list-commands', expected_reply)
@ -64,19 +66,21 @@ class TestTrelloBot(BotTestCase, DefaultTests):
def test_get_all_checklists_command(self) -> None:
with self.mock_config_info(mock_config), patch('requests.get'):
with self.mock_http_conversation('get_checklists'):
self.verify_reply('get-all-checklists TEST', '**Checklists:**\n'
'1. `TEST`:\n'
' * [X] TEST_1\n * [X] TEST_2\n'
' * [-] TEST_3\n * [-] TEST_4')
self.verify_reply(
'get-all-checklists TEST',
'**Checklists:**\n'
'1. `TEST`:\n'
' * [X] TEST_1\n * [X] TEST_2\n'
' * [-] TEST_3\n * [-] TEST_4',
)
def test_get_all_lists_command(self) -> None:
with self.mock_config_info(mock_config), patch('requests.get'):
with self.mock_http_conversation('get_lists'):
self.verify_reply('get-all-lists TEST', ('**Lists:**\n'
'1. TEST_A\n'
' * TEST_1\n'
'2. TEST_B\n'
' * TEST_2'))
self.verify_reply(
'get-all-lists TEST',
('**Lists:**\n' '1. TEST_A\n' ' * TEST_1\n' '2. TEST_B\n' ' * TEST_2'),
)
def test_command_exceptions(self) -> None:
"""Add appropriate tests here for all additional commands with try/except blocks.

View file

@ -10,12 +10,13 @@ supported_commands = [
('get-all-boards', 'Get all the boards under the configured account.'),
('get-all-cards <board_id>', 'Get all the cards in the given board.'),
('get-all-checklists <card_id>', 'Get all the checklists in the given card.'),
('get-all-lists <board_id>', 'Get all the lists in the given board.')
('get-all-lists <board_id>', 'Get all the lists in the given board.'),
]
INVALID_ARGUMENTS_ERROR_MESSAGE = 'Invalid Arguments.'
RESPONSE_ERROR_MESSAGE = 'Invalid Response. Please check configuration and parameters.'
class TrelloHandler:
def initialize(self, bot_handler: BotHandler) -> None:
self.config_info = bot_handler.get_config_info('trello')
@ -23,16 +24,14 @@ class TrelloHandler:
self.access_token = self.config_info['access_token']
self.user_name = self.config_info['user_name']
self.auth_params = {
'key': self.api_key,
'token': self.access_token
}
self.auth_params = {'key': self.api_key, 'token': self.access_token}
self.check_access_token(bot_handler)
def check_access_token(self, bot_handler: BotHandler) -> None:
test_query_response = requests.get('https://api.trello.com/1/members/{}/'.format(self.user_name),
params=self.auth_params)
test_query_response = requests.get(
'https://api.trello.com/1/members/{}/'.format(self.user_name), params=self.auth_params
)
if test_query_response.text == 'invalid key':
bot_handler.quit('Invalid Credentials. Please see doc.md to find out how to get them.')
@ -97,10 +96,14 @@ class TrelloHandler:
bot_response = [] # type: List[str]
get_board_desc_url = 'https://api.trello.com/1/boards/{}/'
for index, board in enumerate(boards):
board_desc_response = requests.get(get_board_desc_url.format(board), params=self.auth_params)
board_desc_response = requests.get(
get_board_desc_url.format(board), params=self.auth_params
)
board_data = board_desc_response.json()
bot_response += ['{_count}.[{name}]({url}) (`{id}`)'.format(_count=index + 1, **board_data)]
bot_response += [
'{_count}.[{name}]({url}) (`{id}`)'.format(_count=index + 1, **board_data)
]
return '\n'.join(bot_response)
@ -116,7 +119,9 @@ class TrelloHandler:
cards = cards_response.json()
bot_response = ['**Cards:**']
for index, card in enumerate(cards):
bot_response += ['{_count}. [{name}]({url}) (`{id}`)'.format(_count=index + 1, **card)]
bot_response += [
'{_count}. [{name}]({url}) (`{id}`)'.format(_count=index + 1, **card)
]
except (KeyError, ValueError, TypeError):
return RESPONSE_ERROR_MESSAGE
@ -139,7 +144,11 @@ class TrelloHandler:
if 'checkItems' in checklist:
for item in checklist['checkItems']:
bot_response += [' * [{}] {}'.format('X' if item['state'] == 'complete' else '-', item['name'])]
bot_response += [
' * [{}] {}'.format(
'X' if item['state'] == 'complete' else '-', item['name']
)
]
except (KeyError, ValueError, TypeError):
return RESPONSE_ERROR_MESSAGE
@ -170,4 +179,5 @@ class TrelloHandler:
return '\n'.join(bot_response)
handler_class = TrelloHandler

View file

@ -17,12 +17,14 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests, StubBotHandler, read_
class TestTriviaQuizBot(BotTestCase, DefaultTests):
bot_name = "trivia_quiz" # type: str
new_question_response = '\nQ: Which class of animals are newts members of?\n\n' + \
'* **A** Amphibian\n' + \
'* **B** Fish\n' + \
'* **C** Reptiles\n' + \
'* **D** Mammals\n' + \
'**reply**: answer Q001 <letter>'
new_question_response = (
'\nQ: Which class of animals are newts members of?\n\n'
+ '* **A** Amphibian\n'
+ '* **B** Fish\n'
+ '* **C** Reptiles\n'
+ '* **D** Mammals\n'
+ '**reply**: answer Q001 <letter>'
)
def get_test_quiz(self) -> Tuple[Dict[str, Any], Any]:
bot_handler = StubBotHandler()
@ -58,13 +60,12 @@ class TestTriviaQuizBot(BotTestCase, DefaultTests):
mock_html_unescape.side_effect = Exception
with self.assertRaises(Exception) as exception:
fix_quotes('test')
self.assertEqual(str(exception.exception), "Please use python3.4 or later for this bot.")
self.assertEqual(
str(exception.exception), "Please use python3.4 or later for this bot."
)
def test_invalid_answer(self) -> None:
invalid_replies = ['answer A',
'answer A Q10',
'answer Q001 K',
'answer 001 A']
invalid_replies = ['answer A', 'answer A Q10', 'answer Q001 K', 'answer 001 A']
for reply in invalid_replies:
self._test(reply, 'Invalid answer format')
@ -84,13 +85,17 @@ class TestTriviaQuizBot(BotTestCase, DefaultTests):
self.assertEqual(quiz['pending'], True)
# test incorrect answer
with patch('zulip_bots.bots.trivia_quiz.trivia_quiz.get_quiz_from_id',
return_value=json.dumps(quiz)):
with patch(
'zulip_bots.bots.trivia_quiz.trivia_quiz.get_quiz_from_id',
return_value=json.dumps(quiz),
):
self._test('answer Q001 B', ':disappointed: WRONG, Foo Test User! B is not correct.')
# test correct answer
with patch('zulip_bots.bots.trivia_quiz.trivia_quiz.get_quiz_from_id',
return_value=json.dumps(quiz)):
with patch(
'zulip_bots.bots.trivia_quiz.trivia_quiz.get_quiz_from_id',
return_value=json.dumps(quiz),
):
with patch('zulip_bots.bots.trivia_quiz.trivia_quiz.start_new_quiz'):
self._test('answer Q001 A', ':tada: **Amphibian** is correct, Foo Test User!')
@ -128,7 +133,9 @@ class TestTriviaQuizBot(BotTestCase, DefaultTests):
# test response and storage after three failed attempts
start_new_question, response = handle_answer(quiz, 'D', 'Q001', bot_handler, 'Test User')
self.assertEqual(response, ':disappointed: WRONG, Test User! The correct answer is **Amphibian**.')
self.assertEqual(
response, ':disappointed: WRONG, Test User! The correct answer is **Amphibian**.'
)
self.assertTrue(start_new_question)
quiz_reset = json.loads(bot_handler.storage.get('Q001'))
self.assertEqual(quiz_reset['pending'], False)
@ -136,8 +143,12 @@ class TestTriviaQuizBot(BotTestCase, DefaultTests):
# test response after question has ended
incorrect_answers = ['B', 'C', 'D']
for ans in incorrect_answers:
start_new_question, response = handle_answer(quiz, ans, 'Q001', bot_handler, 'Test User')
self.assertEqual(response, ':disappointed: WRONG, Test User! The correct answer is **Amphibian**.')
start_new_question, response = handle_answer(
quiz, ans, 'Q001', bot_handler, 'Test User'
)
self.assertEqual(
response, ':disappointed: WRONG, Test User! The correct answer is **Amphibian**.'
)
self.assertFalse(start_new_question)
start_new_question, response = handle_answer(quiz, 'A', 'Q001', bot_handler, 'Test User')
self.assertEqual(response, ':tada: **Amphibian** is correct, Test User!')

View file

@ -12,9 +12,11 @@ from zulip_bots.lib import BotHandler
class NotAvailableException(Exception):
pass
class InvalidAnswerException(Exception):
pass
class TriviaQuizHandler:
def usage(self) -> str:
return '''
@ -45,8 +47,9 @@ class TriviaQuizHandler:
bot_handler.send_reply(message, bot_response)
return
quiz = json.loads(quiz_payload)
start_new_question, bot_response = handle_answer(quiz, answer, quiz_id,
bot_handler, message['sender_full_name'])
start_new_question, bot_response = handle_answer(
quiz, answer, quiz_id, bot_handler, message['sender_full_name']
)
bot_handler.send_reply(message, bot_response)
if start_new_question:
start_new_quiz(message, bot_handler)
@ -55,9 +58,11 @@ class TriviaQuizHandler:
bot_response = 'type "new" for a new question'
bot_handler.send_reply(message, bot_response)
def get_quiz_from_id(quiz_id: str, bot_handler: BotHandler) -> str:
return bot_handler.storage.get(quiz_id)
def start_new_quiz(message: Dict[str, Any], bot_handler: BotHandler) -> None:
quiz = get_trivia_quiz()
quiz_id = generate_quiz_id(bot_handler.storage)
@ -66,6 +71,7 @@ def start_new_quiz(message: Dict[str, Any], bot_handler: BotHandler) -> None:
bot_handler.storage.put(quiz_id, json.dumps(quiz))
bot_handler.send_reply(message, bot_response, widget_content)
def parse_answer(query: str) -> Tuple[str, str]:
m = re.match(r'answer\s+(Q...)\s+(.)', query)
if not m:
@ -78,11 +84,13 @@ def parse_answer(query: str) -> Tuple[str, str]:
return (quiz_id, answer)
def get_trivia_quiz() -> Dict[str, Any]:
payload = get_trivia_payload()
quiz = get_quiz_from_payload(payload)
return quiz
def get_trivia_payload() -> Dict[str, Any]:
url = 'https://opentdb.com/api.php?amount=1&type=multiple'
@ -99,6 +107,7 @@ def get_trivia_payload() -> Dict[str, Any]:
payload = data.json()
return payload
def fix_quotes(s: str) -> Optional[str]:
# opentdb is nice enough to escape HTML for us, but
# we are sending this to code that does that already :)
@ -110,6 +119,7 @@ def fix_quotes(s: str) -> Optional[str]:
except Exception:
raise Exception('Please use python3.4 or later for this bot.')
def get_quiz_from_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
result = payload['results'][0]
question = result['question']
@ -119,12 +129,8 @@ def get_quiz_from_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
answers = dict()
answers[correct_letter] = result['correct_answer']
for i in range(3):
answers[letters[i+1]] = result['incorrect_answers'][i]
answers = {
letter: fix_quotes(answer)
for letter, answer
in answers.items()
}
answers[letters[i + 1]] = result['incorrect_answers'][i]
answers = {letter: fix_quotes(answer) for letter, answer in answers.items()}
quiz = dict(
question=fix_quotes(question),
answers=answers,
@ -134,6 +140,7 @@ def get_quiz_from_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
) # type: Dict[str, Any]
return quiz
def generate_quiz_id(storage: Any) -> str:
try:
quiz_num = storage.get('quiz_id')
@ -145,6 +152,7 @@ def generate_quiz_id(storage: Any) -> str:
quiz_id = 'Q%03d' % (quiz_num,)
return quiz_id
def format_quiz_for_widget(quiz_id: str, quiz: Dict[str, Any]) -> str:
widget_type = 'zform'
question = quiz['question']
@ -178,16 +186,19 @@ def format_quiz_for_widget(quiz_id: str, quiz: Dict[str, Any]) -> str:
payload = json.dumps(widget_content)
return payload
def format_quiz_for_markdown(quiz_id: str, quiz: Dict[str, Any]) -> str:
question = quiz['question']
answers = quiz['answers']
answer_list = '\n'.join([
'* **{letter}** {answer}'.format(
letter=letter,
answer=answers[letter],
)
for letter in 'ABCD'
])
answer_list = '\n'.join(
[
'* **{letter}** {answer}'.format(
letter=letter,
answer=answers[letter],
)
for letter in 'ABCD'
]
)
how_to_respond = '''**reply**: answer {quiz_id} <letter>'''.format(quiz_id=quiz_id)
content = '''
@ -201,9 +212,11 @@ Q: {question}
)
return content
def update_quiz(quiz: Dict[str, Any], quiz_id: str, bot_handler: BotHandler) -> None:
bot_handler.storage.put(quiz_id, json.dumps(quiz))
def build_response(is_correct: bool, num_answers: int) -> str:
if is_correct:
response = ':tada: **{answer}** is correct, {sender_name}!'
@ -214,15 +227,17 @@ def build_response(is_correct: bool, num_answers: int) -> str:
response = ':disappointed: WRONG, {sender_name}! {option} is not correct.'
return response
def handle_answer(quiz: Dict[str, Any], option: str, quiz_id: str,
bot_handler: BotHandler, sender_name: str) -> Tuple[bool, str]:
def handle_answer(
quiz: Dict[str, Any], option: str, quiz_id: str, bot_handler: BotHandler, sender_name: str
) -> Tuple[bool, str]:
answer = quiz['answers'][quiz['correct_letter']]
is_new_answer = (option not in quiz['answered_options'])
is_new_answer = option not in quiz['answered_options']
if is_new_answer:
quiz['answered_options'].append(option)
num_answers = len(quiz['answered_options'])
is_correct = (option == quiz['correct_letter'])
is_correct = option == quiz['correct_letter']
start_new_question = quiz['pending'] and (is_correct or num_answers >= 3)
if start_new_question or is_correct:
@ -232,7 +247,8 @@ def handle_answer(quiz: Dict[str, Any], option: str, quiz_id: str,
update_quiz(quiz, quiz_id, bot_handler)
response = build_response(is_correct, num_answers).format(
option=option, answer=answer, id=quiz_id, sender_name=sender_name)
option=option, answer=answer, id=quiz_id, sender_name=sender_name
)
return start_new_question, response

View file

@ -6,10 +6,12 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests, StubBotHandler, get_b
class TestTwitpostBot(BotTestCase, DefaultTests):
bot_name = "twitpost"
mock_config = {'consumer_key': 'abcdefghijklmnopqrstuvwxy',
'consumer_secret': 'aabbccddeeffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyy',
'access_token': '123456789012345678-ABCDefgh1234afdsa678lKj6gHhslsi',
'access_token_secret': 'yf0SI0x6Ct2OmF0cDQc1E0eLKXrVAPFx4QkZF2f9PfFCt'}
mock_config = {
'consumer_key': 'abcdefghijklmnopqrstuvwxy',
'consumer_secret': 'aabbccddeeffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyy',
'access_token': '123456789012345678-ABCDefgh1234afdsa678lKj6gHhslsi',
'access_token_secret': 'yf0SI0x6Ct2OmF0cDQc1E0eLKXrVAPFx4QkZF2f9PfFCt',
}
api_response = read_bot_fixture_data('twitpost', 'api_response')
def test_bot_usage(self) -> None:
@ -27,12 +29,14 @@ class TestTwitpostBot(BotTestCase, DefaultTests):
def test_help(self) -> None:
with self.mock_config_info(self.mock_config):
self.verify_reply('help',
"*Help for Twitter-post bot* :twitter: : \n\n"
"The bot tweets on twitter when message starts with @twitpost.\n\n"
"`@twitpost tweet <content>` will tweet on twitter with given `<content>`.\n"
"Example:\n"
" * @twitpost tweet hey batman\n")
self.verify_reply(
'help',
"*Help for Twitter-post bot* :twitter: : \n\n"
"The bot tweets on twitter when message starts with @twitpost.\n\n"
"`@twitpost tweet <content>` will tweet on twitter with given `<content>`.\n"
"Example:\n"
" * @twitpost tweet hey batman\n",
)
@patch('tweepy.API.update_status', return_value=api_response)
def test_tweet(self, mockedarg):

View file

@ -6,25 +6,29 @@ from zulip_bots.lib import BotHandler
class TwitpostBot:
def usage(self) -> str:
return ''' This bot posts on twitter from zulip chat itself.
Use '@twitpost help' to get more information
on the bot usage. '''
help_content = "*Help for Twitter-post bot* :twitter: : \n\n"\
"The bot tweets on twitter when message starts "\
"with @twitpost.\n\n"\
"`@twitpost tweet <content>` will tweet on twitter " \
"with given `<content>`.\n" \
"Example:\n" \
" * @twitpost tweet hey batman\n"
help_content = (
"*Help for Twitter-post bot* :twitter: : \n\n"
"The bot tweets on twitter when message starts "
"with @twitpost.\n\n"
"`@twitpost tweet <content>` will tweet on twitter "
"with given `<content>`.\n"
"Example:\n"
" * @twitpost tweet hey batman\n"
)
def initialize(self, bot_handler: BotHandler) -> None:
self.config_info = bot_handler.get_config_info('twitter')
auth = tweepy.OAuthHandler(self.config_info['consumer_key'],
self.config_info['consumer_secret'])
auth.set_access_token(self.config_info['access_token'],
self.config_info['access_token_secret'])
auth = tweepy.OAuthHandler(
self.config_info['consumer_key'], self.config_info['consumer_secret']
)
auth.set_access_token(
self.config_info['access_token'], self.config_info['access_token_secret']
)
self.api = tweepy.API(auth, parser=tweepy.parsers.JSONParser())
def handle_message(self, message: Dict[str, str], bot_handler: BotHandler) -> None:
@ -44,8 +48,7 @@ class TwitpostBot:
status = self.post(" ".join(content[1:]))
screen_name = status["user"]["screen_name"]
id_str = status["id_str"]
bot_reply = "https://twitter.com/{}/status/{}".format(screen_name,
id_str)
bot_reply = "https://twitter.com/{}/status/{}".format(screen_name, id_str)
bot_reply = "Tweet Posted\n" + bot_reply
bot_handler.send_reply(message, bot_reply)

View file

@ -6,21 +6,23 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests
class TestVirtualFsBot(BotTestCase, DefaultTests):
bot_name = "virtual_fs"
help_txt = ('foo@example.com:\n\nThis bot implements a virtual file system for a stream.\n'
'The locations of text are persisted for the lifetime of the bot\n'
'running, and if you rename a stream, you will lose the info.\n'
'Example commands:\n\n```\n'
'@mention-bot sample_conversation: sample conversation with the bot\n'
'@mention-bot mkdir: create a directory\n'
'@mention-bot ls: list a directory\n'
'@mention-bot cd: change directory\n'
'@mention-bot pwd: show current path\n'
'@mention-bot write: write text\n'
'@mention-bot read: read text\n'
'@mention-bot rm: remove a file\n'
'@mention-bot rmdir: remove a directory\n'
'```\n'
'Use commands like `@mention-bot help write` for more details on specific\ncommands.\n')
help_txt = (
'foo@example.com:\n\nThis bot implements a virtual file system for a stream.\n'
'The locations of text are persisted for the lifetime of the bot\n'
'running, and if you rename a stream, you will lose the info.\n'
'Example commands:\n\n```\n'
'@mention-bot sample_conversation: sample conversation with the bot\n'
'@mention-bot mkdir: create a directory\n'
'@mention-bot ls: list a directory\n'
'@mention-bot cd: change directory\n'
'@mention-bot pwd: show current path\n'
'@mention-bot write: write text\n'
'@mention-bot read: read text\n'
'@mention-bot rm: remove a file\n'
'@mention-bot rmdir: remove a directory\n'
'```\n'
'Use commands like `@mention-bot help write` for more details on specific\ncommands.\n'
)
def test_multiple_recipient_conversation(self) -> None:
expected = [
@ -52,9 +54,7 @@ class TestVirtualFsBot(BotTestCase, DefaultTests):
# for the user's benefit if they ask. But then we can also
# use it to test that the bot works as advertised.
expected = [
(request, 'foo@example.com:\n' + reply)
for (request, reply)
in sample_conversation()
(request, 'foo@example.com:\n' + reply) for (request, reply) in sample_conversation()
]
self.verify_dialog(expected)

View file

@ -63,6 +63,7 @@ Use commands like `@mention-bot help write` for more details on specific
commands.
'''
def sample_conversation() -> List[Tuple[str, str]]:
return [
('cd /', 'Current path: /'),
@ -112,6 +113,7 @@ def sample_conversation() -> List[Tuple[str, str]]:
('write', 'ERROR: syntax: write <path> <some_text>'),
]
REGEXES = dict(
command='(cd|ls|mkdir|read|rmdir|rm|write|pwd)',
path=r'(\S+)',
@ -119,6 +121,7 @@ REGEXES = dict(
some_text='(.+)',
)
def get_commands() -> Dict[str, Tuple[Any, List[str]]]:
return {
'help': (fs_help, ['command']),
@ -132,19 +135,16 @@ def get_commands() -> Dict[str, Tuple[Any, List[str]]]:
'pwd': (fs_pwd, []),
}
def fs_command(fs: str, user: str, cmd: str) -> Tuple[str, Any]:
cmd = cmd.strip()
if cmd == 'help':
return fs, get_help()
if cmd == 'sample_conversation':
sample = '\n\n'.join(
'\n'.join(tup)
for tup
in sample_conversation()
)
sample = '\n\n'.join('\n'.join(tup) for tup in sample_conversation())
return fs, sample
cmd_name = cmd.split()[0]
cmd_args = cmd[len(cmd_name):].strip()
cmd_args = cmd[len(cmd_name) :].strip()
commands = get_commands()
if cmd_name not in commands:
return fs, 'ERROR: unrecognized command'
@ -161,6 +161,7 @@ def fs_command(fs: str, user: str, cmd: str) -> Tuple[str, Any]:
else:
return fs, 'ERROR: ' + syntax_help(cmd_name)
def syntax_help(cmd_name: str) -> str:
commands = get_commands()
f, arg_names = commands[cmd_name]
@ -171,16 +172,16 @@ def syntax_help(cmd_name: str) -> str:
cmd = cmd_name
return 'syntax: {}'.format(cmd)
def fs_new() -> Dict[str, Any]:
fs = {
'/': directory([]),
'user_paths': dict()
}
fs = {'/': directory([]), 'user_paths': dict()}
return fs
def fs_help(fs: Dict[str, Any], user: str, cmd_name: str) -> Tuple[Dict[str, Any], Any]:
return fs, syntax_help(cmd_name)
def fs_mkdir(fs: Dict[str, Any], user: str, fn: str) -> Tuple[Dict[str, Any], Any]:
path, msg = make_path(fs, user, fn)
if msg:
@ -198,6 +199,7 @@ def fs_mkdir(fs: Dict[str, Any], user: str, fn: str) -> Tuple[Dict[str, Any], An
msg = 'directory created'
return new_fs, msg
def fs_ls(fs: Dict[str, Any], user: str, fn: str) -> Tuple[Dict[str, Any], Any]:
if fn == '.' or fn == '':
path = fs['user_paths'][user]
@ -216,11 +218,13 @@ def fs_ls(fs: Dict[str, Any], user: str, fn: str) -> Tuple[Dict[str, Any], Any]:
msg = '\n'.join('* ' + nice_path(fs, path) for path in sorted(fns))
return fs, msg
def fs_pwd(fs: Dict[str, Any], user: str) -> Tuple[Dict[str, Any], Any]:
path = fs['user_paths'][user]
msg = nice_path(fs, path)
return fs, msg
def fs_rm(fs: Dict[str, Any], user: str, fn: str) -> Tuple[Dict[str, Any], Any]:
path, msg = make_path(fs, user, fn)
if msg:
@ -238,6 +242,7 @@ def fs_rm(fs: Dict[str, Any], user: str, fn: str) -> Tuple[Dict[str, Any], Any]:
msg = 'removed'
return new_fs, msg
def fs_rmdir(fs: Dict[str, Any], user: str, fn: str) -> Tuple[Dict[str, Any], Any]:
path, msg = make_path(fs, user, fn)
if msg:
@ -253,11 +258,12 @@ def fs_rmdir(fs: Dict[str, Any], user: str, fn: str) -> Tuple[Dict[str, Any], An
directory = get_directory(path)
new_fs[directory]['fns'].remove(path)
for sub_path in list(new_fs.keys()):
if sub_path.startswith(path+'/'):
if sub_path.startswith(path + '/'):
new_fs.pop(sub_path)
msg = 'removed'
return new_fs, msg
def fs_write(fs: Dict[str, Any], user: str, fn: str, content: str) -> Tuple[Dict[str, Any], Any]:
path, msg = make_path(fs, user, fn)
if msg:
@ -276,6 +282,7 @@ def fs_write(fs: Dict[str, Any], user: str, fn: str, content: str) -> Tuple[Dict
msg = 'file written'
return new_fs, msg
def fs_read(fs: Dict[str, Any], user: str, fn: str) -> Tuple[Dict[str, Any], Any]:
path, msg = make_path(fs, user, fn)
if msg:
@ -289,6 +296,7 @@ def fs_read(fs: Dict[str, Any], user: str, fn: str) -> Tuple[Dict[str, Any], Any
val = fs[path]['content']
return fs, val
def fs_cd(fs: Dict[str, Any], user: str, fn: str) -> Tuple[Dict[str, Any], Any]:
if len(fn) > 1 and fn[-1] == '/':
fn = fn[:-1]
@ -302,6 +310,7 @@ def fs_cd(fs: Dict[str, Any], user: str, fn: str) -> Tuple[Dict[str, Any], Any]:
fs['user_paths'][user] = path
return fs, "Current path: {}".format(nice_path(fs, path))
def make_path(fs: Dict[str, Any], user: str, leaf: str) -> List[str]:
if leaf == '/':
return ['/', '']
@ -315,17 +324,19 @@ def make_path(fs: Dict[str, Any], user: str, leaf: str) -> List[str]:
path += leaf
return [path, '']
def nice_path(fs: Dict[str, Any], path: str) -> str:
path_nice = path
slash = path.rfind('/')
if path not in fs:
return 'ERROR: the current directory does not exist'
if fs[path]['kind'] == 'text':
path_nice = '{}*{}*'.format(path[:slash+1], path[slash+1:])
path_nice = '{}*{}*'.format(path[: slash + 1], path[slash + 1 :])
elif path != '/':
path_nice = '{}/'.format(path)
return path_nice
def get_directory(path: str) -> str:
slash = path.rfind('/')
if slash == 0:
@ -333,15 +344,19 @@ def get_directory(path: str) -> str:
else:
return path[:slash]
def directory(fns: Union[Set[str], List[Any]]) -> Dict[str, Union[str, List[Any]]]:
return dict(kind='dir', fns=list(fns))
def text_file(content: str) -> Dict[str, str]:
return dict(kind='text', content=content)
def is_directory(fs: Dict[str, Any], fn: str) -> bool:
if fn not in fs:
return False
return fs[fn]['kind'] == 'dir'
handler_class = VirtualFsHandler

View file

@ -7,6 +7,7 @@ from zulip_bots.lib import BotHandler
api_url = 'http://api.openweathermap.org/data/2.5/weather'
class WeatherHandler:
def initialize(self, bot_handler: BotHandler) -> None:
self.api_key = bot_handler.get_config_info('weather')['key']
@ -68,6 +69,7 @@ def to_celsius(temp_kelvin: float) -> float:
def to_fahrenheit(temp_kelvin: float) -> float:
return int(temp_kelvin) * (9. / 5.) - 459.67
return int(temp_kelvin) * (9.0 / 5.0) - 459.67
handler_class = WeatherHandler

View file

@ -9,31 +9,31 @@ class TestWikipediaBot(BotTestCase, DefaultTests):
# Single-word query
bot_request = 'happy'
bot_response = ('''For search term:happy
bot_response = '''For search term:happy
1:[Happiness](https://en.wikipedia.org/wiki/Happiness)
2:[Happy!](https://en.wikipedia.org/wiki/Happy!)
3:[Happy,_Happy](https://en.wikipedia.org/wiki/Happy,_Happy)
''')
'''
with self.mock_http_conversation('test_single_word'):
self.verify_reply(bot_request, bot_response)
# Multi-word query
bot_request = 'The sky is blue'
bot_response = ('''For search term:The sky is blue
bot_response = '''For search term:The sky is blue
1:[Sky_blue](https://en.wikipedia.org/wiki/Sky_blue)
2:[Sky_Blue_Sky](https://en.wikipedia.org/wiki/Sky_Blue_Sky)
3:[Blue_Sky](https://en.wikipedia.org/wiki/Blue_Sky)
''')
'''
with self.mock_http_conversation('test_multi_word'):
self.verify_reply(bot_request, bot_response)
# Number query
bot_request = '123'
bot_response = ('''For search term:123
bot_response = '''For search term:123
1:[123](https://en.wikipedia.org/wiki/123)
2:[Japan_Airlines_Flight_123](https://en.wikipedia.org/wiki/Japan_Airlines_Flight_123)
3:[Iodine-123](https://en.wikipedia.org/wiki/Iodine-123)
''')
'''
with self.mock_http_conversation('test_number_query'):
self.verify_reply(bot_request, bot_response)
@ -47,7 +47,9 @@ class TestWikipediaBot(BotTestCase, DefaultTests):
# Incorrect word
bot_request = 'sssssss kkkkk'
bot_response = "I am sorry. The search term you provided is not found :slightly_frowning_face:"
bot_response = (
"I am sorry. The search term you provided is not found :slightly_frowning_face:"
)
with self.mock_http_conversation('test_incorrect_query'):
self.verify_reply(bot_request, bot_response)
@ -58,8 +60,10 @@ class TestWikipediaBot(BotTestCase, DefaultTests):
# Incorrect status code
bot_request = 'Zulip'
bot_response = 'Uh-Oh ! Sorry ,couldn\'t process the request right now.:slightly_frowning_face:\n' \
'Please try again later.'
bot_response = (
'Uh-Oh ! Sorry ,couldn\'t process the request right now.:slightly_frowning_face:\n'
'Please try again later.'
)
with self.mock_http_conversation('test_status_code'):
self.verify_reply(bot_request, bot_response)

View file

@ -7,6 +7,7 @@ from zulip_bots.lib import BotHandler
# See readme.md for instructions on running this code.
class WikipediaHandler:
'''
This plugin facilitates searching Wikipedia for a
@ -47,36 +48,47 @@ class WikipediaHandler:
return help_text.format(bot_handler.identity().mention)
query_wiki_url = 'https://en.wikipedia.org/w/api.php'
query_wiki_params = dict(
action='query',
list='search',
srsearch=query,
format='json'
)
query_wiki_params = dict(action='query', list='search', srsearch=query, format='json')
try:
data = requests.get(query_wiki_url, params=query_wiki_params)
except requests.exceptions.RequestException:
logging.error('broken link')
return 'Uh-Oh ! Sorry ,couldn\'t process the request right now.:slightly_frowning_face:\n' \
'Please try again later.'
return (
'Uh-Oh ! Sorry ,couldn\'t process the request right now.:slightly_frowning_face:\n'
'Please try again later.'
)
# Checking if the bot accessed the link.
if data.status_code != 200:
logging.error('Page not found.')
return 'Uh-Oh ! Sorry ,couldn\'t process the request right now.:slightly_frowning_face:\n' \
'Please try again later.'
return (
'Uh-Oh ! Sorry ,couldn\'t process the request right now.:slightly_frowning_face:\n'
'Please try again later.'
)
new_content = 'For search term:' + query + '\n'
# Checking if there is content for the searched term
if len(data.json()['query']['search']) == 0:
new_content = 'I am sorry. The search term you provided is not found :slightly_frowning_face:'
new_content = (
'I am sorry. The search term you provided is not found :slightly_frowning_face:'
)
else:
for i in range(min(3, len(data.json()['query']['search']))):
search_string = data.json()['query']['search'][i]['title'].replace(' ', '_')
url = 'https://en.wikipedia.org/wiki/' + search_string
new_content += str(i+1) + ':' + '[' + search_string + ']' + '(' + url.replace('"', "%22") + ')\n'
new_content += (
str(i + 1)
+ ':'
+ '['
+ search_string
+ ']'
+ '('
+ url.replace('"', "%22")
+ ')\n'
)
return new_content
handler_class = WikipediaHandler

View file

@ -10,17 +10,12 @@ class TestWitaiBot(BotTestCase, DefaultTests):
MOCK_CONFIG_INFO = {
'token': '12345678',
'handler_location': '/Users/abcd/efgh',
'help_message': 'Qwertyuiop!'
'help_message': 'Qwertyuiop!',
}
MOCK_WITAI_RESPONSE = {
'_text': 'What is your favorite food?',
'entities': {
'intent': [{
'confidence': 1.0,
'value': 'favorite_food'
}]
}
'entities': {'intent': [{'confidence': 1.0, 'value': 'favorite_food'}]},
}
def test_normal(self) -> None:
@ -29,10 +24,7 @@ class TestWitaiBot(BotTestCase, DefaultTests):
get_bot_message_handler(self.bot_name).initialize(StubBotHandler())
with patch('wit.Wit.message', return_value=self.MOCK_WITAI_RESPONSE):
self.verify_reply(
'What is your favorite food?',
'pizza'
)
self.verify_reply('What is your favorite food?', 'pizza')
# This overrides the default one in `BotTestCase`.
def test_bot_responds_to_empty_message(self) -> None:
@ -42,6 +34,7 @@ class TestWitaiBot(BotTestCase, DefaultTests):
with patch('wit.Wit.message', return_value=self.MOCK_WITAI_RESPONSE):
self.verify_reply('', 'Qwertyuiop!')
def mock_handle(res: Dict[str, Any]) -> Optional[str]:
if res['entities']['intent'][0]['value'] == 'favorite_food':
return 'pizza'

View file

@ -59,8 +59,10 @@ class WitaiHandler:
print(e)
return
handler_class = WitaiHandler
def get_handle(location: str) -> Optional[Callable[[Dict[str, Any]], Optional[str]]]:
'''Returns a function to be used when generating a response from Wit.ai
bot. This function is the function named `handle` in the module at the

View file

@ -7,17 +7,21 @@ class TestXkcdBot(BotTestCase, DefaultTests):
bot_name = "xkcd"
def test_latest_command(self) -> None:
bot_response = ("#1866: **Russell's Teapot**\n"
"[Unfortunately, NASA regulations state that Bertrand Russell-related "
"payloads can only be launched within launch vehicles which do not launch "
"themselves.](https://imgs.xkcd.com/comics/russells_teapot.png)")
bot_response = (
"#1866: **Russell's Teapot**\n"
"[Unfortunately, NASA regulations state that Bertrand Russell-related "
"payloads can only be launched within launch vehicles which do not launch "
"themselves.](https://imgs.xkcd.com/comics/russells_teapot.png)"
)
with self.mock_http_conversation('test_latest'):
self.verify_reply('latest', bot_response)
def test_random_command(self) -> None:
bot_response = ("#1800: **Chess Notation**\n"
"[I've decided to score all my conversations using chess win-loss "
"notation. (??)](https://imgs.xkcd.com/comics/chess_notation.png)")
bot_response = (
"#1800: **Chess Notation**\n"
"[I've decided to score all my conversations using chess win-loss "
"notation. (??)](https://imgs.xkcd.com/comics/chess_notation.png)"
)
with self.mock_http_conversation('test_random'):
# Mock randint function.
with patch('zulip_bots.bots.xkcd.xkcd.random.randint') as randint:
@ -27,8 +31,10 @@ class TestXkcdBot(BotTestCase, DefaultTests):
self.verify_reply('random', bot_response)
def test_numeric_comic_id_command_1(self) -> None:
bot_response = ("#1: **Barrel - Part 1**\n[Don't we all.]"
"(https://imgs.xkcd.com/comics/barrel_cropped_(1).jpg)")
bot_response = (
"#1: **Barrel - Part 1**\n[Don't we all.]"
"(https://imgs.xkcd.com/comics/barrel_cropped_(1).jpg)"
)
with self.mock_http_conversation('test_specific_id'):
self.verify_reply('1', bot_response)
@ -36,20 +42,23 @@ class TestXkcdBot(BotTestCase, DefaultTests):
def test_invalid_comic_ids(self, mock_logging_exception: MagicMock) -> None:
invalid_id_txt = "Sorry, there is likely no xkcd comic strip with id: #"
for comic_id, fixture in (('0', 'test_not_existing_id_2'),
('999999999', 'test_not_existing_id')):
for comic_id, fixture in (
('0', 'test_not_existing_id_2'),
('999999999', 'test_not_existing_id'),
):
with self.mock_http_conversation(fixture):
self.verify_reply(comic_id, invalid_id_txt + comic_id)
def test_help_responses(self) -> None:
help_txt = "xkcd bot supports these commands:"
err_txt = "xkcd bot only supports these commands, not `{}`:"
err_txt = "xkcd bot only supports these commands, not `{}`:"
commands = '''
* `{0} help` to show this help message.
* `{0} latest` to fetch the latest comic strip from xkcd.
* `{0} random` to fetch a random comic strip from xkcd.
* `{0} <comic id>` to fetch a comic strip based on `<comic id>` e.g `{0} 1234`.'''.format(
"@**test-bot**")
"@**test-bot**"
)
self.verify_reply('', err_txt.format('') + commands)
self.verify_reply('help', help_txt + commands)
# Example invalid command

View file

@ -9,6 +9,7 @@ from zulip_bots.lib import BotHandler
XKCD_TEMPLATE_URL = 'https://xkcd.com/%s/info.0.json'
LATEST_XKCD_URL = 'https://xkcd.com/info.0.json'
class XkcdHandler:
'''
This plugin provides several commands that can be used for fetch a comic
@ -40,27 +41,33 @@ class XkcdHandler:
xkcd_bot_response = get_xkcd_bot_response(message, quoted_name)
bot_handler.send_reply(message, xkcd_bot_response)
class XkcdBotCommand:
LATEST = 0
RANDOM = 1
COMIC_ID = 2
class XkcdNotFoundError(Exception):
pass
class XkcdServerError(Exception):
pass
def get_xkcd_bot_response(message: Dict[str, str], quoted_name: str) -> str:
original_content = message['content'].strip()
command = original_content.strip()
commands_help = ("%s"
"\n* `{0} help` to show this help message."
"\n* `{0} latest` to fetch the latest comic strip from xkcd."
"\n* `{0} random` to fetch a random comic strip from xkcd."
"\n* `{0} <comic id>` to fetch a comic strip based on `<comic id>` "
"e.g `{0} 1234`.".format(quoted_name))
commands_help = (
"%s"
"\n* `{0} help` to show this help message."
"\n* `{0} latest` to fetch the latest comic strip from xkcd."
"\n* `{0} random` to fetch a random comic strip from xkcd."
"\n* `{0} <comic id>` to fetch a comic strip based on `<comic id>` "
"e.g `{0} 1234`.".format(quoted_name)
)
try:
if command == 'help':
@ -72,19 +79,25 @@ def get_xkcd_bot_response(message: Dict[str, str], quoted_name: str) -> str:
elif command.isdigit():
fetched = fetch_xkcd_query(XkcdBotCommand.COMIC_ID, command)
else:
return commands_help % ("xkcd bot only supports these commands, not `%s`:" % (command,),)
return commands_help % (
"xkcd bot only supports these commands, not `%s`:" % (command,),
)
except (requests.exceptions.ConnectionError, XkcdServerError):
logging.exception('Connection error occurred when trying to connect to xkcd server')
return 'Sorry, I cannot process your request right now, please try again later!'
except XkcdNotFoundError:
logging.exception('XKCD server responded 404 when trying to fetch comic with id %s'
% (command,))
logging.exception(
'XKCD server responded 404 when trying to fetch comic with id %s' % (command,)
)
return 'Sorry, there is likely no xkcd comic strip with id: #%s' % (command,)
else:
return ("#%s: **%s**\n[%s](%s)" % (fetched['num'],
fetched['title'],
fetched['alt'],
fetched['img']))
return "#%s: **%s**\n[%s](%s)" % (
fetched['num'],
fetched['title'],
fetched['alt'],
fetched['img'],
)
def fetch_xkcd_query(mode: int, comic_id: Optional[str] = None) -> Dict[str, str]:
try:
@ -120,4 +133,5 @@ def fetch_xkcd_query(mode: int, comic_id: Optional[str] = None) -> Dict[str, str
return xkcd_json
handler_class = XkcdHandler

View file

@ -35,39 +35,46 @@ class TestYodaBot(BotTestCase, DefaultTests):
def test_bot(self) -> None:
# Test normal sentence (1).
self._test('You will learn how to speak like me someday.',
"Learn how to speak like me someday, you will. Yes, hmmm.",
'test_1')
self._test(
'You will learn how to speak like me someday.',
"Learn how to speak like me someday, you will. Yes, hmmm.",
'test_1',
)
# Test normal sentence (2).
self._test('you still have much to learn',
"Much to learn, you still have.",
'test_2')
self._test('you still have much to learn', "Much to learn, you still have.", 'test_2')
# Test only numbers.
self._test('23456', "23456. Herh herh herh.",
'test_only_numbers')
self._test('23456', "23456. Herh herh herh.", 'test_only_numbers')
# Test help.
self._test('help', self.help_text)
# Test invalid input.
self._test('@#$%^&*',
"Invalid input, please check the sentence you have entered.",
'test_invalid_input')
self._test(
'@#$%^&*',
"Invalid input, please check the sentence you have entered.",
'test_invalid_input',
)
# Test 403 response.
self._test('You will learn how to speak like me someday.',
"Invalid Api Key. Did you follow the instructions in the `readme.md` file?",
'test_api_key_error')
self._test(
'You will learn how to speak like me someday.',
"Invalid Api Key. Did you follow the instructions in the `readme.md` file?",
'test_api_key_error',
)
# Test 503 response.
with self.assertRaises(ServiceUnavailableError):
self._test('You will learn how to speak like me someday.',
"The service is temporarily unavailable, please try again.",
'test_service_unavailable_error')
self._test(
'You will learn how to speak like me someday.',
"The service is temporarily unavailable, please try again.",
'test_service_unavailable_error',
)
# Test unknown response.
self._test('You will learn how to speak like me someday.',
"Unknown Error.Error code: 123 Did you follow the instructions in the `readme.md` file?",
'test_unknown_error')
self._test(
'You will learn how to speak like me someday.',
"Unknown Error.Error code: 123 Did you follow the instructions in the `readme.md` file?",
'test_unknown_error',
)

View file

@ -25,6 +25,7 @@ HELP_MESSAGE = '''
class ApiKeyError(Exception):
'''raise this when there is an error with the Mashape Api Key'''
class ServiceUnavailableError(Exception):
'''raise this when the service is unavailable.'''
@ -34,6 +35,7 @@ class YodaSpeakHandler:
This bot will allow users to translate a sentence into 'Yoda speak'.
It looks for messages starting with '@mention-bot'.
'''
def initialize(self, bot_handler: BotHandler) -> None:
self.api_key = bot_handler.get_config_info('yoda')['api_key']
@ -56,13 +58,11 @@ class YodaSpeakHandler:
def send_to_yoda_api(self, sentence: str) -> str:
# function for sending sentence to api
response = requests.get("https://yoda.p.mashape.com/yoda",
params=dict(sentence=sentence),
headers={
"X-Mashape-Key": self.api_key,
"Accept": "text/plain"
}
)
response = requests.get(
"https://yoda.p.mashape.com/yoda",
params=dict(sentence=sentence),
headers={"X-Mashape-Key": self.api_key, "Accept": "text/plain"},
)
if response.status_code == 200:
return response.json()['text']
@ -74,8 +74,12 @@ class YodaSpeakHandler:
error_message = response.json()['message']
logging.error(error_message)
error_code = response.status_code
error_message = error_message + 'Error code: ' + str(error_code) +\
' Did you follow the instructions in the `readme.md` file?'
error_message = (
error_message
+ 'Error code: '
+ str(error_code)
+ ' Did you follow the instructions in the `readme.md` file?'
)
return error_message
def format_input(self, original_content: str) -> str:
@ -104,19 +108,18 @@ class YodaSpeakHandler:
logging.error(reply_message)
except ApiKeyError:
reply_message = 'Invalid Api Key. Did you follow the instructions in the `readme.md` file?'
reply_message = (
'Invalid Api Key. Did you follow the instructions in the `readme.md` file?'
)
logging.error(reply_message)
bot_handler.send_reply(message, reply_message)
def send_message(self, bot_handler: BotHandler, message: str, stream: str, subject: str) -> None:
def send_message(
self, bot_handler: BotHandler, message: str, stream: str, subject: str
) -> None:
# function for sending a message
bot_handler.send_message(dict(
type='stream',
to=stream,
subject=subject,
content=message
))
bot_handler.send_message(dict(type='stream', to=stream, subject=subject, content=message))
def is_help(self, original_content: str) -> bool:
# gets rid of whitespace around the edges, so that they aren't a problem in the future
@ -126,4 +129,5 @@ class YodaSpeakHandler:
else:
return False
handler_class = YodaSpeakHandler

View file

@ -8,87 +8,102 @@ from zulip_bots.test_lib import BotTestCase, DefaultTests, StubBotHandler, get_b
class TestYoutubeBot(BotTestCase, DefaultTests):
bot_name = "youtube"
normal_config = {'key': '12345678',
'number_of_results': '5',
'video_region': 'US'} # type: Dict[str,str]
normal_config = {
'key': '12345678',
'number_of_results': '5',
'video_region': 'US',
} # type: Dict[str,str]
help_content = "*Help for YouTube bot* :robot_face: : \n\n" \
"The bot responds to messages starting with @mention-bot.\n\n" \
"`@mention-bot <search terms>` will return top Youtube video for the given `<search term>`.\n" \
"`@mention-bot top <search terms>` also returns the top Youtube result.\n" \
"`@mention-bot list <search terms>` will return a list Youtube videos for the given <search term>.\n \n" \
"Example:\n" \
" * @mention-bot funny cats\n" \
" * @mention-bot list funny dogs"
help_content = (
"*Help for YouTube bot* :robot_face: : \n\n"
"The bot responds to messages starting with @mention-bot.\n\n"
"`@mention-bot <search terms>` will return top Youtube video for the given `<search term>`.\n"
"`@mention-bot top <search terms>` also returns the top Youtube result.\n"
"`@mention-bot list <search terms>` will return a list Youtube videos for the given <search term>.\n \n"
"Example:\n"
" * @mention-bot funny cats\n"
" * @mention-bot list funny dogs"
)
# Override default function in BotTestCase
def test_bot_responds_to_empty_message(self) -> None:
with self.mock_config_info(self.normal_config), \
self.mock_http_conversation('test_keyok'):
with self.mock_config_info(self.normal_config), self.mock_http_conversation('test_keyok'):
self.verify_reply('', self.help_content)
def test_single(self) -> None:
bot_response = 'Here is what I found for `funny cats` : \n'\
'Cats are so funny you will die laughing - ' \
'Funny cat compilation - [Watch now](https://www.youtube.com/watch?v=5dsGWM5XGdg)'
bot_response = (
'Here is what I found for `funny cats` : \n'
'Cats are so funny you will die laughing - '
'Funny cat compilation - [Watch now](https://www.youtube.com/watch?v=5dsGWM5XGdg)'
)
with self.mock_config_info(self.normal_config), \
self.mock_http_conversation('test_single'):
with self.mock_config_info(self.normal_config), self.mock_http_conversation('test_single'):
self.verify_reply('funny cats', bot_response)
def test_invalid_key(self) -> None:
bot = get_bot_message_handler(self.bot_name)
bot_handler = StubBotHandler()
with self.mock_config_info({'key': 'somethinginvalid', 'number_of_results': '5', 'video_region': 'US'}), \
self.mock_http_conversation('test_invalid_key'), \
self.assertRaises(bot_handler.BotQuitException):
with self.mock_config_info(
{'key': 'somethinginvalid', 'number_of_results': '5', 'video_region': 'US'}
), self.mock_http_conversation('test_invalid_key'), self.assertRaises(
bot_handler.BotQuitException
):
bot.initialize(bot_handler)
def test_unknown_error(self) -> None:
bot = get_bot_message_handler(self.bot_name)
bot_handler = StubBotHandler()
with self.mock_config_info(self.normal_config), \
self.mock_http_conversation('test_unknown_error'), \
self.assertRaises(HTTPError):
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
'test_unknown_error'
), self.assertRaises(HTTPError):
bot.initialize(bot_handler)
def test_multiple(self) -> None:
get_bot_message_handler(self.bot_name)
StubBotHandler()
bot_response = 'Here is what I found for `marvel` : ' \
'\n * Marvel Studios\' Avengers: Infinity War Official Trailer - [Watch now](https://www.youtube.com/watch/6ZfuNTqbHE8)' \
'\n * Marvel Studios\' Black Panther - Official Trailer - [Watch now](https://www.youtube.com/watch/xjDjIWPwcPU)' \
'\n * MARVEL RISING BEGINS! | The Next Generation of Marvel Heroes (EXCLUSIVE) - [Watch now](https://www.youtube.com/watch/6HTPCTtkWoA)' \
'\n * Marvel Contest of Champions Taskmaster Spotlight - [Watch now](https://www.youtube.com/watch/-8uqxdcJ9WM)' \
'\n * 5* Crystal Opening! SO LUCKY! - Marvel Contest Of Champions - [Watch now](https://www.youtube.com/watch/l7rrsGKJ_O4)'
bot_response = (
'Here is what I found for `marvel` : '
'\n * Marvel Studios\' Avengers: Infinity War Official Trailer - [Watch now](https://www.youtube.com/watch/6ZfuNTqbHE8)'
'\n * Marvel Studios\' Black Panther - Official Trailer - [Watch now](https://www.youtube.com/watch/xjDjIWPwcPU)'
'\n * MARVEL RISING BEGINS! | The Next Generation of Marvel Heroes (EXCLUSIVE) - [Watch now](https://www.youtube.com/watch/6HTPCTtkWoA)'
'\n * Marvel Contest of Champions Taskmaster Spotlight - [Watch now](https://www.youtube.com/watch/-8uqxdcJ9WM)'
'\n * 5* Crystal Opening! SO LUCKY! - Marvel Contest Of Champions - [Watch now](https://www.youtube.com/watch/l7rrsGKJ_O4)'
)
with self.mock_config_info(self.normal_config), \
self.mock_http_conversation('test_multiple'):
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
'test_multiple'
):
self.verify_reply('list marvel', bot_response)
def test_noresult(self) -> None:
bot_response = 'Oops ! Sorry I couldn\'t find any video for `somethingrandomwithnoresult` ' \
':slightly_frowning_face:'
bot_response = (
'Oops ! Sorry I couldn\'t find any video for `somethingrandomwithnoresult` '
':slightly_frowning_face:'
)
with self.mock_config_info(self.normal_config), \
self.mock_http_conversation('test_noresult'):
self.verify_reply('somethingrandomwithnoresult', bot_response,)
with self.mock_config_info(self.normal_config), self.mock_http_conversation(
'test_noresult'
):
self.verify_reply(
'somethingrandomwithnoresult',
bot_response,
)
def test_help(self) -> None:
help_content = self.help_content
with self.mock_config_info(self.normal_config), \
self.mock_http_conversation('test_keyok'):
with self.mock_config_info(self.normal_config), self.mock_http_conversation('test_keyok'):
self.verify_reply('help', help_content)
self.verify_reply('list', help_content)
self.verify_reply('help list', help_content)
self.verify_reply('top', help_content)
def test_connection_error(self) -> None:
with self.mock_config_info(self.normal_config), \
patch('requests.get', side_effect=ConnectionError()), \
patch('logging.exception'):
self.verify_reply('Wow !', 'Uh-Oh, couldn\'t process the request '
'right now.\nPlease again later')
with self.mock_config_info(self.normal_config), patch(
'requests.get', side_effect=ConnectionError()
), patch('logging.exception'):
self.verify_reply(
'Wow !', 'Uh-Oh, couldn\'t process the request ' 'right now.\nPlease again later'
)

View file

@ -8,22 +8,25 @@ from zulip_bots.lib import BotHandler
commands_list = ('list', 'top', 'help')
class YoutubeHandler:
class YoutubeHandler:
def usage(self) -> str:
return '''
This plugin will allow users to search
for a given search term on Youtube.
Use '@mention-bot help' to get more information on the bot usage.
'''
help_content = "*Help for YouTube bot* :robot_face: : \n\n" \
"The bot responds to messages starting with @mention-bot.\n\n" \
"`@mention-bot <search terms>` will return top Youtube video for the given `<search term>`.\n" \
"`@mention-bot top <search terms>` also returns the top Youtube result.\n" \
"`@mention-bot list <search terms>` will return a list Youtube videos for the given <search term>.\n \n" \
"Example:\n" \
" * @mention-bot funny cats\n" \
" * @mention-bot list funny dogs"
help_content = (
"*Help for YouTube bot* :robot_face: : \n\n"
"The bot responds to messages starting with @mention-bot.\n\n"
"`@mention-bot <search terms>` will return top Youtube video for the given `<search term>`.\n"
"`@mention-bot top <search terms>` also returns the top Youtube result.\n"
"`@mention-bot list <search terms>` will return a list Youtube videos for the given <search term>.\n \n"
"Example:\n"
" * @mention-bot funny cats\n"
" * @mention-bot list funny dogs"
)
def initialize(self, bot_handler: BotHandler) -> None:
self.config_info = bot_handler.get_config_info('youtube')
@ -31,9 +34,10 @@ class YoutubeHandler:
try:
search_youtube('test', self.config_info['key'], self.config_info['video_region'])
except HTTPError as e:
if (e.response.json()['error']['errors'][0]['reason'] == 'keyInvalid'):
bot_handler.quit('Invalid key.'
'Follow the instructions in doc.md for setting API key.')
if e.response.json()['error']['errors'][0]['reason'] == 'keyInvalid':
bot_handler.quit(
'Invalid key.' 'Follow the instructions in doc.md for setting API key.'
)
else:
raise
except ConnectionError:
@ -45,15 +49,12 @@ class YoutubeHandler:
bot_handler.send_reply(message, self.help_content)
else:
cmd, query = get_command_query(message)
bot_response = get_bot_response(query,
cmd,
self.config_info)
bot_response = get_bot_response(query, cmd, self.config_info)
logging.info(bot_response.format())
bot_handler.send_reply(message, bot_response)
def search_youtube(query: str, key: str,
region: str, max_results: int = 1) -> List[List[str]]:
def search_youtube(query: str, key: str, region: str, max_results: int = 1) -> List[List[str]]:
videos = []
params = {
@ -63,7 +64,8 @@ def search_youtube(query: str, key: str,
'q': query,
'alt': 'json',
'type': 'video',
'regionCode': region} # type: Dict[str, Union[str, int]]
'regionCode': region,
} # type: Dict[str, Union[str, int]]
url = 'https://www.googleapis.com/youtube/v3/search'
try:
@ -77,8 +79,7 @@ def search_youtube(query: str, key: str,
# matching videos, channels, and playlists.
for search_result in search_response.get('items', []):
if search_result['id']['kind'] == 'youtube#video':
videos.append([search_result['snippet']['title'],
search_result['id']['videoId']])
videos.append([search_result['snippet']['title'], search_result['id']['videoId']])
return videos
@ -86,18 +87,20 @@ def get_command_query(message: Dict[str, str]) -> Tuple[Optional[str], str]:
blocks = message['content'].lower().split()
command = blocks[0]
if command in commands_list:
query = message['content'][len(command) + 1:].lstrip()
query = message['content'][len(command) + 1 :].lstrip()
return command, query
else:
return None, message['content']
def get_bot_response(query: Optional[str], command: Optional[str], config_info: Dict[str, str]) -> str:
def get_bot_response(
query: Optional[str], command: Optional[str], config_info: Dict[str, str]
) -> str:
key = config_info['key']
max_results = int(config_info['number_of_results'])
region = config_info['video_region']
video_list = [] # type: List[List[str]]
video_list = [] # type: List[List[str]]
try:
if query == '' or query is None:
return YoutubeHandler.help_content
@ -111,19 +114,23 @@ def get_bot_response(query: Optional[str], command: Optional[str], config_info:
return YoutubeHandler.help_content
except (ConnectionError, HTTPError):
return 'Uh-Oh, couldn\'t process the request ' \
'right now.\nPlease again later'
return 'Uh-Oh, couldn\'t process the request ' 'right now.\nPlease again later'
reply = 'Here is what I found for `' + query + '` : '
if len(video_list) == 0:
return 'Oops ! Sorry I couldn\'t find any video for `' + query + '` :slightly_frowning_face:'
return (
'Oops ! Sorry I couldn\'t find any video for `' + query + '` :slightly_frowning_face:'
)
elif len(video_list) == 1:
return (reply + '\n%s - [Watch now](https://www.youtube.com/watch?v=%s)' % (video_list[0][0], video_list[0][1])).strip()
return (
reply
+ '\n%s - [Watch now](https://www.youtube.com/watch?v=%s)'
% (video_list[0][0], video_list[0][1])
).strip()
for title, id in video_list:
reply = reply + \
'\n * %s - [Watch now](https://www.youtube.com/watch/%s)' % (title, id)
reply = reply + '\n * %s - [Watch now](https://www.youtube.com/watch/%s)' % (title, id)
# Using link https://www.youtube.com/watch/<id> to
# prevent showing multiple previews
return reply

View file

@ -4,6 +4,7 @@
# current architecture works by lib.py importing bots, not
# the other way around.
class ConfigValidationError(Exception):
'''
Raise if the config data passed to a bot's validate_config()

Some files were not shown because too many files have changed in this diff Show more