black: Reformat skipping string normalization.
This commit is contained in:
parent
5580c68ae5
commit
fba21bb00d
178 changed files with 6562 additions and 4469 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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),
|
||||
]
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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, '')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.'
|
||||
|
|
|
@ -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 {}'.format(d['type'], d['definition'], html2text.html2text(example))
|
||||
response += '\n' + '* (**{}**) {}\n {}'.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
|
||||
|
|
|
@ -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 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 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"
|
||||
" they helped her with domestic chores\n\n\n"
|
||||
"* (**verb**) serve someone with (food or drink).\n"
|
||||
" may I help you to some more meat?\n\n\n"
|
||||
"* (**verb**) cannot or could not avoid.\n"
|
||||
" he couldn't help laughing\n\n\n"
|
||||
"* (**noun**) the action of helping someone to do something.\n"
|
||||
" I asked for help from my neighbours\n\n\n"
|
||||
"* (**exclamation**) used as an appeal for urgent assistance.\n"
|
||||
" 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"
|
||||
" they helped her with domestic chores\n\n\n"
|
||||
"* (**verb**) serve someone with (food or drink).\n"
|
||||
" may I help you to some more meat?\n\n\n"
|
||||
"* (**verb**) cannot or could not avoid.\n"
|
||||
" he couldn't help laughing\n\n\n"
|
||||
"* (**noun**) the action of helping someone to do something.\n"
|
||||
" I asked for help from my neighbours\n\n\n"
|
||||
"* (**exclamation**) used as an appeal for urgent assistance.\n"
|
||||
" 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.')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.",
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:',
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)',
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: ',
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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))
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -50,4 +50,5 @@ class SusiHandler:
|
|||
answer = "I don't understand. Can you rephrase?"
|
||||
bot_handler.send_reply(message, answer)
|
||||
|
||||
|
||||
handler_class = SusiHandler
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue